1use alien_error::{AlienError, Context, IntoAlienError};
4
5use crate::error::{ErrorData, Result};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct ExecIdentity {
10 pub uid: u32,
12 pub gid: u32,
14}
15
16pub struct ImageUser<'a> {
18 pub user: &'a str,
20 pub passwd: &'a str,
22 pub group: &'a str,
24}
25
26impl ImageUser<'_> {
27 pub fn exec_identity(
31 &self,
32 default_uid: u32,
33 command_uid: Option<u32>,
34 ) -> Result<ExecIdentity> {
35 let identity = self.image_identity()?;
36 let invalid = |reason: String| ErrorData::SandboxImageUserInvalid {
37 user: self.user.to_string(),
38 reason,
39 };
40 if identity.uid == 0 {
41 let uid = command_uid.unwrap_or(default_uid);
42 if uid == 0 || uid == u32::MAX {
43 return Err(AlienError::new(invalid(
44 "command uid must be non-root and cannot be -1".to_string(),
45 )));
46 }
47 return Ok(ExecIdentity { uid, gid: uid });
48 }
49 if command_uid.is_some_and(|declared| declared != identity.uid) {
50 return Err(AlienError::new(invalid(format!(
51 "privilegedSupervisor.commandUid {} disagrees with image USER uid {}",
52 command_uid.unwrap_or_default(),
53 identity.uid
54 ))));
55 }
56 if identity.gid == 0 {
57 return Err(AlienError::new(invalid(
58 "the root group is not permitted for sandbox commands; declare USER with a non-root primary group".to_string(),
59 )));
60 }
61 Ok(identity)
62 }
63
64 pub fn image_identity(&self) -> Result<ExecIdentity> {
67 let invalid = |reason: String| ErrorData::SandboxImageUserInvalid {
68 user: self.user.to_string(),
69 reason,
70 };
71 let parse_id = |value: &str| -> Result<u32> {
72 let id = value
73 .parse::<u32>()
74 .into_alien_error()
75 .context(invalid(format!("'{value}' is not a numeric uid/gid")))?;
76 if id == u32::MAX {
77 return Err(AlienError::new(invalid(
78 "uid/gid -1 is reserved".to_string(),
79 )));
80 }
81 Ok(id)
82 };
83 let (user, group) = self
84 .user
85 .split_once(':')
86 .map_or((self.user, None), |(u, g)| (u, Some(g)));
87 let numeric_uid = if user.is_empty() {
88 Some(0)
89 } else if user.bytes().all(|c| c.is_ascii_digit()) {
90 Some(parse_id(user)?)
91 } else {
92 None
93 };
94 let account = self.passwd.lines().find_map(|line| {
95 let fields: Vec<_> = line.split(':').collect();
96 (fields.len() >= 4
97 && numeric_uid.map_or_else(
98 || fields[0] == user,
99 |uid| fields[2].parse::<u32>() == Ok(uid),
100 ))
101 .then_some(fields)
102 });
103 let uid = match numeric_uid {
104 Some(uid) => uid,
105 None => parse_id(
106 account.as_ref().ok_or_else(|| {
107 AlienError::new(invalid(format!("user '{user}' is absent from /etc/passwd")))
108 })?[2],
109 )?,
110 };
111 let gid = match group {
112 Some(group) if !group.is_empty() && group.bytes().all(|c| c.is_ascii_digit()) => {
113 parse_id(group)?
114 }
115 Some(group) => {
116 let entry = self
117 .group
118 .lines()
119 .find_map(|line| {
120 let fields: Vec<_> = line.split(':').collect();
121 (fields.len() >= 3 && fields[0] == group).then_some(fields)
122 })
123 .ok_or_else(|| {
124 AlienError::new(invalid(format!(
125 "group '{group}' is absent from /etc/group"
126 )))
127 })?;
128 parse_id(entry[2])?
129 }
130 None => match account {
131 Some(account) => parse_id(account[3])?,
132 None => 0,
134 },
135 };
136 Ok(ExecIdentity { uid, gid })
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn image_root_is_distinct_from_the_non_root_exec_fallback() {
146 for user in ["", "0", "0:0", "root", "supervisor:workers"] {
147 let image = ImageUser {
148 user,
149 passwd: "root:x:0:0::/:/bin/sh\nsupervisor:x:0:2000::/:/bin/sh\n",
150 group: "workers:x:2000:\n",
151 };
152 assert_eq!(image.image_identity().unwrap().uid, 0);
153 assert_eq!(
154 image.exec_identity(60000, None).unwrap(),
155 ExecIdentity {
156 uid: 60000,
157 gid: 60000
158 }
159 );
160 assert_eq!(
161 image.exec_identity(1000, Some(60001)).unwrap(),
162 ExecIdentity {
163 uid: 60001,
164 gid: 60001
165 }
166 );
167 }
168 }
169
170 #[test]
171 fn image_and_exec_preserve_non_root_users_and_named_primary_groups() {
172 for user in ["worker", "worker:workers", "2001:2000", "2001"] {
173 let image = ImageUser {
174 user,
175 passwd: "worker:x:2001:2000::/:/bin/sh\n",
176 group: "workers:x:2000:\n",
177 };
178 let expected = ExecIdentity {
179 uid: 2001,
180 gid: 2000,
181 };
182 assert_eq!(image.image_identity().unwrap(), expected);
183 assert_eq!(image.exec_identity(60000, None).unwrap(), expected);
184 assert!(image.exec_identity(60000, Some(2002)).is_err());
185 }
186 }
187
188 #[test]
189 fn numeric_image_user_can_have_root_group_but_exec_cannot() {
190 let image = ImageUser {
191 user: "2001",
192 passwd: "",
193 group: "",
194 };
195 assert_eq!(
196 image.image_identity().unwrap(),
197 ExecIdentity { uid: 2001, gid: 0 }
198 );
199 assert!(image.exec_identity(60000, None).is_err());
200 }
201
202 #[test]
203 fn invalid_image_users_fail_before_startup() {
204 for user in [
205 "missing",
206 "0:missing",
207 "4294967295",
208 "1:4294967295",
209 "1:2:3",
210 ] {
211 let image = ImageUser {
212 user,
213 passwd: "",
214 group: "",
215 };
216 assert_eq!(
217 image.image_identity().unwrap_err().code,
218 "SANDBOX_IMAGE_USER_INVALID"
219 );
220 }
221 }
222
223 #[test]
224 fn released_command_metadata_keeps_its_legacy_identity() {
225 let image: crate::sandbox_image::SandboxImageCommand = serde_json::from_str(
226 r#"{"command":["/bin/sleep","infinity"],"env":{},"workingDirectory":"/"}"#,
227 )
228 .unwrap();
229 assert_eq!(image.user, None);
230 let root: crate::sandbox_image::SandboxImageCommand = serde_json::from_str(
231 r#"{"user":"","command":["/bin/sleep","infinity"],"env":{},"workingDirectory":"/"}"#,
232 )
233 .unwrap();
234 assert_eq!(root.user.as_deref(), Some(""));
235 }
236}