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#[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 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
69fn 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}