snix_build/oci/
subuid.rs

1use std::{
2    fs::File,
3    io::{BufRead, BufReader},
4    num::ParseIntError,
5    path::PathBuf,
6};
7
8use nix::{
9    errno::Errno,
10    unistd::{Gid, Group, Uid, User},
11};
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15pub(crate) enum SubordinateError {
16    #[error("can't determine user {0}")]
17    UidError(Errno),
18
19    #[error("user entry for {0} does not exist")]
20    NoPasswdEntry(Uid),
21
22    #[error("can't determine group {0}")]
23    GidError(Errno),
24
25    #[error("group entry for {0} does not exist")]
26    NoGroupEntry(Gid),
27
28    #[error("io error {0:?}, file {1}")]
29    IoError(std::io::Error, PathBuf),
30
31    #[error("failed to parse {0} line '{1}', error {2}")]
32    ParseError(PathBuf, String, ParseIntError),
33
34    #[error("Missing entry in {0}, for {1}({2})")]
35    MissingEntry(PathBuf, String, u32),
36}
37
38/// Represents a single (subuid,subgid) pair for a user and their group.
39///
40/// In practice there are usually many more subordinate ids than just one, but
41/// for oci builds we only need one. If we ever need more, we can improve this
42/// implementation.
43#[derive(Debug, PartialEq, Eq)]
44pub(crate) struct SubordinateInfo {
45    pub uid: u32,
46    pub gid: u32,
47    pub subuid: u32,
48    pub subgid: u32,
49}
50
51impl SubordinateInfo {
52    /// Parses /etc/subuid and /etc/subgid and returns a single [SubordinateInfo] for the effective user.
53    pub(crate) fn for_effective_user() -> Result<SubordinateInfo, SubordinateError> {
54        let (user, group) = user_info()?;
55
56        let subuid =
57            first_subordinate_id(&PathBuf::from("/etc/subuid"), user.uid.as_raw(), &user.name)?;
58        let subgid =
59            first_subordinate_id(&PathBuf::from("/etc/subgid"), user.uid.as_raw(), &user.name)?;
60        Ok(SubordinateInfo {
61            uid: user.uid.as_raw(),
62            gid: group.gid.as_raw(),
63            subuid,
64            subgid,
65        })
66    }
67}
68
69/// Returns user and group entries for current effective user.
70fn user_info() -> Result<(User, Group), SubordinateError> {
71    let u = Uid::effective();
72    let user = User::from_uid(u)
73        .map_err(SubordinateError::UidError)?
74        .ok_or(SubordinateError::NoPasswdEntry(u))?;
75    let g = Gid::effective();
76    let group = Group::from_gid(g)
77        .map_err(SubordinateError::GidError)?
78        .ok_or(SubordinateError::NoGroupEntry(g))?;
79    Ok((user, group))
80}
81
82fn first_subordinate_id(file: &PathBuf, id: u32, name: &str) -> Result<u32, SubordinateError> {
83    let f = File::open(file).map_err(|e| SubordinateError::IoError(e, file.clone()))?;
84    let reader = BufReader::new(f).lines();
85
86    for line in reader {
87        let line = line.map_err(|e| SubordinateError::IoError(e, file.clone()))?;
88        let line = line.trim();
89        let parts: Vec<&str> = line.split(':').collect();
90        if parts.len() == 3 && (parts[0] == name || id.to_string() == parts[0]) {
91            let subuid = parts[1]
92                .parse::<u32>()
93                .map_err(|e| SubordinateError::ParseError(file.clone(), line.into(), e))?;
94            let range = parts[2]
95                .parse::<u32>()
96                .map_err(|e| SubordinateError::ParseError(file.clone(), line.into(), e))?;
97            if range > 0 {
98                return Ok(subuid);
99            }
100        }
101    }
102
103    Err(SubordinateError::MissingEntry(
104        file.clone(),
105        name.into(),
106        id,
107    ))
108}
109
110#[cfg(test)]
111mod tests {
112    use crate::oci::subuid::SubordinateError;
113
114    fn create_fixture<'a>(content: impl IntoIterator<Item = &'a str>) -> tempfile::NamedTempFile {
115        use std::io::Write;
116        let mut file = tempfile::NamedTempFile::new().expect("Could not create tempfile");
117        for line in content.into_iter() {
118            writeln!(file, "{line}").expect("");
119        }
120        file
121    }
122
123    #[test]
124    fn test_parse_uid_file_with_name_should_return_first_match() {
125        let file = create_fixture(["nobody:10000:65", "root:1000:2", "0:2:2"]);
126        let id = super::first_subordinate_id(&file.path().into(), 0, "root")
127            .expect("Faild to look up subordinate id.");
128        assert_eq!(id, 1000);
129    }
130
131    #[test]
132    fn test_parse_uid_file_with_uid_should_return_first_match() {
133        let file = create_fixture(["nobody:10000:65", "0:2:2"]);
134        let id = super::first_subordinate_id(&file.path().into(), 0, "root")
135            .expect("Failed to look up subordinate id.");
136        assert_eq!(id, 2);
137    }
138
139    #[test]
140    fn test_missing() {
141        let file = create_fixture(["roots:1000:2", "1000:2:2"]);
142        let id = super::first_subordinate_id(&file.path().into(), 0, "root")
143            .expect_err("Expected not to find a matching subordinate entry.");
144        assert!(matches!(id, SubordinateError::MissingEntry(_, _, _)));
145    }
146
147    #[test]
148    fn test_parse_error() {
149        let file = create_fixture(["root:hello:2", "1000:2:2"]);
150        let id = super::first_subordinate_id(&file.path().into(), 0, "root")
151            .expect_err("Expected parsing to fail.");
152        assert!(matches!(id, SubordinateError::ParseError(_, _, _)));
153    }
154
155    #[test]
156    fn test_parse_errors_in_other_users_files_are_ignored() {
157        let file = create_fixture(["root:hello:2", "1000:2:2"]);
158        let id = super::first_subordinate_id(&file.path().into(), 1000, "user")
159            .expect("Failed to look up subordinate id.");
160        assert_eq!(id, 2);
161    }
162}