From c72134a89e6d41b4df9313e6464af23805b46842 Mon Sep 17 00:00:00 2001 From: sisungo Date: Sun, 19 Jul 2026 13:05:26 +0800 Subject: [PATCH] feat: add host id allocation Signed-off-by: sisungo --- bin/account/src/create_group.rs | 38 ++++++++++++++++++++++++++++++ bin/account/src/create_user.rs | 9 +++++-- bin/account/src/main.rs | 5 ++++ daemon/accountd/src/api.rs | 26 +++++++++++++++++--- daemon/accountd/src/local.rs | 35 +++++++++++++++++++++++++-- lib/semios_account/src/client.rs | 4 ++++ lib/semios_account/src/error.rs | 4 ++++ lib/semios_account/src/protocol.rs | 12 ++++++++-- 8 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 bin/account/src/create_group.rs diff --git a/bin/account/src/create_group.rs b/bin/account/src/create_group.rs new file mode 100644 index 0000000..80a6a99 --- /dev/null +++ b/bin/account/src/create_group.rs @@ -0,0 +1,38 @@ +use crate::client; +use clap::Parser; +use semios_account::{ + protocol::{CreateGroupArgs, HostIdRule}, + record::HostGid, +}; + +#[derive(Debug, Clone, Parser)] +pub struct Cli { + #[arg(short = 'D', long, default_value_t)] + description: String, + + #[arg(short = 'N', long)] + fullname: Option, + + #[arg(short = 'G', long)] + host_gid: Option, + + groupname: String, +} + +pub fn main(cli: Cli) -> anyhow::Result<()> { + let mut client = client()?; + + let host_gid = match cli.host_gid { + Some(x) => HostIdRule::Manual(x), + None => HostIdRule::Auto(1000, HostGid::MAX), + }; + + client.create_group(CreateGroupArgs { + groupname: cli.groupname, + fullname: cli.fullname, + host_gid, + description: cli.description, + })?; + + Ok(()) +} diff --git a/bin/account/src/create_user.rs b/bin/account/src/create_user.rs index fa336b2..63d8b01 100644 --- a/bin/account/src/create_user.rs +++ b/bin/account/src/create_user.rs @@ -4,7 +4,7 @@ use crate::client; use anyhow::anyhow; use clap::Parser; use semios_account::{ - protocol::CreateUserArgs, + protocol::{CreateUserArgs, HostIdRule}, record::{HostUid, SecureTag}, wellknown, }; @@ -67,10 +67,15 @@ pub fn main(cli: Cli) -> anyhow::Result<()> { let mut defaults = HashMap::new(); defaults.insert(wellknown::CLI_LOGIN_SHELL.into(), cli.cli_shell); + let host_uid = match cli.host_uid { + Some(x) => HostIdRule::Manual(x), + None => HostIdRule::Auto(1000, HostUid::MAX), + }; + client.create_user(CreateUserArgs { username: cli.username, fullname: cli.fullname, - host_uid: cli.host_uid, + host_uid, description: cli.description, secure_tags, extra_records: Default::default(), diff --git a/bin/account/src/main.rs b/bin/account/src/main.rs index d3fa2f8..1c1aec8 100644 --- a/bin/account/src/main.rs +++ b/bin/account/src/main.rs @@ -1,4 +1,5 @@ mod authenticate; +mod create_group; mod create_user; mod info; mod update_auth; @@ -15,6 +16,9 @@ pub enum Cli { /// Create new user CreateUser(create_user::Cli), + /// Create new group + CreateGroup(create_group::Cli), + /// Authenticate user Authenticate(authenticate::Cli), @@ -28,6 +32,7 @@ fn main() { let result = match cli { Cli::Info(cli) => info::main(cli), Cli::CreateUser(cli) => create_user::main(cli), + Cli::CreateGroup(cli) => create_group::main(cli), Cli::Authenticate(cli) => authenticate::main(cli), Cli::UpdateAuth(cli) => update_auth::main(cli), }; diff --git a/daemon/accountd/src/api.rs b/daemon/accountd/src/api.rs index fb732de..9bb7ae2 100644 --- a/daemon/accountd/src/api.rs +++ b/daemon/accountd/src/api.rs @@ -15,7 +15,7 @@ use semios_account::{ record::{GroupInfo, SecureTag, UserInfo}, secret::Secret, }; -use std::{fmt::Debug, path::PathBuf, pin::Pin, sync::Arc}; +use std::{fmt::Debug, path::PathBuf, pin::Pin, range::RangeInclusive, sync::Arc}; use uuid::Uuid; pub async fn launch_server(app_state: Arc) -> anyhow::Result<()> { @@ -215,11 +215,21 @@ async fn create_user( args: CreateUserArgs, ) -> Result<(), Error> { require_secure_tags(&state, caller, &[SecureTag::CreateUser]).await?; + let host_uid = match args.host_uid { + HostIdRule::Unallocated => None, + HostIdRule::Manual(x) => Some(x), + HostIdRule::Auto(a, b) => Some( + state + .local_db + .allocate_host_uid(RangeInclusive::from(a..=b)) + .ok_or(Error::AllocHostId)?, + ), + }; 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, + host_uid, description: args.description, secure_tags: args.secure_tags, extra_records: args.extra_records, @@ -241,11 +251,21 @@ async fn create_group( args: CreateGroupArgs, ) -> Result<(), Error> { require_secure_tags(&state, caller, &[SecureTag::CreateUser]).await?; + let host_gid = match args.host_gid { + HostIdRule::Unallocated => None, + HostIdRule::Manual(x) => Some(x), + HostIdRule::Auto(a, b) => Some( + state + .local_db + .allocate_host_gid(RangeInclusive::from(a..=b)) + .ok_or(Error::AllocHostId)?, + ), + }; 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, + host_gid, description: args.description, creation_time: timestamp_s(), }; diff --git a/daemon/accountd/src/local.rs b/daemon/accountd/src/local.rs index deb3895..28c6850 100644 --- a/daemon/accountd/src/local.rs +++ b/daemon/accountd/src/local.rs @@ -1,4 +1,4 @@ -use rusqlite::params; +use rusqlite::{OptionalExtension, ToSql, params, types::FromSql}; use semios_account::{ auth::{UserAuthMethodFlags, UserAuthMethodRecord}, error::Error, @@ -6,7 +6,7 @@ use semios_account::{ secret::{Secret, SecurityFlags}, }; use serde::de::DeserializeOwned; -use std::{path::Path, sync::Mutex}; +use std::{path::Path, range::RangeInclusive, sync::Mutex}; use uuid::Uuid; /// Schema of the database. @@ -285,6 +285,18 @@ impl LocalDb { self._find_uuid_by("group", "group_host_gid", &host_gid.to_string()) } + pub fn allocate_host_uid(&self, range: RangeInclusive) -> Option { + self._allocate_host_id_max("user", "user_host_uid", range) + .ok() + .map(|x| x.map(|y| y + 1).unwrap_or(range.start)) + } + + pub fn allocate_host_gid(&self, range: RangeInclusive) -> Option { + self._allocate_host_id_max("group", "group_host_gid", range) + .ok() + .map(|x| x.map(|y| y + 1).unwrap_or(range.start)) + } + pub fn is_first_run(&self) -> bool { !self._kv_get("initialized").is_some() } @@ -349,6 +361,25 @@ impl LocalDb { .ok() } + fn _allocate_host_id_max( + &self, + table: &'static str, + column: &'static str, + range: RangeInclusive, + ) -> rusqlite::Result> { + let lock = self.0.lock().unwrap(); + let mut stmt = lock.prepare_cached(&format!( + "SELECT MAX(CAST(\"{column}\" AS INTEGER)) + FROM \"{table}\" + WHERE CAST(\"{column}\" AS INTEGER) BETWEEN ?1 AND ?2" + ))?; + let max: Option = stmt.query_one(params![range.start, range.last], |row| row.get(0))?; + if max == Some(range.last) { + return Ok(None); + } + Ok(max) + } + fn _kv_get(&self, key: &str) -> Option { let lock = self.0.lock().unwrap(); let mut stmt = lock diff --git a/lib/semios_account/src/client.rs b/lib/semios_account/src/client.rs index d5b8b67..e653c41 100644 --- a/lib/semios_account/src/client.rs +++ b/lib/semios_account/src/client.rs @@ -92,4 +92,8 @@ impl Client { pub fn create_user(&mut self, args: CreateUserArgs) -> Result<(), Error> { self.invoke(CREATE_USER.into(), args) } + + pub fn create_group(&mut self, args: CreateGroupArgs) -> Result<(), Error> { + self.invoke(CREATE_GROUP.into(), args) + } } diff --git a/lib/semios_account/src/error.rs b/lib/semios_account/src/error.rs index f0c2859..32bd02f 100644 --- a/lib/semios_account/src/error.rs +++ b/lib/semios_account/src/error.rs @@ -44,6 +44,10 @@ pub enum Error { #[error("the specified auth method is not activated for the user")] NotActivatedAuthMethod, + /// Cannot allocate host ID + #[error("cannot allocate host id")] + AllocHostId, + /// TODO #[error("already exists")] AlreadyExists, diff --git a/lib/semios_account/src/protocol.rs b/lib/semios_account/src/protocol.rs index b116cb7..9b62453 100644 --- a/lib/semios_account/src/protocol.rs +++ b/lib/semios_account/src/protocol.rs @@ -184,7 +184,7 @@ pub struct CreateUserArgs { pub fullname: Option, /// Specify a host UID. - pub host_uid: Option, + pub host_uid: HostIdRule, /// User description. #[serde(default)] @@ -225,7 +225,7 @@ pub struct CreateGroupArgs { pub fullname: Option, /// Group ID on current host. - pub host_gid: Option, + pub host_gid: HostIdRule, /// Group description. #[serde(default)] @@ -238,6 +238,14 @@ pub struct RemoveGroupArgs { pub uuid: Uuid, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum HostIdRule { + Unallocated, + Auto(T, T), + Manual(T), +} + // ==- Methods: Authentication -== pub const GET_USER_AUTH_METHODS: &str = "GetUserAuthMethods";