opencode_cloud_core/docker/
users.rs1use super::exec::{exec_command, exec_command_exit_code, exec_command_with_stdin};
11use super::{DockerClient, DockerError};
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct UserInfo {
16 pub username: String,
18 pub uid: u32,
20 pub home: String,
22 pub shell: String,
24 pub locked: bool,
26}
27
28pub async fn create_user(
43 client: &DockerClient,
44 container: &str,
45 username: &str,
46) -> Result<(), DockerError> {
47 let cmd = vec!["useradd", "-m", "-s", "/bin/bash", username];
48
49 let exit_code = exec_command_exit_code(client, container, cmd).await?;
50
51 if exit_code != 0 {
52 if user_exists(client, container, username).await? {
54 return Err(DockerError::Container(format!(
55 "User '{username}' already exists"
56 )));
57 }
58 return Err(DockerError::Container(format!(
59 "Failed to create user '{username}': useradd returned exit code {exit_code}"
60 )));
61 }
62
63 Ok(())
64}
65
66pub async fn set_user_password(
86 client: &DockerClient,
87 container: &str,
88 username: &str,
89 password: &str,
90) -> Result<(), DockerError> {
91 let cmd = vec!["chpasswd"];
92 let stdin_data = format!("{username}:{password}\n");
93
94 exec_command_with_stdin(client, container, cmd, &stdin_data).await?;
95
96 Ok(())
97}
98
99pub async fn user_exists(
109 client: &DockerClient,
110 container: &str,
111 username: &str,
112) -> Result<bool, DockerError> {
113 let cmd = vec!["id", "-u", username];
114 let exit_code = exec_command_exit_code(client, container, cmd).await?;
115
116 Ok(exit_code == 0)
117}
118
119pub async fn lock_user(
129 client: &DockerClient,
130 container: &str,
131 username: &str,
132) -> Result<(), DockerError> {
133 let cmd = vec!["passwd", "-l", username];
134 let exit_code = exec_command_exit_code(client, container, cmd).await?;
135
136 if exit_code != 0 {
137 return Err(DockerError::Container(format!(
138 "Failed to lock user '{username}': passwd returned exit code {exit_code}"
139 )));
140 }
141
142 Ok(())
143}
144
145pub async fn unlock_user(
154 client: &DockerClient,
155 container: &str,
156 username: &str,
157) -> Result<(), DockerError> {
158 let cmd = vec!["passwd", "-u", username];
159 let exit_code = exec_command_exit_code(client, container, cmd).await?;
160
161 if exit_code != 0 {
162 return Err(DockerError::Container(format!(
163 "Failed to unlock user '{username}': passwd returned exit code {exit_code}"
164 )));
165 }
166
167 Ok(())
168}
169
170pub async fn delete_user(
179 client: &DockerClient,
180 container: &str,
181 username: &str,
182) -> Result<(), DockerError> {
183 let cmd = vec!["userdel", "-r", username];
184 let exit_code = exec_command_exit_code(client, container, cmd).await?;
185
186 if exit_code != 0 {
187 if !user_exists(client, container, username).await? {
189 return Err(DockerError::Container(format!(
190 "User '{username}' does not exist"
191 )));
192 }
193 return Err(DockerError::Container(format!(
194 "Failed to delete user '{username}': userdel returned exit code {exit_code}"
195 )));
196 }
197
198 Ok(())
199}
200
201pub async fn list_users(
210 client: &DockerClient,
211 container: &str,
212) -> Result<Vec<UserInfo>, DockerError> {
213 let cmd = vec!["sh", "-c", "getent passwd | grep '/home/'"];
215 let output = exec_command(client, container, cmd).await?;
216
217 let mut users = Vec::new();
218
219 for line in output.lines() {
220 if let Some(info) = parse_passwd_line(line) {
221 let locked = is_user_locked(client, container, &info.username).await?;
223
224 users.push(UserInfo {
225 username: info.username,
226 uid: info.uid,
227 home: info.home,
228 shell: info.shell,
229 locked,
230 });
231 }
232 }
233
234 Ok(users)
235}
236
237async fn is_user_locked(
242 client: &DockerClient,
243 container: &str,
244 username: &str,
245) -> Result<bool, DockerError> {
246 let cmd = vec!["passwd", "-S", username];
247 let output = exec_command(client, container, cmd).await?;
248
249 let parts: Vec<&str> = output.split_whitespace().collect();
252 if parts.len() >= 2 {
253 return Ok(parts[1] == "L");
254 }
255
256 Ok(false)
257}
258
259struct ParsedUser {
261 username: String,
262 uid: u32,
263 home: String,
264 shell: String,
265}
266
267fn parse_passwd_line(line: &str) -> Option<ParsedUser> {
271 let fields: Vec<&str> = line.split(':').collect();
272 if fields.len() < 7 {
273 return None;
274 }
275
276 let uid = fields[2].parse::<u32>().ok()?;
277
278 Some(ParsedUser {
279 username: fields[0].to_string(),
280 uid,
281 home: fields[5].to_string(),
282 shell: fields[6].to_string(),
283 })
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_parse_passwd_line_valid() {
292 let line = "admin:x:1001:1001:Admin User:/home/admin:/bin/bash";
293 let parsed = parse_passwd_line(line).unwrap();
294 assert_eq!(parsed.username, "admin");
295 assert_eq!(parsed.uid, 1001);
296 assert_eq!(parsed.home, "/home/admin");
297 assert_eq!(parsed.shell, "/bin/bash");
298 }
299
300 #[test]
301 fn test_parse_passwd_line_minimal() {
302 let line = "user:x:1000:1000::/home/user:/bin/sh";
303 let parsed = parse_passwd_line(line).unwrap();
304 assert_eq!(parsed.username, "user");
305 assert_eq!(parsed.uid, 1000);
306 assert_eq!(parsed.home, "/home/user");
307 assert_eq!(parsed.shell, "/bin/sh");
308 }
309
310 #[test]
311 fn test_parse_passwd_line_invalid() {
312 assert!(parse_passwd_line("invalid").is_none());
313 assert!(parse_passwd_line("too:few:fields").is_none());
314 assert!(parse_passwd_line("user:x:not_a_number:1000::/home/user:/bin/bash").is_none());
315 }
316
317 #[test]
318 fn test_user_info_struct() {
319 let info = UserInfo {
320 username: "admin".to_string(),
321 uid: 1001,
322 home: "/home/admin".to_string(),
323 shell: "/bin/bash".to_string(),
324 locked: false,
325 };
326 assert_eq!(info.username, "admin");
327 assert!(!info.locked);
328 }
329
330 #[test]
331 fn test_user_info_equality() {
332 let info1 = UserInfo {
333 username: "admin".to_string(),
334 uid: 1001,
335 home: "/home/admin".to_string(),
336 shell: "/bin/bash".to_string(),
337 locked: false,
338 };
339 let info2 = info1.clone();
340 assert_eq!(info1, info2);
341 }
342
343 #[test]
344 fn test_user_info_debug() {
345 let info = UserInfo {
346 username: "test".to_string(),
347 uid: 1000,
348 home: "/home/test".to_string(),
349 shell: "/bin/bash".to_string(),
350 locked: true,
351 };
352 let debug = format!("{info:?}");
353 assert!(debug.contains("test"));
354 assert!(debug.contains("1000"));
355 assert!(debug.contains("locked: true"));
356 }
357}