1use std::{
3 fs::{Permissions, create_dir_all, read_dir, read_to_string, set_permissions, write},
4 io::{BufReader, Read as _, Write as _},
5 os::{linux::fs::MetadataExt, unix::fs::PermissionsExt},
6 path::{Path, PathBuf},
7 process::{Child, Command, ExitStatus, Stdio},
8 thread,
9 time,
10};
11
12use log::debug;
13use nethsm_config::{
14 ConfigInteractivity,
15 ConfigSettings,
16 ExtendedUserMapping,
17 HermeticParallelConfig,
18};
19use signstar_common::{config::get_default_config_file_path, system_user::get_home_base_dir_path};
20use tempfile::NamedTempFile;
21use testresult::TestResult;
22use which::which;
23
24#[derive(Debug, thiserror::Error)]
26pub enum Error {
27 #[error("Unable to apply permissions to {path}:\n{source}")]
29 ApplyPermissions {
30 path: PathBuf,
32
33 source: std::io::Error,
35 },
36
37 #[error("Unable to create directory {dir}:\n{source}")]
39 CreateDirectory {
40 dir: PathBuf,
42
43 source: std::io::Error,
45 },
46
47 #[error("Unable to start socket for io.systemd.Credentials:\n{0}")]
49 CredentialsSocket(#[source] std::io::Error),
50
51 #[error("I/O error while {context}:\n{source}")]
53 Io {
54 context: &'static str,
56
57 source: std::io::Error,
59 },
60
61 #[error("I/O error at {path} while {context}:\n{source}")]
63 IoPath {
64 path: PathBuf,
66
67 context: &'static str,
69
70 source: std::io::Error,
72 },
73
74 #[error("Signstar-config error:\n{0}")]
76 SignstarConfig(#[from] signstar_config::Error),
77
78 #[error("Timeout of {timeout}ms reached while {context}")]
80 Timeout {
81 timeout: u64,
83
84 context: String,
86 },
87
88 #[error("A temporary file for {purpose} cannot be created:\n{source}")]
90 Tmpfile {
91 purpose: &'static str,
93
94 source: std::io::Error,
96 },
97}
98
99pub fn list_files_in_dir(path: impl AsRef<Path>) -> Result<(), Error> {
101 let path = path.as_ref();
102 let entries = read_dir(path).map_err(|source| Error::IoPath {
103 path: path.to_path_buf(),
104 context: "reading its children",
105 source,
106 })?;
107
108 for entry in entries {
109 let entry = entry.map_err(|source| Error::IoPath {
110 path: path.to_path_buf(),
111 context: "getting an entry below it",
112 source,
113 })?;
114 let meta = entry.metadata().map_err(|source| Error::IoPath {
115 path: path.to_path_buf(),
116 context: "getting metadata",
117 source,
118 })?;
119
120 debug!(
121 "{} {}/{} {entry:?}",
122 meta.permissions().mode(),
123 meta.st_uid(),
124 meta.st_gid()
125 );
126
127 if meta.is_dir() {
128 list_files_in_dir(entry.path())?;
129 }
130 }
131
132 Ok(())
133}
134
135pub fn get_tmp_config(data: &[u8]) -> Result<NamedTempFile, Error> {
137 let tmp_config = NamedTempFile::new().map_err(|source| Error::Tmpfile {
138 purpose: "full signstar configuration",
139 source,
140 })?;
141 write(&tmp_config, data).map_err(|source| Error::Io {
142 context: "writing full signstar configuration to temporary file",
143 source,
144 })?;
145 Ok(tmp_config)
146}
147
148pub fn write_machine_id() -> Result<(), Error> {
157 debug!("Write dummy /etc/machine-id, required for systemd-creds");
158 let machine_id = PathBuf::from("/etc/machine-id");
159 std::fs::write(&machine_id, "d3b07384d113edec49eaa6238ad5ff00").map_err(|source| {
160 Error::IoPath {
161 path: machine_id.to_path_buf(),
162 context: "writing machine-id",
163 source,
164 }
165 })?;
166
167 let metadata = machine_id.metadata().map_err(|source| Error::IoPath {
168 path: machine_id,
169 context: "getting metadata of file",
170 source,
171 })?;
172 debug!(
173 "/etc/machine-id\nmode: {}\nuid: {}\ngid: {}",
174 metadata.permissions().mode(),
175 metadata.st_uid(),
176 metadata.st_gid()
177 );
178 Ok(())
179}
180
181#[derive(Debug)]
186pub struct BackgroundProcess {
187 child: Child,
188 command: String,
189}
190
191impl BackgroundProcess {
192 pub fn kill(&mut self) -> Result<(), Error> {
198 self.child.kill().map_err(|source| Error::Io {
199 context: "killing process",
200 source,
201 })
202 }
203}
204
205impl Drop for BackgroundProcess {
206 fn drop(&mut self) {
208 if let Err(error) = self.child.kill() {
209 log::debug!(
210 "Unable to kill background process of command {}:\n{error}",
211 self.command
212 )
213 }
214 }
215}
216
217pub fn start_credentials_socket() -> Result<BackgroundProcess, Error> {
230 let systemd_run_path = PathBuf::from("/run/systemd");
231 let socket_path = PathBuf::from("/run/systemd/io.systemd.Credentials");
232 create_dir_all(&systemd_run_path).map_err(|source| Error::CreateDirectory {
233 dir: systemd_run_path,
234 source,
235 })?;
236
237 let command = "systemd-socket-activate";
239 let systemd_socket_activate = which(command).map_err(|source| {
240 Error::SignstarConfig(
241 signstar_config::utils::Error::ExecutableNotFound {
242 command: command.to_string(),
243 source,
244 }
245 .into(),
246 )
247 })?;
248 let mut command = Command::new(systemd_socket_activate);
249 let command = command.args([
250 "--listen",
251 "/run/systemd/io.systemd.Credentials",
252 "--accept",
253 "--fdname=varlink",
254 "systemd-creds",
255 ]);
256 let child = command.spawn().map_err(Error::CredentialsSocket)?;
257
258 let timeout = 10000;
260 let step = 100;
261 let mut elapsed = 0;
262 let mut permissions_set = false;
263 while elapsed < timeout {
264 if socket_path.exists() {
265 debug!("Found {socket_path:?}");
266 set_permissions(socket_path.as_path(), Permissions::from_mode(0o666)).map_err(
267 |source| Error::ApplyPermissions {
268 path: socket_path.to_path_buf(),
269 source,
270 },
271 )?;
272 permissions_set = true;
273 break;
274 } else {
275 thread::sleep(time::Duration::from_millis(step));
276 elapsed += step;
277 }
278 }
279 if !permissions_set {
280 return Err(Error::Timeout {
281 timeout,
282 context: format!("waiting for {socket_path:?}"),
283 });
284 }
285
286 Ok(BackgroundProcess {
287 child,
288 command: format!("{command:?}"),
289 })
290}
291
292#[derive(Debug)]
296pub struct CommandOutput {
297 pub command: String,
299
300 pub status: ExitStatus,
302
303 pub stdout: String,
305
306 pub stderr: String,
308}
309
310pub fn run_command_as_user(
317 command: &str,
318 command_args: &[&str],
319 user: &str,
320 command_input: Option<&[u8]>,
321) -> Result<CommandOutput, Error> {
322 fn get_command(command: &str) -> Result<PathBuf, Error> {
328 which(command).map_err(|source| {
329 Error::SignstarConfig(signstar_config::Error::Utils(
330 signstar_config::utils::Error::ExecutableNotFound {
331 command: command.to_string(),
332 source,
333 },
334 ))
335 })
336 }
337
338 let priv_command = get_command("runuser")?;
339 log::debug!("Checking availability of command {command}");
340 get_command(command)?;
341
342 let command_arg = format!(
343 "--command='{command}{}'",
344 if !command_args.is_empty() {
345 format!(" {}", command_args.join(" "))
346 } else {
347 "".to_string()
348 }
349 );
350
351 let mut command = Command::new(priv_command);
353 let command = command
354 .arg(command_arg)
355 .arg("--group")
356 .arg(user)
357 .arg("--login")
358 .arg(user)
359 .stdout(Stdio::piped())
360 .stderr(Stdio::piped())
361 .stdin(if command_input.is_none() {
362 Stdio::null()
363 } else {
364 Stdio::piped()
365 });
366
367 let command_string = format!("{command:?}");
368 log::debug!("Running command {command_string}");
369 let mut command_output = command.spawn().map_err(|source| {
370 Error::SignstarConfig(signstar_config::Error::CommandExec {
371 command: command_string.clone(),
372 source,
373 })
374 })?;
375
376 if let Some(input) = command_input {
377 command_output
378 .stdin
379 .take()
380 .expect("stdin to be set")
381 .write_all(input)
382 .map_err(|source| signstar_config::Error::CommandExec {
383 command: command_string.clone(),
384 source,
385 })?;
386 }
387
388 let exit = command_output.wait().unwrap();
389 let mut stdout = String::new();
390 BufReader::new(command_output.stdout.take().expect("stdout to be set"))
391 .read_to_string(&mut stdout)
392 .map_err(|source| signstar_config::Error::CommandExec {
393 command: command_string.clone(),
394 source,
395 })?;
396 log::debug!("stdout:\n{stdout}");
397
398 let mut stderr = String::new();
399 BufReader::new(command_output.stderr.take().expect("stderr to be set"))
400 .read_to_string(&mut stderr)
401 .map_err(|source| signstar_config::Error::CommandExec {
402 command: command_string.clone(),
403 source,
404 })?;
405
406 log::debug!("stderr:\n{stderr}");
407
408 Ok(CommandOutput {
409 status: exit,
410 stdout,
411 stderr,
412 command: command_string,
413 })
414}
415
416pub fn create_users(users: &[String]) -> TestResult {
418 debug!("Creating users: {:?}", users);
419 for user in users {
420 debug!("Creating user: {}", user);
421
422 let mut command = Command::new("useradd");
424 let command = command
425 .arg("--base-dir")
426 .arg(get_home_base_dir_path())
427 .arg("--create-home")
428 .arg("--user-group")
429 .arg("--shell")
430 .arg("/usr/bin/bash")
431 .arg(user);
432
433 let command_output = command.output()?;
434 if !command_output.status.success() {
435 return Err(signstar_config::Error::CommandNonZero {
436 command: format!("{command:?}"),
437 exit_status: command_output.status,
438 stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
439 }
440 .into());
441 }
442
443 let mut command = Command::new("usermod");
445 command.arg("--unlock");
446 command.arg(user);
447 let command_output = command.output()?;
448 if !command_output.status.success() {
449 return Err(signstar_config::Error::CommandNonZero {
450 command: format!("{command:?}"),
451 exit_status: command_output.status,
452 stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
453 }
454 .into());
455 }
456 }
457
458 debug!("/etc/passwd:\n{}", read_to_string("/etc/passwd")?);
459
460 Ok(())
461}
462
463pub fn prepare_system_with_config(
484 config_data: &[u8],
485) -> Result<(Vec<ExtendedUserMapping>, BackgroundProcess), Error> {
486 write_machine_id()?;
487
488 let config = HermeticParallelConfig::new_from_file(
490 ConfigSettings::new(
491 "my_app".to_string(),
492 ConfigInteractivity::NonInteractive,
493 None,
494 ),
495 Some(get_tmp_config(config_data)?.path()),
496 )
497 .map_err(|source| {
498 Error::SignstarConfig(signstar_config::Error::Config(
499 signstar_config::config::Error::NetHsmConfig(source),
500 ))
501 })?;
502
503 config
505 .store(Some(&get_default_config_file_path()))
506 .map_err(|source| {
507 Error::SignstarConfig(signstar_config::Error::Config(
508 signstar_config::config::Error::NetHsmConfig(source),
509 ))
510 })?;
511
512 let creds_mapping: Vec<ExtendedUserMapping> = config.into();
514
515 Ok((creds_mapping, start_credentials_socket()?))
518}