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<u32>,
}
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<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GroupRecord {
pub gid: u32,
pub name: String,
pub members: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UserConfig {
pub uid: Option<u32>,
pub gid: Option<u32>,
pub euid: Option<u32>,
pub egid: Option<u32>,
pub username: Option<String>,
pub homedir: Option<String>,
pub shell: Option<String>,
pub gecos: Option<String>,
pub group_name: Option<String>,
pub supplementary_gids: Vec<u32>,
pub accounts: Vec<UserAccount>,
pub groups: Vec<GroupRecord>,
}
#[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<u32>,
accounts_by_uid: BTreeMap<u32, UserAccount>,
account_uids_by_name: BTreeMap<String, u32>,
groups_by_gid: BTreeMap<u32, GroupRecord>,
group_gids_by_name: BTreeMap<String, u32>,
}
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<u32> {
self.supplementary_gids.clone()
}
pub fn getpwuid(&self, uid: u32) -> Option<String> {
self.accounts_by_uid.get(&uid).map(render_passwd)
}
pub fn getpwnam(&self, username: &str) -> Option<String> {
self.account_uids_by_name
.get(username)
.and_then(|uid| self.getpwuid(*uid))
}
pub fn getgrgid(&self, gid: u32) -> Option<String> {
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::<Vec<_>>();
(!members.is_empty()).then(|| format!("group{gid}:x:{gid}:{}", members.join(",")))
})
}
pub fn getgrnam(&self, name: &str) -> Option<String> {
self.group_gids_by_name
.get(name)
.and_then(|gid| self.getgrgid(*gid))
}
pub fn passwd_entries(&self) -> Vec<String> {
self.accounts_by_uid.values().map(render_passwd).collect()
}
pub fn group_entries(&self) -> Vec<String> {
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<u32>) -> Vec<u32> {
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
}