signstar_test/
lib.rs

1//! Utilities used for test setups.
2use 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/// An error that may occur when using test utils.
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27    /// Applying permissions to a file failed.
28    #[error("Unable to apply permissions to {path}:\n{source}")]
29    ApplyPermissions {
30        /// The file that was being modified.
31        path: PathBuf,
32
33        /// The source error.
34        source: std::io::Error,
35    },
36
37    /// A directory can not be created.
38    #[error("Unable to create directory {dir}:\n{source}")]
39    CreateDirectory {
40        /// The directory which was about to be created.
41        dir: PathBuf,
42
43        /// The source error.
44        source: std::io::Error,
45    },
46
47    /// The socket for io.systemd.Credentials could not be started.
48    #[error("Unable to start socket for io.systemd.Credentials:\n{0}")]
49    CredentialsSocket(#[source] std::io::Error),
50
51    /// An I/O error.
52    #[error("I/O error while {context}:\n{source}")]
53    Io {
54        /// The short description of the operation.
55        context: &'static str,
56
57        /// The source error.
58        source: std::io::Error,
59    },
60
61    /// An I/O error with a specific path.
62    #[error("I/O error at {path} while {context}:\n{source}")]
63    IoPath {
64        /// The file that was being accessed.
65        path: PathBuf,
66
67        /// The short description of the operation.
68        context: &'static str,
69
70        /// The source error.
71        source: std::io::Error,
72    },
73
74    /// A signstar-config error.
75    #[error("Signstar-config error:\n{0}")]
76    SignstarConfig(#[from] signstar_config::Error),
77
78    /// A timeout has been reached.
79    #[error("Timeout of {timeout}ms reached while {context}")]
80    Timeout {
81        /// The value of the timeout in milliseconds.
82        timeout: u64,
83
84        /// The short description of the operation.
85        context: String,
86    },
87
88    /// A temporary file cannot be created.
89    #[error("A temporary file for {purpose} cannot be created:\n{source}")]
90    Tmpfile {
91        /// The purpose of the temporary file.
92        purpose: &'static str,
93
94        /// The source error.
95        source: std::io::Error,
96    },
97}
98
99/// Recursively lists files, their permissions and ownership.
100pub 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
135/// Returns a configuration file with `data` as contents in a temporary location.
136pub 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
148/// Writes a dummy `/etc/machine-id`, which is required for systemd-creds.
149///
150/// # Errors
151///
152/// Returns an error if
153///
154/// - a static machine-id can not be written to `/etc/machine-id`,
155/// - or metadata on the created `/etc/machine-id` can not be retrieved.
156pub 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/// A background process.
182///
183/// Tracks a [`Child`] which represents a process that runs in the background.
184/// The background process is automatically killed upon dropping the [`BackgroundProcess`].
185#[derive(Debug)]
186pub struct BackgroundProcess {
187    child: Child,
188    command: String,
189}
190
191impl BackgroundProcess {
192    /// Kills the tracked background process.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the process could not be killed.
197    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    /// Kills the tracked background process when destructing the [`BackgroundProcess`].
207    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
217/// Starts a socket for `io.systemd.Credentials` using `systemd-socket-activate`.
218///
219/// Sets the file mode of the socket to `666` so that all users on the system have access.
220///
221/// # Errors
222///
223/// Returns an error if
224///
225/// - `systemd-socket-activate` is unable to start the required socket,
226/// - one or more files in `/run/systemd` can not be listed,
227/// - applying of permissions on `/run/systemd/io.systemd.Credentials` fails,
228/// - or the socket has not been made available within 10000ms.
229pub 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    // Run systemd-socket-activate to provide /run/systemd/io.systemd.Credentials
238    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    // Set the socket to be writable by all, once it's available.
259    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/// Data on a command that has been executed.
293///
294/// Tracks the command that has been executed, its stdout, stderr and status code.
295#[derive(Debug)]
296pub struct CommandOutput {
297    /// The command that has been executed.
298    pub command: String,
299
300    /// Status code of `command`.
301    pub status: ExitStatus,
302
303    /// Standard output of `command`.
304    pub stdout: String,
305
306    /// Standard error of `command`.
307    pub stderr: String,
308}
309
310/// Runs a `command` with `command_args` as a specific `user` and returns [`CommandOutput`], which
311/// captures the `command`'s completion status.
312///
313/// Uses [`runuser`] to run the the command as a specific user.
314///
315/// [`runuser`]: https://man.archlinux.org/man/runuser.1
316pub 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    /// Returns the path to a `command`.
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if `command` can not be found in PATH.
327    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    // Run command as user
352    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
416/// Creates a set of users.
417pub fn create_users(users: &[String]) -> TestResult {
418    debug!("Creating users: {:?}", users);
419    for user in users {
420        debug!("Creating user: {}", user);
421
422        // create the user and its home
423        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        // unlock the user
444        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
463/// Prepares a system for use with Signstar.
464///
465/// Prepares the following:
466///
467/// - Creates `/etc/machine-id`, which is needed for `systemd-creds` to function.
468/// - Reads Signstar configuration from data and writes to default config location.
469/// - Creates `/run/systemd/io.systemd.Credentials` by running `systemd-socket-activate` in the
470///   background
471///
472/// Returns the list of [`ExtendedUserMapping`]s derived from the Signstar configuration and the
473/// [`BackgroundProcess`] returned from [`start_credentials_socket`].
474///
475/// # Errors
476///
477/// Returns an error if
478///
479/// - [`write_machine_id`] fails,
480/// - a new [`HermeticParallelConfig`] can not be created from `config_data`,
481/// - a [`HermeticParallelConfig`] can not be saved to a system-wide location,
482/// - or [`start_credentials_socket`] fails.
483pub fn prepare_system_with_config(
484    config_data: &[u8],
485) -> Result<(Vec<ExtendedUserMapping>, BackgroundProcess), Error> {
486    write_machine_id()?;
487
488    // Read Signstar config from `config_data`
489    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    // Store Signstar config in default location
504    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    // Get extended user mappings for all users.
513    let creds_mapping: Vec<ExtendedUserMapping> = config.into();
514
515    // Return extended user mappings contained in Signstar config and the background process
516    // providing /run/systemd/io.systemd.Credentials
517    Ok((creds_mapping, start_credentials_socket()?))
518}