signstar_config/
test.rs

1//! Utilities used for test setups.
2use std::{
3    fs::{Permissions, create_dir_all, read_dir, set_permissions, write},
4    os::{linux::fs::MetadataExt, unix::fs::PermissionsExt},
5    path::{Path, PathBuf},
6    process::{Child, Command},
7    thread,
8    time,
9};
10
11use log::debug;
12use nethsm::{FullCredentials, Passphrase, UserId};
13use rand::{Rng, distributions::Alphanumeric, thread_rng};
14use signstar_common::config::get_default_config_file_path;
15use tempfile::NamedTempFile;
16use which::which;
17
18use crate::{
19    AdminCredentials,
20    AdministrativeSecretHandling,
21    ExtendedUserMapping,
22    NetHsmAdminCredentials,
23    SignstarConfig,
24};
25
26/// An error that may occur when using test utils.
27#[derive(Debug, thiserror::Error)]
28pub enum Error {
29    /// Applying permissions to a file failed.
30    #[error("Unable to apply permissions to {path}:\n{source}")]
31    ApplyPermissions {
32        /// The file that was being modified.
33        path: PathBuf,
34
35        /// The source error.
36        source: std::io::Error,
37    },
38
39    /// A directory can not be created.
40    #[error("Unable to create directory {dir}:\n{source}")]
41    CreateDirectory {
42        /// The directory which was about to be created.
43        dir: PathBuf,
44
45        /// The source error.
46        source: std::io::Error,
47    },
48
49    /// The socket for io.systemd.Credentials could not be started.
50    #[error("Unable to start socket for io.systemd.Credentials:\n{0}")]
51    CredentialsSocket(#[source] std::io::Error),
52
53    /// An I/O error.
54    #[error("I/O error while {context}:\n{source}")]
55    Io {
56        /// The short description of the operation.
57        context: &'static str,
58
59        /// The source error.
60        source: std::io::Error,
61    },
62
63    /// An I/O error with a specific path.
64    #[error("I/O error at {path} while {context}:\n{source}")]
65    IoPath {
66        /// The file that was being accessed.
67        path: PathBuf,
68
69        /// The short description of the operation.
70        context: &'static str,
71
72        /// The source error.
73        source: std::io::Error,
74    },
75
76    /// A signstar-config error.
77    #[error("Signstar-config error:\n{0}")]
78    SignstarConfig(#[from] crate::Error),
79
80    /// A timeout has been reached.
81    #[error("Timeout of {timeout}ms reached while {context}")]
82    Timeout {
83        /// The value of the timeout in milliseconds.
84        timeout: u64,
85
86        /// The short description of the operation.
87        context: String,
88    },
89
90    /// A temporary file cannot be created.
91    #[error("A temporary file for {purpose} cannot be created:\n{source}")]
92    Tmpfile {
93        /// The purpose of the temporary file.
94        purpose: &'static str,
95
96        /// The source error.
97        source: std::io::Error,
98    },
99}
100
101/// Recursively lists files, their permissions and ownership.
102pub fn list_files_in_dir(path: impl AsRef<Path>) -> Result<(), Error> {
103    let path = path.as_ref();
104    let entries = read_dir(path).map_err(|source| Error::IoPath {
105        path: path.to_path_buf(),
106        context: "reading its children",
107        source,
108    })?;
109
110    for entry in entries {
111        let entry = entry.map_err(|source| Error::IoPath {
112            path: path.to_path_buf(),
113            context: "getting an entry below it",
114            source,
115        })?;
116        let meta = entry.metadata().map_err(|source| Error::IoPath {
117            path: path.to_path_buf(),
118            context: "getting metadata",
119            source,
120        })?;
121
122        debug!(
123            "{} {}/{} {entry:?}",
124            meta.permissions().mode(),
125            meta.st_uid(),
126            meta.st_gid()
127        );
128
129        if meta.is_dir() {
130            list_files_in_dir(entry.path())?;
131        }
132    }
133
134    Ok(())
135}
136
137/// Returns a configuration file with `data` as contents in a temporary location.
138pub fn get_tmp_config(data: &[u8]) -> Result<NamedTempFile, Error> {
139    let tmp_config = NamedTempFile::new().map_err(|source| Error::Tmpfile {
140        purpose: "full signstar configuration",
141        source,
142    })?;
143    write(&tmp_config, data).map_err(|source| Error::Io {
144        context: "writing full signstar configuration to temporary file",
145        source,
146    })?;
147    Ok(tmp_config)
148}
149
150/// Writes a dummy `/etc/machine-id`, which is required for systemd-creds.
151///
152/// # Errors
153///
154/// Returns an error if
155///
156/// - a static machine-id can not be written to `/etc/machine-id`,
157/// - or metadata on the created `/etc/machine-id` can not be retrieved.
158pub fn write_machine_id() -> Result<(), Error> {
159    debug!("Write dummy /etc/machine-id, required for systemd-creds");
160    let machine_id = PathBuf::from("/etc/machine-id");
161    std::fs::write(&machine_id, "d3b07384d113edec49eaa6238ad5ff00").map_err(|source| {
162        Error::IoPath {
163            path: machine_id.to_path_buf(),
164            context: "writing machine-id",
165            source,
166        }
167    })?;
168
169    let metadata = machine_id.metadata().map_err(|source| Error::IoPath {
170        path: machine_id,
171        context: "getting metadata of file",
172        source,
173    })?;
174    debug!(
175        "/etc/machine-id\nmode: {}\nuid: {}\ngid: {}",
176        metadata.permissions().mode(),
177        metadata.st_uid(),
178        metadata.st_gid()
179    );
180    Ok(())
181}
182
183/// A background process.
184///
185/// Tracks a [`Child`] which represents a process that runs in the background.
186/// The background process is automatically killed upon dropping the [`BackgroundProcess`].
187#[derive(Debug)]
188pub struct BackgroundProcess {
189    child: Child,
190    command: String,
191}
192
193impl BackgroundProcess {
194    /// Kills the tracked background process.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the process could not be killed.
199    pub fn kill(&mut self) -> Result<(), Error> {
200        self.child.kill().map_err(|source| Error::Io {
201            context: "killing process",
202            source,
203        })
204    }
205}
206
207impl Drop for BackgroundProcess {
208    /// Kills the tracked background process when destructing the [`BackgroundProcess`].
209    fn drop(&mut self) {
210        if let Err(error) = self.child.kill() {
211            log::debug!(
212                "Unable to kill background process of command {}:\n{error}",
213                self.command
214            )
215        }
216    }
217}
218
219/// Starts a socket for `io.systemd.Credentials` using `systemd-socket-activate`.
220///
221/// Sets the file mode of the socket to `666` so that all users on the system have access.
222///
223/// # Errors
224///
225/// Returns an error if
226///
227/// - `systemd-socket-activate` is unable to start the required socket,
228/// - one or more files in `/run/systemd` can not be listed,
229/// - applying of permissions on `/run/systemd/io.systemd.Credentials` fails,
230/// - or the socket has not been made available within 10000ms.
231pub fn start_credentials_socket() -> Result<BackgroundProcess, Error> {
232    let systemd_run_path = PathBuf::from("/run/systemd");
233    let socket_path = PathBuf::from("/run/systemd/io.systemd.Credentials");
234    create_dir_all(&systemd_run_path).map_err(|source| Error::CreateDirectory {
235        dir: systemd_run_path,
236        source,
237    })?;
238
239    // Run systemd-socket-activate to provide /run/systemd/io.systemd.Credentials
240    let command = "systemd-socket-activate";
241    let systemd_socket_activate = which(command).map_err(|source| {
242        Error::SignstarConfig(
243            crate::utils::Error::ExecutableNotFound {
244                command: command.to_string(),
245                source,
246            }
247            .into(),
248        )
249    })?;
250    let mut command = Command::new(systemd_socket_activate);
251    let command = command.args([
252        "--listen",
253        "/run/systemd/io.systemd.Credentials",
254        "--accept",
255        "--fdname=varlink",
256        "systemd-creds",
257    ]);
258    let child = command.spawn().map_err(Error::CredentialsSocket)?;
259
260    // Set the socket to be writable by all, once it's available.
261    let timeout = 10000;
262    let step = 100;
263    let mut elapsed = 0;
264    let mut permissions_set = false;
265    while elapsed < timeout {
266        if socket_path.exists() {
267            debug!("Found {socket_path:?}");
268            set_permissions(socket_path.as_path(), Permissions::from_mode(0o666)).map_err(
269                |source| Error::ApplyPermissions {
270                    path: socket_path.to_path_buf(),
271                    source,
272                },
273            )?;
274            permissions_set = true;
275            break;
276        } else {
277            thread::sleep(time::Duration::from_millis(step));
278            elapsed += step;
279        }
280    }
281    if !permissions_set {
282        return Err(Error::Timeout {
283            timeout,
284            context: format!("waiting for {socket_path:?}"),
285        });
286    }
287
288    Ok(BackgroundProcess {
289        child,
290        command: format!("{command:?}"),
291    })
292}
293
294/// Prepares a system for use with Signstar.
295///
296/// Prepares the following:
297///
298/// - Creates `/etc/machine-id`, which is needed for `systemd-creds` to function.
299/// - Reads Signstar configuration from data and writes to default config location.
300/// - Creates `/run/systemd/io.systemd.Credentials` by running `systemd-socket-activate` in the
301///   background
302///
303/// Returns the list of [`ExtendedUserMapping`]s derived from the Signstar configuration and the
304/// [`BackgroundProcess`] returned from [`start_credentials_socket`].
305///
306/// # Errors
307///
308/// Returns an error if
309///
310/// - [`write_machine_id`] fails,
311/// - a new [`SignstarConfig`] can not be created from `config_data`,
312/// - a [`SignstarConfig`] can not be saved to a system-wide location,
313/// - or [`start_credentials_socket`] fails.
314pub fn prepare_system_with_config(
315    config_data: &[u8],
316) -> Result<(Vec<ExtendedUserMapping>, BackgroundProcess), Error> {
317    write_machine_id()?;
318
319    // Read Signstar config from `config_data`
320    let config = SignstarConfig::new_from_file(Some(get_tmp_config(config_data)?.path()))?;
321
322    // Store Signstar config in default location
323    config.store(Some(&get_default_config_file_path()))?;
324
325    // Get extended user mappings for all users.
326    let creds_mapping: Vec<ExtendedUserMapping> = config.into();
327
328    // Return extended user mappings contained in Signstar config and the background process
329    // providing /run/systemd/io.systemd.Credentials
330    Ok((creds_mapping, start_credentials_socket()?))
331}
332
333/// Creates an [`AdminCredentials`] from config data.
334///
335/// Accepts a byte slice containing configuration data.
336///
337/// # Errors
338///
339/// Returns an error if
340///
341/// - a temporary config file can not be created from `config_data`,
342/// - an [`AdminCredentials`] can not be created from the temporary config file.
343pub fn admin_credentials(config_data: &[u8]) -> Result<NetHsmAdminCredentials, Error> {
344    let config_file = get_tmp_config(config_data)?;
345    NetHsmAdminCredentials::load_from_file(
346        config_file.path(),
347        AdministrativeSecretHandling::Plaintext,
348    )
349    .map_err(Error::SignstarConfig)
350}
351
352/// Creates a [`SignstarConfig`] from config data.
353///
354/// Accepts a byte slice containing configuration data.
355///
356/// # Errors
357///
358/// Returns an error if
359///
360/// - a temporary config file can not be created from `config_data`,
361/// - a [`SignstarConfig`] can not be created from the temporary config file.
362pub fn signstar_config(config_data: &[u8]) -> Result<SignstarConfig, Error> {
363    SignstarConfig::new_from_file(Some(get_tmp_config(config_data)?.path()))
364        .map_err(Error::SignstarConfig)
365}
366
367/// Creates a list of [`FullCredentials`] for a list of [`UserId`]s.
368///
369/// Creates a 30-char long alphanumeric passphrase for each [`UserId`] in `users` and then
370/// constructs a [`FullCredentials`].
371pub fn create_full_credentials(users: &[UserId]) -> Vec<FullCredentials> {
372    /// Creates a passphrase
373    fn create_passphrase() -> String {
374        thread_rng()
375            .sample_iter(&Alphanumeric)
376            .take(30)
377            .map(char::from)
378            .collect()
379    }
380
381    users
382        .iter()
383        .map(|user| FullCredentials::new(user.clone(), Passphrase::new(create_passphrase())))
384        .collect()
385}