Skip to main content

fs_mistrust/
user.rs

1//! Code to inspect user db information on unix.
2
3#[cfg(feature = "serde")]
4mod serde_support;
5
6use crate::Error;
7use std::sync::LazyLock;
8use std::{
9    collections::HashMap,
10    ffi::{OsStr, OsString},
11    io,
12    sync::Mutex,
13};
14
15use pwd_grp::{PwdGrp, PwdGrpProvider};
16
17/// uids and gids, convenient type alias
18type Id = u32;
19
20/// Cache for the trusted uid/gid answers
21#[derive(Default, Debug)]
22struct TrustedUsersCache<U: PwdGrpProvider> {
23    /// The passwd/group provider (possibly mocked)
24    pwd_grp: U,
25    /// Cached trusted uid determination
26    trusted_uid: HashMap<TrustedUser, Option<Id>>,
27    /// Cached trusted gid determination
28    trusted_gid: HashMap<TrustedGroup, Option<Id>>,
29}
30
31/// Cached trusted id determinations
32///
33/// Caching here saves time - including passwd/group lookups, which can be slow enough
34/// we don't want to do them often.
35///
36/// It isn't 100% correct since we don't track changes to the passwd/group databases.
37/// That might not be OK everywhere, but it is OK in this application.
38static CACHE: LazyLock<Mutex<TrustedUsersCache<PwdGrp>>> =
39    LazyLock::new(|| Mutex::new(TrustedUsersCache::default()));
40
41/// Convert an [`io::Error `] representing a user/group handling failure into an [`Error`]
42fn handle_pwd_error(e: io::Error) -> Error {
43    Error::PasswdGroupIoError(e.into())
44}
45
46/// Obtain the gid of a group named after the current user
47fn get_self_named_gid_impl<U: PwdGrpProvider>(userdb: &U) -> io::Result<Option<u32>> {
48    let Some(username) = get_own_username(userdb)? else {
49        return Ok(None);
50    };
51
52    let Some(group) = userdb.getgrnam::<Vec<u8>>(username)? else {
53        return Ok(None);
54    };
55
56    // TODO: Perhaps we should enforce a requirement that the group contains
57    // _only_ the current users.  That's kinda tricky to do, though, without
58    // walking the entire user db.
59
60    Ok(if cur_groups()?.contains(&group.gid) {
61        Some(group.gid)
62    } else {
63        None
64    })
65}
66
67/// Find our username, if possible.
68///
69/// By default, we look for the USER environment variable, and see whether we an
70/// find a user db entry for that username with a UID that matches our own.
71///
72/// Failing that, we look for a user entry for our current UID.
73fn get_own_username<U: PwdGrpProvider>(userdb: &U) -> io::Result<Option<Vec<u8>>> {
74    use std::os::unix::ffi::OsStringExt as _;
75
76    let my_uid = userdb.getuid();
77
78    if let Some(username) = std::env::var_os("USER") {
79        let username = username.into_vec();
80        if let Some(passwd) = userdb.getpwnam::<Vec<u8>>(&username)? {
81            if passwd.uid == my_uid {
82                return Ok(Some(username));
83            }
84        }
85    }
86
87    if let Some(passwd) = userdb.getpwuid(my_uid)? {
88        // This check should always pass, but let's be extra careful.
89        if passwd.uid == my_uid {
90            return Ok(Some(passwd.name));
91        }
92    }
93
94    Ok(None)
95}
96
97/// Return a vector of the group ID values for every group to which we belong.
98fn cur_groups() -> io::Result<Vec<u32>> {
99    PwdGrp.getgroups()
100}
101
102/// A user that we can be configured to trust.
103///
104/// # Serde support
105///
106/// If this crate is build with the `serde1` feature enabled, you can serialize
107/// and deserialize this type from any of the following:
108///
109///  * `false` and the string `":none"` correspond to `TrustedUser::None`.
110///  * The string `":current"` and the map `{ special = ":current" }` correspond
111///    to `TrustedUser::Current`.
112///  * A numeric value (e.g., `413`) and the map `{ id = 413 }` correspond to
113///    `TrustedUser::Id(413)`.
114///  * A string not starting with `:` (e.g., "jane") and the map `{ name = "jane" }`
115///    correspond to `TrustedUser::Name("jane".into())`.
116///
117/// ## Limitations
118///
119/// Non-UTF8 usernames cannot currently be represented in all serde formats.
120/// Notably, toml doesn't support them.
121#[derive(Clone, Default, Debug, Eq, PartialEq, Hash)]
122#[cfg_attr(
123    feature = "serde",
124    derive(serde::Serialize, serde::Deserialize),
125    serde(try_from = "serde_support::Serde", into = "serde_support::Serde")
126)]
127#[non_exhaustive]
128pub enum TrustedUser {
129    /// We won't treat any user as trusted.
130    None,
131    /// Treat the current user as trusted.
132    #[default]
133    Current,
134    /// Treat the user with a particular UID as trusted.
135    Id(u32),
136    /// Treat a user with a particular name as trusted.
137    ///
138    /// If there is no such user, we'll report an error.
139    //
140    // TODO change type of TrustedUser::Name.0 to Vec<u8> ? (also TrustedGroup)
141    // This is a Unix-only module.  Arguably we shouldn't be using the OsString
142    // type which is super-inconvenient and only really exists because on Windows
143    // the environment, arguments, and filenames, are WTF-16.
144    Name(OsString),
145}
146
147impl From<u32> for TrustedUser {
148    fn from(val: u32) -> Self {
149        TrustedUser::Id(val)
150    }
151}
152impl From<OsString> for TrustedUser {
153    fn from(val: OsString) -> Self {
154        TrustedUser::Name(val)
155    }
156}
157impl From<&OsStr> for TrustedUser {
158    fn from(val: &OsStr) -> Self {
159        val.to_owned().into()
160    }
161}
162impl From<String> for TrustedUser {
163    fn from(val: String) -> Self {
164        OsString::from(val).into()
165    }
166}
167impl From<&str> for TrustedUser {
168    fn from(val: &str) -> Self {
169        val.to_owned().into()
170    }
171}
172
173impl TrustedUser {
174    /// Try to convert this `User` into an optional UID.
175    pub(crate) fn get_uid(&self) -> Result<Option<u32>, Error> {
176        let mut cache = CACHE.lock().expect("poisoned lock");
177        if let Some(got) = cache.trusted_uid.get(self) {
178            return Ok(*got);
179        }
180        let calculated = self.get_uid_impl(&cache.pwd_grp)?;
181        cache.trusted_uid.insert(self.clone(), calculated);
182        Ok(calculated)
183    }
184    /// As `get_uid`, but take a userdb.
185    fn get_uid_impl<U: PwdGrpProvider>(&self, userdb: &U) -> Result<Option<u32>, Error> {
186        use std::os::unix::ffi::OsStrExt as _;
187
188        match self {
189            TrustedUser::None => Ok(None),
190            TrustedUser::Current => Ok(Some(userdb.getuid())),
191            TrustedUser::Id(id) => Ok(Some(*id)),
192            TrustedUser::Name(name) => userdb
193                .getpwnam(name.as_bytes())
194                .map_err(handle_pwd_error)?
195                .map(|u: pwd_grp::Passwd<Vec<u8>>| Some(u.uid))
196                .ok_or_else(|| Error::NoSuchUser(name.to_string_lossy().into_owned())),
197        }
198    }
199}
200
201/// A group that we can be configured to trust.
202///
203/// # Serde support
204///
205/// See the `serde support` section in [`TrustedUser`].  Additionally,
206/// you can represent `TrustedGroup::SelfNamed` with the string `":username"`
207/// or the map `{ special = ":username" }`.
208#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
209#[cfg_attr(
210    feature = "serde",
211    derive(serde::Serialize, serde::Deserialize),
212    serde(try_from = "serde_support::Serde", into = "serde_support::Serde")
213)]
214#[non_exhaustive]
215pub enum TrustedGroup {
216    /// We won't treat any group as trusted
217    None,
218    /// We'll treat any group with same name as the current user as trusted.
219    ///
220    /// If there is no such group, we trust no group.
221    ///
222    /// (This is the default.)
223    #[default]
224    SelfNamed,
225    /// We'll treat a specific group ID as trusted.
226    Id(u32),
227    /// We'll treat a group with a specific name as trusted.
228    ///
229    /// If there is no such group, we'll report an error.
230    Name(OsString),
231}
232
233impl From<u32> for TrustedGroup {
234    fn from(val: u32) -> Self {
235        TrustedGroup::Id(val)
236    }
237}
238impl From<OsString> for TrustedGroup {
239    fn from(val: OsString) -> TrustedGroup {
240        TrustedGroup::Name(val)
241    }
242}
243impl From<&OsStr> for TrustedGroup {
244    fn from(val: &OsStr) -> TrustedGroup {
245        val.to_owned().into()
246    }
247}
248impl From<String> for TrustedGroup {
249    fn from(val: String) -> TrustedGroup {
250        OsString::from(val).into()
251    }
252}
253impl From<&str> for TrustedGroup {
254    fn from(val: &str) -> TrustedGroup {
255        val.to_owned().into()
256    }
257}
258
259impl TrustedGroup {
260    /// Try to convert this `Group` into an optional GID.
261    pub(crate) fn get_gid(&self) -> Result<Option<u32>, Error> {
262        let mut cache = CACHE.lock().expect("poisoned lock");
263        if let Some(got) = cache.trusted_gid.get(self) {
264            return Ok(*got);
265        }
266        let calculated = self.get_gid_impl(&cache.pwd_grp)?;
267        cache.trusted_gid.insert(self.clone(), calculated);
268        Ok(calculated)
269    }
270    /// Like `get_gid`, but take a user db as an argument.
271    fn get_gid_impl<U: PwdGrpProvider>(&self, userdb: &U) -> Result<Option<u32>, Error> {
272        use std::os::unix::ffi::OsStrExt as _;
273
274        match self {
275            TrustedGroup::None => Ok(None),
276            TrustedGroup::SelfNamed => get_self_named_gid_impl(userdb).map_err(handle_pwd_error),
277            TrustedGroup::Id(id) => Ok(Some(*id)),
278            TrustedGroup::Name(name) => userdb
279                .getgrnam(name.as_bytes())
280                .map_err(handle_pwd_error)?
281                .map(|g: pwd_grp::Group<Vec<u8>>| Some(g.gid))
282                .ok_or_else(|| Error::NoSuchGroup(name.to_string_lossy().into_owned())),
283        }
284    }
285}
286
287#[cfg(test)]
288mod test {
289    // @@ begin test lint list maintained by maint/add_warning @@
290    #![allow(clippy::bool_assert_comparison)]
291    #![allow(clippy::clone_on_copy)]
292    #![allow(clippy::dbg_macro)]
293    #![allow(clippy::mixed_attributes_style)]
294    #![allow(clippy::print_stderr)]
295    #![allow(clippy::print_stdout)]
296    #![allow(clippy::single_char_pattern)]
297    #![allow(clippy::unwrap_used)]
298    #![allow(clippy::unchecked_time_subtraction)]
299    #![allow(clippy::useless_vec)]
300    #![allow(clippy::needless_pass_by_value)]
301    #![allow(clippy::string_slice)] // See arti#2571
302    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
303    use super::*;
304    use pwd_grp::mock::MockPwdGrpProvider;
305    type Id = u32;
306
307    fn mock_users() -> MockPwdGrpProvider {
308        let mock = MockPwdGrpProvider::new();
309        mock.set_uids(413.into());
310        mock
311    }
312    fn add_user(mock: &MockPwdGrpProvider, uid: Id, name: &str, gid: Id) {
313        mock.add_to_passwds([pwd_grp::Passwd::<String> {
314            name: name.into(),
315            uid,
316            gid,
317            ..pwd_grp::Passwd::blank()
318        }]);
319    }
320    fn add_group(mock: &MockPwdGrpProvider, gid: Id, name: &str) {
321        mock.add_to_groups([pwd_grp::Group::<String> {
322            name: name.into(),
323            gid,
324            ..pwd_grp::Group::blank()
325        }]);
326    }
327
328    #[test]
329    fn groups() {
330        let groups = cur_groups().unwrap();
331        let cur_gid = pwd_grp::getgid();
332        if groups.is_empty() {
333            // Some container/VM setups forget to put the (root) user into any
334            // groups at all.
335            return;
336        }
337        assert!(groups.contains(&cur_gid));
338    }
339
340    #[test]
341    fn username_real() {
342        // Here we'll do tests with our real username.  THere's not much we can
343        // actually test there, but we'll try anyway.
344        let cache = CACHE.lock().expect("poisoned lock");
345        let uname = get_own_username(&cache.pwd_grp)
346            .unwrap()
347            .expect("Running on a misconfigured host");
348        let user = PwdGrp.getpwnam::<Vec<u8>>(&uname).unwrap().unwrap();
349        assert_eq!(user.name, uname);
350        assert_eq!(user.uid, PwdGrp.getuid());
351    }
352
353    #[test]
354    fn username_from_env() {
355        let Ok(username_s) = std::env::var("USER")
356        // If USER isn't set, can't test this without setting the environment,
357        // and we don't do that in tests.
358        // Likewise if USER is not UTF-8, we can't make mock usernames.
359        else {
360            return;
361        };
362        let username = username_s.as_bytes().to_vec();
363
364        let other_name = format!("{}2", &username_s);
365
366        // Case 1: Current user in environment exists, though there are some distractions.
367        let db = mock_users();
368        add_user(&db, 413, &username_s, 413);
369        add_user(&db, 999, &other_name, 999);
370        // I'd like to add another user with the same UID and a different name,
371        // but MockUsers doesn't support that.
372        let found = get_own_username(&db).unwrap();
373        assert_eq!(found.as_ref(), Some(&username));
374
375        // Case 2: Current user in environment exists, but has the wrong uid.
376        let db = mock_users();
377        add_user(&db, 999, &username_s, 999);
378        add_user(&db, 413, &other_name, 413);
379        let found = get_own_username(&db).unwrap();
380        assert_eq!(found, Some(other_name.clone().into_bytes()));
381
382        // Case 3: Current user in environment does not exist; no user can be found.
383        let db = mock_users();
384        add_user(&db, 999413, &other_name, 999);
385        let found = get_own_username(&db).unwrap();
386        assert!(found.is_none());
387    }
388
389    #[test]
390    fn username_ignoring_env() {
391        // Case 1: uid is found.
392        let db = mock_users();
393        add_user(&db, 413, "aranea", 413413);
394        add_user(&db, 415, "notyouru!sername", 413413);
395        let found = get_own_username(&db).unwrap();
396        assert_eq!(found, Some(b"aranea".to_vec()));
397
398        // Case 2: uid not found.
399        let db = mock_users();
400        add_user(&db, 999413, "notyourn!ame", 999);
401        let found = get_own_username(&db).unwrap();
402        assert!(found.is_none());
403    }
404
405    #[test]
406    fn selfnamed() {
407        // check the real groups we're in, since this isn't mockable.
408        let cur_groups = cur_groups().unwrap();
409        if cur_groups.is_empty() {
410            // Can't actually proceed with the test unless we're in a group.
411            return;
412        }
413        let not_our_gid = (1..65536)
414            .find(|n| !cur_groups.contains(n))
415            .expect("We are somehow in all groups 1..65535!");
416
417        // Case 1: we find our username but no group with the same name.
418        let db = mock_users();
419        add_user(&db, 413, "aranea", 413413);
420        add_group(&db, 413413, "serket");
421        let found = get_self_named_gid_impl(&db).unwrap();
422        assert!(found.is_none());
423
424        // Case 2: we find our username and a group with the same name, but we
425        // are not a member of that group.
426        let db = mock_users();
427        add_user(&db, 413, "aranea", 413413);
428        add_group(&db, not_our_gid, "aranea");
429        let found = get_self_named_gid_impl(&db).unwrap();
430        assert!(found.is_none());
431
432        // Case 3: we find our username and a group with the same name, AND we
433        // are indeed a member of that group.
434        let db = mock_users();
435        add_user(&db, 413, "aranea", 413413);
436        add_group(&db, cur_groups[0], "aranea");
437        let found = get_self_named_gid_impl(&db).unwrap();
438        assert_eq!(found, Some(cur_groups[0]));
439    }
440
441    #[test]
442    fn lookup_id() {
443        let db = mock_users();
444        add_user(&db, 413, "aranea", 413413);
445        add_group(&db, 33, "nepeta");
446
447        assert_eq!(TrustedUser::None.get_uid_impl(&db).unwrap(), None);
448        assert_eq!(TrustedUser::Current.get_uid_impl(&db).unwrap(), Some(413));
449        assert_eq!(TrustedUser::Id(413).get_uid_impl(&db).unwrap(), Some(413));
450        assert_eq!(
451            TrustedUser::Name("aranea".into())
452                .get_uid_impl(&db)
453                .unwrap(),
454            Some(413)
455        );
456        assert!(TrustedUser::Name("ac".into()).get_uid_impl(&db).is_err());
457
458        assert_eq!(TrustedGroup::None.get_gid_impl(&db).unwrap(), None);
459        assert_eq!(TrustedGroup::Id(33).get_gid_impl(&db).unwrap(), Some(33));
460        assert_eq!(
461            TrustedGroup::Name("nepeta".into())
462                .get_gid_impl(&db)
463                .unwrap(),
464            Some(33)
465        );
466        assert!(TrustedGroup::Name("ac".into()).get_gid_impl(&db).is_err());
467    }
468}