initial commit

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-07-18 20:30:39 +08:00
commit bfcc98b50e
39 changed files with 2904 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "accountd"
version = "0.1.0"
edition = "2024"
[features]
airup = []
embed_init_file = []
[dependencies]
anyhow = "1"
cfg-if = "1"
bitflags = "2"
clap = { workspace = true }
rusqlite = "0.39"
rustc-hash = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
semios_account = { path = "../../lib/semios_account" }
uuid = { version = "1", features = ["v7"] }
tracing = "0.1"
tracing-subscriber = "0.3"
[dependencies.tokio]
version = "1"
features = ["rt", "net", "macros", "io-util", "process", "signal", "sync", "time"]
+1
View File
@@ -0,0 +1 @@
// Sandbox rules for running an authentication method.
+387
View File
@@ -0,0 +1,387 @@
//! Implementation of the API server.
use crate::{
AppState,
util::{
ipc::{Connection, Listener},
time::timestamp_s,
},
};
use rustc_hash::FxHashMap;
use semios_account::{
auth::{UserAuthMethodFlags, UserAuthMethodRecord},
error::Error,
protocol::*,
record::{GroupInfo, SecureTag, UserInfo},
secret::Secret,
};
use std::{fmt::Debug, path::PathBuf, pin::Pin, sync::Arc};
use uuid::Uuid;
pub async fn launch_server(app_state: Arc<AppState>) -> anyhow::Result<()> {
let context = Arc::new(ApiContext::new(app_state));
let listener = Listener::new(&semios_account::protocol::uri()).await?;
let server = Server { context, listener };
tokio::spawn(server.run());
Ok(())
}
macro_rules! compose {
($orig:ident) => {
Box::new(|state, caller, args| {
Box::pin(async move {
let args = match serde_json::from_value(args) {
Ok(x) => x,
Err(e) => return Err(Error::InvalidParams(e.to_string())),
};
$orig(state, caller, args)
.await
.map(|x| serde_json::to_value(x).expect("invalid api return value"))
}) as FutureMethod
}) as FnMethod
};
}
type FutureMethod = Pin<Box<dyn Future<Output = Result<serde_json::Value, Error>> + Send>>;
type FnMethod = Box<dyn Fn(Arc<AppState>, Caller, serde_json::Value) -> FutureMethod + Send + Sync>;
/// A context for API users.
pub struct ApiContext {
methods: FxHashMap<&'static str, FnMethod>,
app_state: Arc<AppState>,
}
impl ApiContext {
pub fn new(app_state: Arc<AppState>) -> Self {
let mut methods = FxHashMap::default();
methods.insert(GET_USER_INFO, compose!(get_user_info));
methods.insert(GET_GROUP_INFO, compose!(get_group_info));
methods.insert(LIST_USER, compose!(list_user));
methods.insert(LIST_GROUP, compose!(list_group));
methods.insert(CREATE_USER, compose!(create_user));
methods.insert(REMOVE_USER, compose!(remove_user));
methods.insert(CREATE_GROUP, compose!(create_group));
methods.insert(GET_SECRET, compose!(get_secret));
methods.insert(SET_SECRET, compose!(set_secret));
methods.insert(GET_USER_AUTH_METHODS, compose!(get_user_auth_methods));
methods.insert(USER_ADD_AUTH_METHOD, compose!(user_add_auth_method));
methods.insert(GET_AUTH_METHOD_PATH, compose!(get_auth_method_path));
Self { methods, app_state }
}
pub async fn invoke(&self, method: &str, caller: Caller, args: serde_json::Value) -> Response {
let Some(method) = self.methods.get(&method) else {
return Response::from_result::<()>(Err(Error::NotImplemented(method.into())));
};
Response::from_result(method(self.app_state.clone(), caller, args).await)
}
}
impl Debug for ApiContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiContext")
.field("methods", &self.methods.keys())
.field("app_state", &self.app_state)
.finish()
}
}
#[derive(Debug, Clone, Copy)]
pub enum Caller {
/// Unknown user. No privileges are granted.
Unknown,
/// Explicit user.
User(Uuid),
/// All privileges are granted, but no user is specified.
Immortal,
}
#[derive(Debug)]
pub struct Server {
context: Arc<ApiContext>,
listener: Listener,
}
impl Server {
pub async fn run(self) -> anyhow::Result<()> {
loop {
let connection = match self.listener.accept().await {
Ok(x) => x,
Err(e) => {
tracing::warn!("ipc_accept() failed: {e}");
continue;
}
};
let session = Session {
context: self.context.clone(),
connection,
caller: Caller::Unknown,
};
tokio::spawn(session.run());
}
}
}
#[derive(Debug)]
pub struct Session {
context: Arc<ApiContext>,
connection: Connection,
caller: Caller,
}
impl Session {
pub async fn run(mut self) -> anyhow::Result<()> {
let peer_uuid = match self.connection.peer_host_uid() {
Some(uid) => self.context.app_state.local_db.find_user_by_host_uid(uid),
None => None,
};
self.caller = match peer_uuid {
Some(uuid) => Caller::User(uuid),
None => Caller::Unknown,
};
let mut buf = Vec::with_capacity(1024);
loop {
self.connection.recv(&mut buf).await?;
let req: Request = serde_json::from_slice(&buf)?;
let resp = self
.context
.invoke(&req.method, self.caller, req.params)
.await;
buf.clear();
serde_json::to_writer(&mut buf, &resp).expect("invalid response emitted");
self.connection.send(&buf).await?;
}
}
}
async fn list_user(
state: Arc<AppState>,
caller: Caller,
args: ListUserArgs,
) -> Result<Vec<Uuid>, Error> {
Ok(state.local_db.list_user(args.start, args.len))
}
async fn list_group(
state: Arc<AppState>,
caller: Caller,
args: ListGroupArgs,
) -> Result<Vec<Uuid>, Error> {
Ok(state.local_db.list_group(args.start, args.len))
}
async fn get_user_info(
state: Arc<AppState>,
caller: Caller,
args: GetUserInfoArgs,
) -> Result<UserInfo, Error> {
let uuid = match args {
GetUserInfoArgs::Uuid(x) => x,
GetUserInfoArgs::HostUid(uid) => state
.local_db
.find_user_by_host_uid(uid)
.ok_or(Error::NoSuchUser)?,
GetUserInfoArgs::Username(name) => state
.local_db
.find_user_by_name(&name)
.ok_or(Error::NoSuchUser)?,
};
state.local_db.get_user_info(&uuid).ok_or(Error::NoSuchUser)
}
async fn get_group_info(
state: Arc<AppState>,
caller: Caller,
args: GetGroupInfoArgs,
) -> Result<GroupInfo, Error> {
let uuid = match args {
GetGroupInfoArgs::Uuid(x) => x,
GetGroupInfoArgs::HostGid(gid) => state
.local_db
.find_group_by_host_gid(gid)
.ok_or(Error::NoSuchGroup)?,
GetGroupInfoArgs::Groupname(name) => state
.local_db
.find_group_by_name(&name)
.ok_or(Error::NoSuchGroup)?,
};
state
.local_db
.get_group_info(&uuid)
.ok_or(Error::NoSuchGroup)
}
async fn create_user(
state: Arc<AppState>,
caller: Caller,
args: CreateUserArgs,
) -> Result<(), Error> {
require_secure_tags(&state, caller, &[SecureTag::CreateUser]).await?;
let user_info = UserInfo {
uuid: Uuid::new_v7(uuid::Timestamp::now(uuid::ContextV7::new())),
fullname: args.fullname.unwrap_or_else(|| args.username.clone()),
username: args.username,
host_uid: args.host_uid,
description: args.description,
secure_tags: args.secure_tags,
extra_records: args.extra_records,
defaults: args.defaults,
home_directory: args.home_directory,
creation_time: timestamp_s(),
last_login_time: 0,
};
state
.local_db
.insert_user_info(user_info)
.map_err(|_| Error::AlreadyExists)?;
Ok(())
}
async fn create_group(
state: Arc<AppState>,
caller: Caller,
args: CreateGroupArgs,
) -> Result<(), Error> {
require_secure_tags(&state, caller, &[SecureTag::CreateUser]).await?;
let group_info = GroupInfo {
uuid: Uuid::new_v7(uuid::Timestamp::now(uuid::ContextV7::new())),
fullname: args.fullname.unwrap_or_else(|| args.groupname.clone()),
groupname: args.groupname,
host_gid: args.host_gid,
description: args.description,
creation_time: timestamp_s(),
};
state
.local_db
.insert_group_info(group_info)
.map_err(|_| Error::AlreadyExists)?;
Ok(())
}
async fn remove_user(
state: Arc<AppState>,
caller: Caller,
args: RemoveUserArgs,
) -> Result<(), Error> {
require_secure_tags(&state, caller, &[SecureTag::RemoveUser]).await?;
todo!();
}
async fn get_secret(
state: Arc<AppState>,
caller: Caller,
args: GetSecretArgs,
) -> Result<Secret, Error> {
require_secure_tags(&state, caller, &[SecureTag::ReadSecret])
.await
.or(require_same_user(&state, caller, args.user).await)?;
let raw_secret = state
.local_db
.get_secret(&args.user, &args.name)
.ok_or(Error::NoSuchSecret)?;
Ok(raw_secret)
}
async fn set_secret(
state: Arc<AppState>,
caller: Caller,
args: SetSecretArgs,
) -> Result<(), Error> {
require_secure_tags(&state, caller, &[SecureTag::WriteSecret])
.await
.or(require_same_user(&state, caller, args.user).await)?;
let raw_secret = args.secret;
_ = state.local_db.delete_secret(&args.user, &raw_secret.name);
state.local_db.insert_secret(&args.user, raw_secret)?;
Ok(())
}
async fn get_user_auth_methods(
state: Arc<AppState>,
caller: Caller,
args: GetUserAuthMethodsArgs,
) -> Result<Vec<UserAuthMethodRecord>, Error> {
require_secure_tags(&state, Caller::User(args.user), &[SecureTag::Login]).await?;
state.local_db.get_user_auth_methods(&args.user)
}
async fn user_add_auth_method(
state: Arc<AppState>,
caller: Caller,
args: UserAddAuthMethodArgs,
) -> Result<(), Error> {
require_secure_tags(&state, Caller::User(args.user), &[SecureTag::Login]).await?;
let current = state.local_db.get_user_auth_methods(&args.user)?;
let exec = state
.auth_method(&args.auth_method)
.ok_or(Error::NoSuchAuthMethod)?;
let info = exec
.info()
.await
.map_err(|e| Error::Internal(e.to_string()))?;
let mut flags = UserAuthMethodFlags::empty();
if info.provides_master_key && current.is_empty() {
flags |= UserAuthMethodFlags::MAIN;
}
state
.local_db
.insert_user_auth_method(&args.user, &args.auth_method, flags)?;
Ok(())
}
async fn get_auth_method_path(
state: Arc<AppState>,
caller: Caller,
args: GetAuthMethodPathArgs,
) -> Result<PathBuf, Error> {
Ok(state
.auth_method(&args.name)
.ok_or(Error::NoSuchAuthMethod)?
.path()
.into())
}
// ==- Helpers -==
async fn require_same_user(
state: &AppState,
caller: Caller,
requested_user: Uuid,
) -> Result<(), Error> {
let peer_user = match caller {
Caller::Unknown => return Err(Error::PermissionDenied),
Caller::User(uuid) => uuid,
Caller::Immortal => return Ok(()),
};
if requested_user == peer_user {
Ok(())
} else {
Err(Error::PermissionDenied)
}
}
async fn require_secure_tags(
state: &AppState,
caller: Caller,
tags: &[SecureTag],
) -> Result<(), Error> {
let peer_user = match caller {
Caller::Unknown => return Err(Error::PermissionDenied),
Caller::User(uuid) => uuid,
Caller::Immortal => return Ok(()),
};
let user_info = state
.local_db
.get_user_info(&peer_user)
.ok_or(Error::PermissionDenied)?;
for tag in tags {
if !user_info.secure_tags.contains(tag) {
return Err(Error::PermissionDenied);
}
}
Ok(())
}
+57
View File
@@ -0,0 +1,57 @@
use anyhow::anyhow;
use semios_account::auth::{AuthCli, AuthMethodInfo};
use std::{
path::{Path, PathBuf},
process::Stdio,
};
#[derive(Debug)]
pub struct AuthExec(PathBuf);
impl AuthExec {
pub fn from_path_buf(path_buf: PathBuf) -> Self {
Self(path_buf)
}
pub fn name(&self) -> anyhow::Result<String> {
let err = || anyhow!("invalid auth exec \"{}\"", self.0.display());
self.0
.file_name()
.ok_or_else(err)?
.to_string_lossy()
.strip_prefix("auth_")
.ok_or_else(err)
.map(Into::into)
}
pub async fn info(&self) -> anyhow::Result<AuthMethodInfo> {
let wait = tokio::process::Command::new(&self.0)
.args(AuthCli::QueryInformation.compose())
.stdout(Stdio::piped())
.spawn()?
.wait_with_output()
.await?;
if !wait.status.success() {
return Err(anyhow!(
"process \"{}\" exited with status code {}",
self.0.display(),
wait.status
));
}
let parsed: AuthMethodInfo = serde_json::from_slice(&wait.stdout)?;
if parsed.name != self.name()? {
return Err(anyhow!(
"mismatched filename \"{}\" for auth method \"{}\"",
self.name()?,
parsed.name
));
}
Ok(parsed)
}
pub fn path(&self) -> &Path {
&self.0
}
}
+67
View File
@@ -0,0 +1,67 @@
//! Initialization on first run.
use crate::{
AppState,
api::{ApiContext, Caller},
};
use anyhow::anyhow;
use semios_account::protocol::Request;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[cfg(feature = "embed_init_file")]
const EMBEDDED: &str = include_str!("../../../build_config/init.json");
#[cfg(not(feature = "embed_init_file"))]
const EMBEDDED: &str = "[]";
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InitCommand {
pub on_failure: OnFailure,
pub request: Request,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum OnFailure {
Terminate,
Warn,
Skip,
}
pub async fn run(app_state: Arc<AppState>) -> anyhow::Result<()> {
let init_file: Vec<InitCommand> = serde_json::from_str(EMBEDDED)?;
let api_context = ApiContext::new(app_state);
for cmd in init_file {
let resp = api_context
.invoke(
&cmd.request.method,
Caller::Immortal,
cmd.request.params.clone(),
)
.await;
if resp.success {
tracing::debug!(
"Successfully ran init_command {:?} with response {:?}",
cmd,
resp
);
} else {
match cmd.on_failure {
OnFailure::Terminate => {
tracing::error!("Failed to run init_command {:?}: {:?}", cmd, resp);
return Err(anyhow!(
"An init command failed to run, see logs for details"
));
}
OnFailure::Warn => {
tracing::warn!("Failed to run init_command {:?}: {:?}", cmd, resp);
}
OnFailure::Skip => {
tracing::debug!("Failed to run init_command {:?}: {:?}", cmd, resp);
}
}
}
}
Ok(())
}
+391
View File
@@ -0,0 +1,391 @@
use rusqlite::params;
use semios_account::{
auth::{UserAuthMethodFlags, UserAuthMethodRecord},
error::Error,
record::{GroupInfo, HostGid, HostUid, UserInfo},
secret::{Secret, SecurityFlags},
};
use serde::de::DeserializeOwned;
use std::{path::Path, sync::Mutex};
use uuid::Uuid;
/// Schema of the database.
///
/// The "user" table has columns:
/// - "user_uuid": UUID of the user.
/// - "user_name": Name of the user.
/// - "user_host_uid": Host UID of the user.
/// - "user_info": User information JSON (contains host specific information).
///
/// The "group" table has columns:
/// - "group_uuid": UUID of the group.
/// - "group_name": Name of the group.
/// - "group_host_gid": Host GID of the group.
/// - "group_info": Group information JSON (contains host specific information).
///
/// The "kv_store" table is used to store automatic configurations that are not edited by user.
///
/// The "secret" table has columns:
/// - "secret_owner": Owner user of the secret, in UUID text.
/// - "secret_name": Name of the secret.
/// - "secret_data": Data of the secret.
/// - "secret_security_flags": Secret security flags.
/// - "secret_creation_time": Timestamp the secret is created.
/// - "secret_expiration_time": Timestamp the secret will be expired.
///
/// The "group_membership" table has columns:
/// - "group_membership_user": UUID of the user in the record.
/// - "group_membership_group": UUID of the group in the record.
///
/// The "user_auth" table has columns:
/// - "user_auth_user": UUID of the user in the record.
/// - "user_auth_method": Name of the auth method specified in the record.
/// - "user_auth_flags": Flags of the record.
const SCHEMA: &str = "
CREATE TABLE IF NOT EXISTS \"user\"(
\"user_uuid\" TEXT PRIMARY KEY,
\"user_name\" TEXT NOT NULL UNIQUE,
\"user_host_uid\" TEXT NOT NULL UNIQUE,
\"user_info\" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS \"group\"(
\"group_uuid\" TEXT PRIMARY KEY,
\"group_name\" TEXT NOT NULL UNIQUE,
\"group_host_gid\" TEXT NOT NULL UNIQUE,
\"group_info\" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS \"kv_store\"(
\"kv_store_key\" TEXT PRIMARY KEY,
\"kv_store_value\" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS \"secret\"(
\"secret_owner\" TEXT,
\"secret_name\" TEXT,
\"secret_data\" BLOB NOT NULL,
\"secret_security_flags\" INTEGER NOT NULL,
\"secret_creation_time\" INTEGER NOT NULL,
\"secret_expiration_time\" INTEGER NOT NULL,
PRIMARY KEY (\"secret_owner\", \"secret_name\")
);
CREATE TABLE IF NOT EXISTS \"group_membership\"(
\"group_membership_user\" TEXT NOT NULL,
\"group_membership_group\" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS \"user_auth\"(
\"user_auth_user\" TEXT NOT NULL,
\"user_auth_method\" TEXT NOT NULL,
\"user_auth_flags\" INTEGER NOT NULL
);
";
#[derive(Debug)]
pub struct LocalDb(Mutex<rusqlite::Connection>);
impl LocalDb {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let connection = rusqlite::Connection::open(path).map_err(Error::make_internal)?;
connection
.execute_batch(SCHEMA)
.map_err(Error::make_internal)?;
Ok(Self(Mutex::new(connection)))
}
pub fn get_secret(&self, owner: &Uuid, name: &str) -> Option<Secret> {
let lock = self.0.lock().unwrap();
let mut stmt = lock
.prepare_cached(
"SELECT * FROM \"secret\" WHERE \"secret_owner\" = ?1 AND \"secret_name\" = ?2",
)
.ok()?;
stmt.query_one(params![owner.to_string(), name], |row| {
Ok(Secret {
name: name.into(),
data: row.get("secret_data")?,
security_flags: SecurityFlags::from_bits_retain(row.get("secret_security_flags")?),
creation_time: row.get("secret_creation_time")?,
expiration_time: row.get("secret_expiration_time")?,
})
})
.ok()
}
pub fn insert_secret(&self, owner: &Uuid, secret: Secret) -> Result<(), Error> {
let mut lock = self.0.lock().unwrap();
execute(
&mut *lock,
"INSERT INTO \"secret\"(
\"secret_owner\",
\"secret_name\",
\"secret_data\",
\"secret_security_flags\",
\"secret_creation_time\",
\"secret_expiration_time\"
) VALUES(?1, ?2, ?3, ?4, ?5, ?6)",
params![
owner.to_string(),
secret.name,
secret.data,
secret.security_flags.bits(),
secret.creation_time,
secret.expiration_time
],
)
}
pub fn delete_secret(&self, uuid: &Uuid, name: &str) -> Result<(), Error> {
execute(
&mut *self.0.lock().unwrap(),
"DELETE FROM \"secret\" WHERE \"secret_owner\" = ?1 AND \"secret_name\" = ?2",
params![uuid.to_string(), name],
)
}
pub fn list_user(&self, start: u32, len: u32) -> Vec<Uuid> {
self._list_user_group("user", start, len)
}
pub fn list_group(&self, start: u32, len: u32) -> Vec<Uuid> {
self._list_user_group("group", start, len)
}
pub fn get_user_info(&self, uuid: &Uuid) -> Option<UserInfo> {
self._get_user_group_info("user", uuid)
}
pub fn get_group_info(&self, uuid: &Uuid) -> Option<GroupInfo> {
self._get_user_group_info("group", uuid)
}
pub fn insert_user_info(&self, user_info: UserInfo) -> Result<(), Error> {
let mut lock = self.0.lock().unwrap();
execute(
&mut *lock,
"INSERT INTO \"user\"(\"user_uuid\", \"user_name\", \"user_host_uid\", \"user_info\")
VALUES(?1, ?2, ?3, ?4)",
params![
user_info.uuid.to_string(),
user_info.username,
user_info
.host_uid
.ok_or_else(|| Error::make_internal("host_uid not specified"))?,
serde_json::to_string(&user_info).map_err(Error::make_internal)?,
],
)
}
pub fn delete_user(&self, uuid: &Uuid) -> Result<(), Error> {
let mut lock = self.0.lock().unwrap();
execute(
&mut *lock,
"DELETE FROM \"user\" WHERE \"user_uuid\" = ?1",
params![uuid.to_string()],
)?;
execute(
&mut *lock,
"DELETE FROM \"secret\" WHERE \"secret_owner\" = ?1",
params![uuid.to_string()],
)?;
execute(
&mut *lock,
"DELETE FROM \"group_membership\" WHERE \"group_membership_user\" = ?1",
params![uuid.to_string()],
)?;
execute(
&mut *lock,
"DELETE FROM \"user_auth\" WHERE \"user_auth_user\" = ?1",
params![uuid.to_string()],
)?;
Ok(())
}
pub fn insert_group_info(&self, group_info: GroupInfo) -> Result<(), Error> {
let mut lock = self.0.lock().unwrap();
execute(
&mut *lock,
"INSERT INTO \"group\"(\"group_uuid\", \"group_name\", \"group_host_gid\", \"group_info\")
VALUES(?1, ?2, ?3, ?4)",
params![
group_info.uuid.to_string(),
group_info.groupname,
group_info
.host_gid
.ok_or_else(|| Error::make_internal("host_gid not specified"))?,
serde_json::to_string(&group_info).map_err(Error::make_internal)?,
],
)
}
pub fn delete_group(&self, uuid: &Uuid) -> Result<(), Error> {
let mut lock = self.0.lock().unwrap();
execute(
&mut *lock,
"DELETE FROM \"group\" WHERE \"group_uuid\" = ?1",
params![uuid.to_string()],
)?;
execute(
&mut *lock,
"DELETE FROM \"group_membership\" WHERE \"group_membership_group\" = ?1",
params![uuid.to_string()],
)?;
Ok(())
}
pub fn get_user_auth_methods(&self, uuid: &Uuid) -> Result<Vec<UserAuthMethodRecord>, Error> {
let lock = self.0.lock().unwrap();
let mut stmt = lock
.prepare_cached("SELECT * FROM \"user_auth\" WHERE \"user_auth_user\" = ?1")
.unwrap();
let mut rows = stmt
.query(params![uuid.to_string()])
.map_err(Error::make_internal)?;
let mut all = Vec::new();
while let Ok(Some(i)) = rows.next() {
let name = i.get("user_auth_method").map_err(Error::make_internal)?;
let flags = i.get("user_auth_flags").map_err(Error::make_internal)?;
all.push(UserAuthMethodRecord {
name,
flags: UserAuthMethodFlags::from_bits_retain(flags),
});
}
Ok(all)
}
pub fn insert_user_auth_method(
&self,
uuid: &Uuid,
method: &str,
flags: UserAuthMethodFlags,
) -> Result<(), Error> {
let mut lock = self.0.lock().unwrap();
execute(
&mut *lock,
"INSERT INTO \"user_auth\"(\"user_auth_user\", \"user_auth_method\", \"user_auth_flags\")
VALUES(?1, ?2, ?3)",
params![uuid.to_string(), method, flags.bits()],
)
}
pub fn find_user_by_name(&self, username: &str) -> Option<Uuid> {
self._find_uuid_by("user", "user_name", &username)
}
pub fn find_group_by_name(&self, groupname: &str) -> Option<Uuid> {
self._find_uuid_by("group", "group_name", &groupname)
}
pub fn find_user_by_host_uid(&self, host_uid: HostUid) -> Option<Uuid> {
self._find_uuid_by("user", "user_host_uid", &host_uid.to_string())
}
pub fn find_group_by_host_gid(&self, host_gid: HostGid) -> Option<Uuid> {
self._find_uuid_by("group", "group_host_gid", &host_gid.to_string())
}
pub fn is_first_run(&self) -> bool {
!self._kv_get("initialized").is_some()
}
pub fn unset_first_run(&self) -> Result<(), Error> {
self._kv_set("initialized", "1")
.map_err(Error::make_internal)
}
fn _list_user_group(&self, table: &'static str, start: u32, len: u32) -> Vec<Uuid> {
let lock = self.0.lock().unwrap();
let stmt = lock.prepare_cached(&format!(
"SELECT \"{table}_uuid\" FROM \"{table}\" ORDER BY \"{table}_uuid\" LIMIT ?1 OFFSET ?2"
));
let Ok(mut stmt) = stmt else {
return Vec::new();
};
stmt.query_map(params![len, start], |x| {
x.get::<_, String>(&format!("{table}_uuid")[..])?
.parse::<Uuid>()
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))
})
.map(|x| x.filter_map(|y| y.ok()).collect())
.unwrap_or_default()
}
fn _get_user_group_info<T: DeserializeOwned>(
&self,
table: &'static str,
uuid: &Uuid,
) -> Option<T> {
let lock = self.0.lock().unwrap();
let mut stmt = lock
.prepare_cached(&format!(
"SELECT * FROM \"{table}\" WHERE \"{table}_uuid\" = ?1"
))
.ok()?;
stmt.query_one(params![uuid.to_string()], |rows| {
serde_json::from_str(&rows.get::<_, String>(&format!("{table}_info")[..])?)
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))
})
.ok()
}
fn _find_uuid_by<T: rusqlite::types::ToSql>(
&self,
table: &'static str,
column: &'static str,
val: &T,
) -> Option<Uuid> {
let lock = self.0.lock().unwrap();
let mut stmt = lock
.prepare_cached(&format!(
"SELECT \"{table}_uuid\" FROM \"{table}\" WHERE \"{column}\" = ?1"
))
.ok()?;
stmt.query_one(params![val], |rows| {
rows.get::<_, String>(&format!("{table}_uuid")[..])
})
.ok()?
.parse::<Uuid>()
.ok()
}
fn _kv_get(&self, key: &str) -> Option<String> {
let lock = self.0.lock().unwrap();
let mut stmt = lock
.prepare_cached(
"SELECT \"kv_store_value\" FROM \"kv_store\" WHERE \"kv_store_key\" = ?1",
)
.ok()?;
stmt.query_one(params![key], |rows| rows.get("kv_store_value"))
.ok()
}
fn _kv_set(&self, key: &str, val: &str) -> rusqlite::Result<()> {
let lock = self.0.lock().unwrap();
let mut stmt_get = lock.prepare_cached(
"SELECT \"kv_store_key\" FROM \"kv_store\" WHERE \"kv_store_key\" = ?1",
)?;
let mut stmt_set = lock.prepare_cached(
"UPDATE \"kv_store\" SET \"kv_store_value\" = ?1 WHERE \"kv_store_key\" = ?2",
)?;
let mut stmt_insert = lock.prepare_cached(
"INSERT INTO \"kv_store\"(\"kv_store_key\", \"kv_store_value\") VALUES(?1, ?2)",
)?;
if stmt_get.query_one(params![key], |_| Ok(())).is_ok() {
stmt_set.execute(params![val, key])?;
} else {
stmt_insert.execute(params![key, val])?;
}
Ok(())
}
}
fn execute(
conn: &mut rusqlite::Connection,
sql: &str,
params: &[&dyn rusqlite::ToSql],
) -> Result<(), Error> {
let mut stmt = conn.prepare_cached(sql).map_err(Error::make_internal)?;
stmt.execute(params).map_err(Error::make_internal)?;
Ok(())
}
+107
View File
@@ -0,0 +1,107 @@
mod api;
mod auth;
mod init_file;
mod local;
mod secret;
mod util;
use crate::{local::LocalDb, secret::ResidentKeys};
use clap::Parser;
use std::{path::PathBuf, sync::Arc};
const DEFAULT_LIBEXEC_DIR: &str = match std::option_env!("DEFAULT_LIBEXEC_DIR") {
Some(x) => x,
None => "/usr/libexec/accountd",
};
const DEFAULT_DATA_DIR: &str = match std::option_env!("DEFAULT_DATA_DIR") {
Some(x) => x,
None => "/var/lib/accountd",
};
#[derive(Debug)]
struct AppState {
/// The `accountd` dedicated libexec directory, e.g. `/usr/libexec/accountd`.
libexec_dir: PathBuf,
/// The `accountd` dedicated data directory, e.g. `/var/lib/accountd`.
data_dir: PathBuf,
/// Local database.
local_db: LocalDb,
/// Resident keys.
resident_keys: ResidentKeys,
}
impl AppState {
fn auth_methods(&self) -> Box<dyn Iterator<Item = auth::AuthExec>> {
let Ok(tree_dir) = util::fs::TreeDir::open(&self.libexec_dir) else {
return Box::new(std::iter::empty());
};
Box::new(
tree_dir
.filter(|x| x.file_name().to_string_lossy().starts_with("auth_"))
.map(|x| auth::AuthExec::from_path_buf(x.path())),
)
}
fn auth_method(&self, name: &str) -> Option<auth::AuthExec> {
self.auth_methods()
.find(|x| matches!(x.name().as_deref(), Ok(x) if x == name))
}
}
#[derive(Debug, Clone, Parser)]
struct Cli {
/// Specify libexec directory
#[arg(long)]
libexec_dir: Option<PathBuf>,
/// Specify data directory
#[arg(long)]
data_dir: Option<PathBuf>,
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let cli = Cli::parse();
tracing_subscriber::fmt::init();
let libexec_dir = cli
.libexec_dir
.unwrap_or_else(|| DEFAULT_LIBEXEC_DIR.into());
let data_dir = cli.data_dir.unwrap_or_else(|| DEFAULT_DATA_DIR.into());
_ = std::fs::create_dir_all(&data_dir);
let local_db_path = data_dir.join("local.db");
let local_db = match LocalDb::open(&local_db_path) {
Ok(x) => x,
Err(e) => {
tracing::error!("failed to open local database: {e}",);
std::process::exit(1);
}
};
let app_state = Arc::new(AppState {
libexec_dir,
data_dir,
local_db,
resident_keys: ResidentKeys::new(),
});
if app_state.local_db.is_first_run() {
if init_file::run(app_state.clone()).await.is_err() {
std::process::exit(1);
}
if let Err(e) = app_state.local_db.unset_first_run() {
tracing::error!("failed to unset first run: {e}");
std::process::exit(1);
}
}
if let Err(e) = api::launch_server(app_state).await {
tracing::error!("failed to launch api server: {e}");
std::process::exit(1);
}
while tokio::signal::ctrl_c().await.is_err() {}
}
+31
View File
@@ -0,0 +1,31 @@
use crate::util::time::timestamp_s;
use rustc_hash::FxHashMap;
use semios_account::secret::MasterKeyProvision;
use std::sync::Mutex;
use uuid::Uuid;
#[derive(Debug, Default)]
pub struct ResidentKeys(Mutex<FxHashMap<Uuid, MasterKeyProvision>>);
impl ResidentKeys {
pub fn new() -> Self {
Self::default()
}
pub fn set(&self, uuid: Uuid, provision: MasterKeyProvision) {
self.0.lock().unwrap().insert(uuid, provision);
}
pub fn get(&self, uuid: &Uuid) -> Option<MasterKeyProvision> {
let mut lock = self.0.lock().unwrap();
let provision = lock.get(uuid)?;
if provision.expiration_time < timestamp_s() {
lock.remove(uuid);
return None;
}
Some(provision.clone())
}
pub fn remove(&self, uuid: &Uuid) {
self.0.lock().unwrap().remove(uuid);
}
}
+39
View File
@@ -0,0 +1,39 @@
use std::{
fs::{DirEntry, ReadDir},
path::Path,
};
#[derive(Debug)]
pub struct TreeDir(Vec<ReadDir>);
impl TreeDir {
pub fn open<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
Ok(Self(vec![std::fs::read_dir(path)?]))
}
}
impl Iterator for TreeDir {
type Item = DirEntry;
fn next(&mut self) -> Option<Self::Item> {
while let Some(current_dir) = self.0.last_mut() {
match current_dir.next() {
Some(Ok(entry)) => {
if let Ok(file_type) = entry.file_type() {
if file_type.is_dir() {
if let Ok(sub_dir) = std::fs::read_dir(entry.path()) {
self.0.push(sub_dir);
}
}
}
return Some(entry);
}
Some(Err(_)) => {
continue;
}
None => {
self.0.pop();
}
}
}
None
}
}
+74
View File
@@ -0,0 +1,74 @@
//! Local IPC implementation -- Unix-like Operating Systems.
use anyhow::anyhow;
use cfg_if::cfg_if;
use semios_account::{
protocol::{MAX_MESSAGE_LEN, Uri},
record::HostUid,
};
use std::os::unix::net::{SocketAddr, UnixListener as StdUnixListener};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{UnixListener, UnixStream},
};
#[derive(Debug)]
pub struct Listener(UnixListener);
impl Listener {
pub async fn new(uri: &str) -> anyhow::Result<Self> {
let uri: Uri = uri.parse()?;
let sockaddr = match uri {
Uri::Unix(path) => {
if path.exists() && tokio::net::UnixStream::connect(&path).await.is_err() {
std::fs::remove_file(&path)?;
}
SocketAddr::from_pathname(path)?
}
Uri::UnixAbstract(_name) => {
cfg_if! {
if #[cfg(target_os = "linux")] {
use std::os::linux::net::SocketAddrExt;
SocketAddr::from_abstract_name(_name.as_bytes())?
} else {
return Err(anyhow!("abstract socket names are only supported on Linux"));
}
}
}
_ => return Err(anyhow!("only \"unix://\" ipc is supported on unix")),
};
let listener = StdUnixListener::bind_addr(&sockaddr)?;
listener.set_nonblocking(true)?;
Ok(Self(UnixListener::from_std(listener)?))
}
pub async fn accept(&self) -> anyhow::Result<Connection> {
Ok(Connection(self.0.accept().await?.0))
}
}
#[derive(Debug)]
pub struct Connection(UnixStream);
impl Connection {
pub async fn recv(&mut self, data: &mut Vec<u8>) -> anyhow::Result<()> {
let len = self.0.read_u32_le().await? as usize;
if len > MAX_MESSAGE_LEN {
return Err(anyhow!("message too large: {len} bytes"));
}
data.resize(len, 0);
self.0.read_exact(data).await?;
Ok(())
}
pub async fn send(&mut self, data: &[u8]) -> anyhow::Result<()> {
if data.len() > MAX_MESSAGE_LEN {
return Err(anyhow!("message too large: {} bytes", data.len()));
}
self.0.write_u32_le(data.len() as u32).await?;
self.0.write_all(data).await?;
Ok(())
}
pub fn peer_host_uid(&self) -> Option<HostUid> {
Some(self.0.peer_cred().ok()?.uid())
}
}
+13
View File
@@ -0,0 +1,13 @@
pub mod fs;
pub mod time;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(target_family = "unix")] {
#[path = "ipc_unix.rs"]
pub mod ipc;
} else {
std::compile_error!("target not supported");
}
}
+15
View File
@@ -0,0 +1,15 @@
use std::time::SystemTime;
macro_rules! timestamp {
($f:ident) => {{
let now = SystemTime::now();
match now.duration_since(SystemTime::UNIX_EPOCH) {
Ok(x) => x.$f() as i64,
Err(_) => -(SystemTime::UNIX_EPOCH.duration_since(now).unwrap().$f() as i64),
}
}};
}
pub fn timestamp_s() -> i64 {
timestamp!(as_secs)
}