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 = first_subordinate_id(
59            &PathBuf::from("/etc/subgid"),
60            group.gid.as_raw(),
61            &group.name,
62        )?;
63        Ok(SubordinateInfo {
64            uid: user.uid.as_raw(),
65            gid: group.gid.as_raw(),
66            subuid,
67            subgid,
68        })
69    }
70}
71
72/// Returns user and group entries for current effective user.
73fn user_info() -> Result<(User, Group), SubordinateError> {
74    let u = Uid::effective();
75    let user = User::from_uid(u)
76        .map_err(SubordinateError::UidError)?
77        .ok_or(SubordinateError::NoPasswdEntry(u))?;
78    let g = Gid::effective();
79    let group = Group::from_gid(g)
80        .map_err(SubordinateError::GidError)?
81        .ok_or(SubordinateError::NoGroupEntry(g))?;
82    Ok((user, group))
83}
84
85fn first_subordinate_id(file: &PathBuf, id: u32, name: &str) -> Result<u32, SubordinateError> {
86    let f = File::open(file).map_err(|e| SubordinateError::IoError(e, file.clone()))?;
87    let reader = BufReader::new(f).lines();
88
89    for line in reader {
90        let line = line.map_err(|e| SubordinateError::IoError(e, file.clone()))?;
91        let line = line.trim();
92        let parts: Vec<&str> = line.split(':').collect();
93        if parts.len() == 3 && (parts[0] == name || id.to_string() == parts[0]) {
94            let subuid = parts[1]
95                .parse::<u32>()
96                .map_err(|e| SubordinateError::ParseError(file.clone(), line.into(), e))?;
97            let range = parts[2]
98                .parse::<u32>()
99                .map_err(|e| SubordinateError::ParseError(file.clone(), line.into(), e))?;
100            if range > 0 {
101                return Ok(subuid);
102            }
103        }
104    }
105
106    Err(SubordinateError::MissingEntry(
107        file.clone(),
108        name.into(),
109        id,
110    ))
111}
112
113#[cfg(test)]
114mod tests {
115    use crate::oci::subuid::SubordinateError;
116
117    fn create_fixture<'a>(content: impl IntoIterator<Item = &'a str>) -> tempfile::NamedTempFile {
118        use std::io::Write;
119        let mut file = tempfile::NamedTempFile::new().expect("Could not create tempfile");
120        for line in content.into_iter() {
121            writeln!(file, "{}", line).expect("");
122        }
123        file
124    }
125
126    #[test]
127    fn test_parse_uid_file_with_name_should_return_first_match() {
128        let file = create_fixture(["nobody:10000:65", "root:1000:2", "0:2:2"]);
129        let id = super::first_subordinate_id(&file.path().into(), 0, "root")
130            .expect("Faild to look up subordinate id.");
131        assert_eq!(id, 1000);
132    }
133
134    #[test]
135    fn test_parse_uid_file_with_uid_should_return_first_match() {
136        let file = create_fixture(["nobody:10000:65", "0:2:2"]);
137        let id = super::first_subordinate_id(&file.path().into(), 0, "root")
138            .expect("Failed to look up subordinate id.");
139        assert_eq!(id, 2);
140    }
141
142    #[test]
143    fn test_missing() {
144        let file = create_fixture(["roots:1000:2", "1000:2:2"]);
145        let id = super::first_subordinate_id(&file.path().into(), 0, "root")
146            .expect_err("Expected not to find a matching subordinate entry.");
147        assert!(matches!(id, SubordinateError::MissingEntry(_, _, _)));
148    }
149
150    #[test]
151    fn test_parse_error() {
152        let file = create_fixture(["root:hello:2", "1000:2:2"]);
153        let id = super::first_subordinate_id(&file.path().into(), 0, "root")
154            .expect_err("Expected parsing to fail.");
155        assert!(matches!(id, SubordinateError::ParseError(_, _, _)));
156    }
157
158    #[test]
159    fn test_parse_errors_in_other_users_files_are_ignored() {
160        let file = create_fixture(["root:hello:2", "1000:2:2"]);
161        let id = super::first_subordinate_id(&file.path().into(), 1000, "user")
162            .expect("Failed to look up subordinate id.");
163        assert_eq!(id, 2);
164    }
165}