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 = 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
72fn 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}