use std::collections::BTreeMap; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProcessIdentity { pub uid: u32, pub gid: u32, pub euid: u32, pub egid: u32, pub suid: u32, pub sgid: u32, pub supplementary_gids: Vec, } impl Default for ProcessIdentity { fn default() -> Self { Self { uid: 1000, gid: 1000, euid: 1000, egid: 1000, suid: 1000, sgid: 1000, supplementary_gids: vec![1000], } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct UserAccount { pub uid: u32, pub gid: u32, pub username: String, pub homedir: String, pub shell: String, pub gecos: String, pub supplementary_gids: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct GroupRecord { pub gid: u32, pub name: String, pub members: Vec, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct UserConfig { pub uid: Option, pub gid: Option, pub euid: Option, pub egid: Option, pub username: Option, pub homedir: Option, pub shell: Option, pub gecos: Option, pub group_name: Option, /// Supplementary groups are VM configuration, not guest-mutable state. /// The primary gid is always injected and duplicate gids are dropped. pub supplementary_gids: Vec, pub accounts: Vec, pub groups: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct UserManager { pub uid: u32, pub gid: u32, pub euid: u32, pub egid: u32, pub username: String, pub homedir: String, pub shell: String, pub gecos: String, pub group_name: String, pub supplementary_gids: Vec, accounts_by_uid: BTreeMap, account_uids_by_name: BTreeMap, groups_by_gid: BTreeMap, group_gids_by_name: BTreeMap, } impl Default for UserManager { fn default() -> Self { Self::from_config(UserConfig::default()) } } impl UserManager { pub fn new() -> Self { Self::default() } pub fn from_config(config: UserConfig) -> Self { let uid = config.uid.unwrap_or(1000); let gid = config.gid.unwrap_or(1000); let username = config.username.unwrap_or_else(|| String::from("agentos")); let supplementary_gids = normalize_supplementary_gids(gid, config.supplementary_gids); let primary_account = UserAccount { uid, gid, username: username.clone(), homedir: config .homedir .clone() .unwrap_or_else(|| String::from("/home/agentos")), shell: config .shell .clone() .unwrap_or_else(|| String::from("/bin/sh")), gecos: config.gecos.clone().unwrap_or_default(), supplementary_gids: supplementary_gids.clone(), }; let mut accounts_by_uid = BTreeMap::new(); let mut account_uids_by_name = BTreeMap::new(); for mut account in config.accounts { account.supplementary_gids = normalize_supplementary_gids(account.gid, account.supplementary_gids); account_uids_by_name.insert(account.username.clone(), account.uid); accounts_by_uid.insert(account.uid, account); } account_uids_by_name.insert(primary_account.username.clone(), primary_account.uid); accounts_by_uid.insert(primary_account.uid, primary_account.clone()); let primary_group_name = config.group_name.unwrap_or_else(|| username.clone()); let mut groups_by_gid = BTreeMap::new(); let mut group_gids_by_name = BTreeMap::new(); for group in config.groups { group_gids_by_name.insert(group.name.clone(), group.gid); groups_by_gid.insert(group.gid, group); } groups_by_gid.entry(gid).or_insert_with(|| GroupRecord { gid, name: primary_group_name.clone(), members: vec![username.clone()], }); group_gids_by_name.insert(primary_group_name.clone(), gid); Self { uid, gid, euid: config.euid.unwrap_or(uid), egid: config.egid.unwrap_or(gid), username: username.clone(), homedir: primary_account.homedir, shell: primary_account.shell, gecos: primary_account.gecos, group_name: primary_group_name, supplementary_gids, accounts_by_uid, account_uids_by_name, groups_by_gid, group_gids_by_name, } } pub fn identity(&self) -> ProcessIdentity { ProcessIdentity { uid: self.uid, gid: self.gid, euid: self.euid, egid: self.egid, suid: self.euid, sgid: self.egid, supplementary_gids: self.supplementary_gids.clone(), } } pub fn getgroups(&self) -> Vec { self.supplementary_gids.clone() } pub fn getpwuid(&self, uid: u32) -> Option { self.accounts_by_uid.get(&uid).map(render_passwd) } pub fn getpwnam(&self, username: &str) -> Option { self.account_uids_by_name .get(username) .and_then(|uid| self.getpwuid(*uid)) } pub fn getgrgid(&self, gid: u32) -> Option { self.groups_by_gid.get(&gid).map(render_group).or_else(|| { let members = self .accounts_by_uid .values() .filter(|account| account.supplementary_gids.contains(&gid)) .map(|account| account.username.as_str()) .collect::>(); (!members.is_empty()).then(|| format!("group{gid}:x:{gid}:{}", members.join(","))) }) } pub fn getgrnam(&self, name: &str) -> Option { self.group_gids_by_name .get(name) .and_then(|gid| self.getgrgid(*gid)) } pub fn passwd_entries(&self) -> Vec { self.accounts_by_uid.values().map(render_passwd).collect() } pub fn group_entries(&self) -> Vec { self.groups_by_gid.values().map(render_group).collect() } pub fn account(&self, uid: u32) -> Option<&UserAccount> { self.accounts_by_uid.get(&uid) } } fn render_passwd(account: &UserAccount) -> String { format!( "{}:x:{}:{}:{}:{}:{}", account.username, account.uid, account.gid, account.gecos, account.homedir, account.shell ) } fn render_group(group: &GroupRecord) -> String { format!("{}:x:{}:{}", group.name, group.gid, group.members.join(",")) } fn normalize_supplementary_gids(primary_gid: u32, supplementary_gids: Vec) -> Vec { let mut normalized = Vec::with_capacity(supplementary_gids.len() + 1); normalized.push(primary_gid); for gid in supplementary_gids { if !normalized.contains(&gid) { normalized.push(gid); } } normalized }