Skip to main content

agentos_kernel/
user.rs

1use std::collections::BTreeMap;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct ProcessIdentity {
5    pub uid: u32,
6    pub gid: u32,
7    pub euid: u32,
8    pub egid: u32,
9    pub suid: u32,
10    pub sgid: u32,
11    pub supplementary_gids: Vec<u32>,
12}
13
14impl Default for ProcessIdentity {
15    fn default() -> Self {
16        Self {
17            uid: 1000,
18            gid: 1000,
19            euid: 1000,
20            egid: 1000,
21            suid: 1000,
22            sgid: 1000,
23            supplementary_gids: vec![1000],
24        }
25    }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct UserAccount {
30    pub uid: u32,
31    pub gid: u32,
32    pub username: String,
33    pub homedir: String,
34    pub shell: String,
35    pub gecos: String,
36    pub supplementary_gids: Vec<u32>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct GroupRecord {
41    pub gid: u32,
42    pub name: String,
43    pub members: Vec<String>,
44}
45
46#[derive(Debug, Clone, Default, PartialEq, Eq)]
47pub struct UserConfig {
48    pub uid: Option<u32>,
49    pub gid: Option<u32>,
50    pub euid: Option<u32>,
51    pub egid: Option<u32>,
52    pub username: Option<String>,
53    pub homedir: Option<String>,
54    pub shell: Option<String>,
55    pub gecos: Option<String>,
56    pub group_name: Option<String>,
57    /// Supplementary groups are VM configuration, not guest-mutable state.
58    /// The primary gid is always injected and duplicate gids are dropped.
59    pub supplementary_gids: Vec<u32>,
60    pub accounts: Vec<UserAccount>,
61    pub groups: Vec<GroupRecord>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct UserManager {
66    pub uid: u32,
67    pub gid: u32,
68    pub euid: u32,
69    pub egid: u32,
70    pub username: String,
71    pub homedir: String,
72    pub shell: String,
73    pub gecos: String,
74    pub group_name: String,
75    pub supplementary_gids: Vec<u32>,
76    accounts_by_uid: BTreeMap<u32, UserAccount>,
77    account_uids_by_name: BTreeMap<String, u32>,
78    groups_by_gid: BTreeMap<u32, GroupRecord>,
79    group_gids_by_name: BTreeMap<String, u32>,
80}
81
82impl Default for UserManager {
83    fn default() -> Self {
84        Self::from_config(UserConfig::default())
85    }
86}
87
88impl UserManager {
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    pub fn from_config(config: UserConfig) -> Self {
94        let uid = config.uid.unwrap_or(1000);
95        let gid = config.gid.unwrap_or(1000);
96        let username = config.username.unwrap_or_else(|| String::from("agentos"));
97        let supplementary_gids = normalize_supplementary_gids(gid, config.supplementary_gids);
98
99        let primary_account = UserAccount {
100            uid,
101            gid,
102            username: username.clone(),
103            homedir: config
104                .homedir
105                .clone()
106                .unwrap_or_else(|| String::from("/home/agentos")),
107            shell: config
108                .shell
109                .clone()
110                .unwrap_or_else(|| String::from("/bin/sh")),
111            gecos: config.gecos.clone().unwrap_or_default(),
112            supplementary_gids: supplementary_gids.clone(),
113        };
114        let mut accounts_by_uid = BTreeMap::new();
115        let mut account_uids_by_name = BTreeMap::new();
116        for mut account in config.accounts {
117            account.supplementary_gids =
118                normalize_supplementary_gids(account.gid, account.supplementary_gids);
119            account_uids_by_name.insert(account.username.clone(), account.uid);
120            accounts_by_uid.insert(account.uid, account);
121        }
122        account_uids_by_name.insert(primary_account.username.clone(), primary_account.uid);
123        accounts_by_uid.insert(primary_account.uid, primary_account.clone());
124
125        let primary_group_name = config.group_name.unwrap_or_else(|| username.clone());
126        let mut groups_by_gid = BTreeMap::new();
127        let mut group_gids_by_name = BTreeMap::new();
128        for group in config.groups {
129            group_gids_by_name.insert(group.name.clone(), group.gid);
130            groups_by_gid.insert(group.gid, group);
131        }
132        groups_by_gid.entry(gid).or_insert_with(|| GroupRecord {
133            gid,
134            name: primary_group_name.clone(),
135            members: vec![username.clone()],
136        });
137        group_gids_by_name.insert(primary_group_name.clone(), gid);
138
139        Self {
140            uid,
141            gid,
142            euid: config.euid.unwrap_or(uid),
143            egid: config.egid.unwrap_or(gid),
144            username: username.clone(),
145            homedir: primary_account.homedir,
146            shell: primary_account.shell,
147            gecos: primary_account.gecos,
148            group_name: primary_group_name,
149            supplementary_gids,
150            accounts_by_uid,
151            account_uids_by_name,
152            groups_by_gid,
153            group_gids_by_name,
154        }
155    }
156
157    pub fn identity(&self) -> ProcessIdentity {
158        ProcessIdentity {
159            uid: self.uid,
160            gid: self.gid,
161            euid: self.euid,
162            egid: self.egid,
163            suid: self.euid,
164            sgid: self.egid,
165            supplementary_gids: self.supplementary_gids.clone(),
166        }
167    }
168
169    pub fn getgroups(&self) -> Vec<u32> {
170        self.supplementary_gids.clone()
171    }
172
173    pub fn getpwuid(&self, uid: u32) -> Option<String> {
174        self.accounts_by_uid.get(&uid).map(render_passwd)
175    }
176
177    pub fn getpwnam(&self, username: &str) -> Option<String> {
178        self.account_uids_by_name
179            .get(username)
180            .and_then(|uid| self.getpwuid(*uid))
181    }
182
183    pub fn getgrgid(&self, gid: u32) -> Option<String> {
184        self.groups_by_gid.get(&gid).map(render_group).or_else(|| {
185            let members = self
186                .accounts_by_uid
187                .values()
188                .filter(|account| account.supplementary_gids.contains(&gid))
189                .map(|account| account.username.as_str())
190                .collect::<Vec<_>>();
191            (!members.is_empty()).then(|| format!("group{gid}:x:{gid}:{}", members.join(",")))
192        })
193    }
194
195    pub fn getgrnam(&self, name: &str) -> Option<String> {
196        self.group_gids_by_name
197            .get(name)
198            .and_then(|gid| self.getgrgid(*gid))
199    }
200
201    pub fn passwd_entries(&self) -> Vec<String> {
202        self.accounts_by_uid.values().map(render_passwd).collect()
203    }
204
205    pub fn group_entries(&self) -> Vec<String> {
206        self.groups_by_gid.values().map(render_group).collect()
207    }
208
209    pub fn account(&self, uid: u32) -> Option<&UserAccount> {
210        self.accounts_by_uid.get(&uid)
211    }
212}
213
214fn render_passwd(account: &UserAccount) -> String {
215    format!(
216        "{}:x:{}:{}:{}:{}:{}",
217        account.username, account.uid, account.gid, account.gecos, account.homedir, account.shell
218    )
219}
220
221fn render_group(group: &GroupRecord) -> String {
222    format!("{}:x:{}:{}", group.name, group.gid, group.members.join(","))
223}
224
225fn normalize_supplementary_gids(primary_gid: u32, supplementary_gids: Vec<u32>) -> Vec<u32> {
226    let mut normalized = Vec::with_capacity(supplementary_gids.len() + 1);
227    normalized.push(primary_gid);
228    for gid in supplementary_gids {
229        if !normalized.contains(&gid) {
230            normalized.push(gid);
231        }
232    }
233    normalized
234}