signstar_config/config/
base.rs

1//! [`SignstarConfig`] for _Signstar hosts_.
2
3use std::{
4    collections::HashSet,
5    fs::{File, create_dir_all, read_to_string},
6    io::Write,
7    path::Path,
8};
9
10use nethsm::{Connection, NamespaceId};
11use serde::{Deserialize, Serialize};
12use signstar_common::config::{get_config_file, get_run_override_config_file_path};
13
14use crate::{
15    ConfigError as Error,
16    SystemUserId,
17    config::mapping::{ExtendedUserMapping, UserMapping},
18};
19
20/// The handling of administrative secrets.
21///
22/// Administrative secrets may be handled in different ways (e.g. persistent or non-persistent).
23#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(rename_all = "kebab-case")]
25pub enum AdministrativeSecretHandling {
26    /// The administrative secrets are handled in a plaintext file in a non-volatile directory.
27    ///
28    /// ## Warning
29    ///
30    /// This variant should only be used in non-production test setups, as it implies the
31    /// persistence of unencrypted administrative secrets on a file system.
32    Plaintext,
33
34    /// The administrative secrets are handled in a file encrypted using [systemd-creds] in a
35    /// non-volatile directory.
36    ///
37    /// ## Warning
38    ///
39    /// This variant should only be used in non-production test setups, as it implies the
40    /// persistence of (host-specific) encrypted administrative secrets on a file system, that
41    /// could be extracted if the host is compromised.
42    ///
43    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
44    SystemdCreds,
45
46    /// The administrative secrets are handled using [Shamir's Secret Sharing] (SSS).
47    ///
48    /// This variant is the default for production use, as the administrative secrets are only ever
49    /// exposed on a volatile filesystem for the time of their use.
50    /// The secrets are only made available to the system as shares of a shared secret, split using
51    /// SSS.
52    /// This way no holder of a share is aware of the administrative secrets and the system only
53    /// for as long as it needs to use the administrative secrets.
54    ///
55    /// [Shamir's Secret Sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
56    #[default]
57    ShamirsSecretSharing,
58}
59
60/// The handling of non-administrative secrets.
61///
62/// Non-administrative secrets represent passphrases for (non-Administrator) NetHSM users and may be
63/// handled in different ways (e.g. encrypted or not encrypted).
64#[derive(
65    Clone,
66    Copy,
67    Debug,
68    Default,
69    Deserialize,
70    strum::Display,
71    strum::EnumString,
72    Eq,
73    PartialEq,
74    Serialize,
75)]
76#[serde(rename_all = "kebab-case")]
77#[strum(serialize_all = "kebab-case")]
78pub enum NonAdministrativeSecretHandling {
79    /// Each non-administrative secret is handled in a plaintext file in a non-volatile
80    /// directory.
81    ///
82    /// ## Warning
83    ///
84    /// This variant should only be used in non-production test setups, as it implies the
85    /// persistence of unencrypted non-administrative secrets on a file system.
86    Plaintext,
87
88    /// Each non-administrative secret is encrypted for a specific system user using
89    /// [systemd-creds] and the resulting files are stored in a non-volatile directory.
90    ///
91    /// ## Note
92    ///
93    /// Although secrets are stored as encrypted strings in dedicated files, they may be extracted
94    /// under certain circumstances:
95    ///
96    /// - the root account is compromised
97    ///   - decrypts and exfiltrates _all_ secrets
98    ///   - the secret is not encrypted using a [TPM] and the file
99    ///     `/var/lib/systemd/credential.secret` as well as _any_ encrypted secret is exfiltrated
100    /// - a specific user is compromised, decrypts and exfiltrates its own secret
101    ///
102    /// It is therefore crucial to follow common best-practices:
103    ///
104    /// - rely on a [TPM] for encrypting secrets, so that files become host-specific
105    /// - heavily guard access to all users, especially root
106    ///
107    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
108    /// [TPM]: https://en.wikipedia.org/wiki/Trusted_Platform_Module
109    #[default]
110    SystemdCreds,
111}
112
113/// A configuration for parallel use of connections with a set of system and NetHSM users.
114///
115/// This configuration type is meant to be used in a read-only fashion and does not support tracking
116/// the passphrases for users.
117/// As such, it is useful for tools, that create system users, as well as NetHSM users and keys
118/// according to it.
119///
120/// Various mappings of system and NetHSM users exist, that are defined by the variants of
121/// [`UserMapping`].
122///
123/// Some system users require providing SSH authorized key(s), while others do not allow that at
124/// all.
125/// NetHSM users can be added in namespaces, or system-wide, depending on their use-case.
126/// System and NetHSM users must be unique.
127///
128/// Key IDs must be unique per namespace or system-wide (depending on where they are used).
129/// Tags, used to provide access to keys for NetHSM users must be unique per namespace or
130/// system-wide (depending on in which scope the user and key are used)
131///
132/// # Examples
133///
134/// The below example provides a fully functional TOML configuration, outlining all available
135/// functionalities.
136///
137/// ```
138/// # use std::io::Write;
139/// #
140/// # use signstar_config::{SignstarConfig};
141/// #
142/// # fn main() -> testresult::TestResult {
143/// # let config_file = testdir::testdir!().join("signstar_config_example.conf");
144/// # {
145/// let config_string = r#"
146/// ## A non-negative integer, that describes the iteration of the configuration.
147/// ## The iteration should only ever be increased between changes to the config and only under the circumstance,
148/// ## that user mappings are removed and should also be removed from the state of the system making use of this
149/// ## configuration.
150/// ## Applications reading the configuration are thereby enabled to compare existing state on the system with the
151/// ## current iteration and remove user mappings and accompanying data accordingly.
152/// iteration = 1
153///
154/// ## The handling of administrative secrets on the system.
155/// ## One of:
156/// ## - "shamirs-secret-sharing": Administrative secrets are never persisted on the system and only provided as shares of a shared secret.
157/// ## - "systemd-creds": Administrative secrets are persisted on the system as host-specific files, encrypted using systemd-creds (only for testing).
158/// ## - "plaintext": Administrative secrets are persisted on the system in unencrypted plaintext files (only for testing).
159/// admin_secret_handling = "shamirs-secret-sharing"
160///
161/// ## The handling of non-administrative secrets on the system.
162/// ## One of:
163/// ## - "systemd-creds": Non-administrative secrets are persisted on the system as host-specific files, encrypted using systemd-creds (the default).
164/// ## - "plaintext": Non-administrative secrets are persisted on the system in unencrypted plaintext files (only for testing).
165/// non_admin_secret_handling = "systemd-creds"
166///
167/// [[connections]]
168/// url = "https://localhost:8443/api/v1/"
169/// tls_security = "Unsafe"
170///
171/// ## The NetHSM user "admin" is a system-wide Administrator
172/// [[users]]
173/// nethsm_only_admin = "admin"
174///
175/// ## The SSH-accessible system user "ssh-backup1" is used in conjunction with
176/// ## the NetHSM user "backup1" (system-wide Backup)
177/// [[users]]
178///
179/// [users.system_nethsm_backup]
180/// nethsm_user = "backup1"
181/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
182/// system_user = "ssh-backup1"
183///
184/// ## The SSH-accessible system user "ssh-metrics1" is used with several NetHSM users:
185/// ## - "metrics1" (system-wide Metrics)
186/// ## - "keymetrics1" (system-wide Operator)
187/// ## - "ns1~keymetrics1" (namespace Operator)
188/// [[users]]
189///
190/// [users.system_nethsm_metrics]
191/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
192/// system_user = "ssh-metrics1"
193///
194/// [users.system_nethsm_metrics.nethsm_users]
195/// metrics_user = "metrics1"
196/// operator_users = ["keymetrics1", "ns1~keymetrics1"]
197///
198/// ## The SSH-accessible system user "ssh-operator1" is used in conjunction with
199/// ## the NetHSM user "operator1" (system-wide Operator).
200/// ## User "operator1" shares tag "tag1" with key "key1" and can therefore use it
201/// ## (for OpenPGP signing).
202/// [[users]]
203///
204/// [users.system_nethsm_operator_signing]
205/// nethsm_user = "operator1"
206/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
207/// system_user = "ssh-operator1"
208/// tag = "tag1"
209///
210/// [users.system_nethsm_operator_signing.nethsm_key_setup]
211/// key_id = "key1"
212/// key_type = "Curve25519"
213/// key_mechanisms = ["EdDsaSignature"]
214/// signature_type = "EdDsa"
215///
216/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
217/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
218/// version = "4"
219///
220/// ## The SSH-accessible system user "ssh-operator2" is used in conjunction with
221/// ## the NetHSM user "operator2" (system-wide Operator).
222/// ## User "operator2" shares tag "tag2" with key "key2" and can therefore use it
223/// ## (for OpenPGP signing).
224/// [[users]]
225///
226/// [users.system_nethsm_operator_signing]
227/// nethsm_user = "operator2"
228/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
229/// system_user = "ssh-operator2"
230/// tag = "tag2"
231///
232/// [users.system_nethsm_operator_signing.nethsm_key_setup]
233/// key_id = "key2"
234/// key_type = "Curve25519"
235/// key_mechanisms = ["EdDsaSignature"]
236/// signature_type = "EdDsa"
237///
238/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
239/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
240/// version = "4"
241///
242/// ## The NetHSM user "ns1~admin" is a namespace Administrator
243/// [[users]]
244/// nethsm_only_admin = "ns1~admin"
245///
246/// ## The SSH-accessible system user "ns1-ssh-operator1" is used in conjunction with
247/// ## the NetHSM user "ns1~operator1" (namespace Operator).
248/// ## User "ns1~operator1" shares tag "tag1" with key "key1" and can therefore use it
249/// ## in its namespace (for OpenPGP signing).
250/// [[users]]
251///
252/// [users.system_nethsm_operator_signing]
253/// nethsm_user = "ns1~operator1"
254/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host"
255/// system_user = "ns1-ssh-operator1"
256/// tag = "tag1"
257///
258/// [users.system_nethsm_operator_signing.nethsm_key_setup]
259/// key_id = "key1"
260/// key_type = "Curve25519"
261/// key_mechanisms = ["EdDsaSignature"]
262/// signature_type = "EdDsa"
263///
264/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
265/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
266/// version = "4"
267///
268/// ## The SSH-accessible system user "ns1-ssh-operator2" is used in conjunction with
269/// ## the NetHSM user "ns2~operator1" (namespace Operator).
270/// ## User "ns1~operator2" shares tag "tag2" with key "key1" and can therefore use it
271/// ## in its namespace (for OpenPGP signing).
272/// [[users]]
273///
274/// [users.system_nethsm_operator_signing]
275/// nethsm_user = "ns1~operator2"
276/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIYA+bfMBThUP5lKbMFEHiytmcCPhpkGrB/85n0mAN user@host"
277/// system_user = "ns1-ssh-operator2"
278/// tag = "tag2"
279///
280/// [users.system_nethsm_operator_signing.nethsm_key_setup]
281/// key_id = "key2"
282/// key_type = "Curve25519"
283/// key_mechanisms = ["EdDsaSignature"]
284/// signature_type = "EdDsa"
285///
286/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
287/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
288/// version = "4"
289///
290/// ## The hermetic system user "local-metrics1" is used with several NetHSM users:
291/// ## - "metrics2" (system-wide Metrics)
292/// ## - "keymetrics2" (system-wide Operator)
293/// ## - "ns1~keymetrics2" (namespace Operator)
294/// [[users]]
295///
296/// [users.hermetic_system_nethsm_metrics]
297/// system_user = "local-metrics1"
298///
299/// [users.hermetic_system_nethsm_metrics.nethsm_users]
300/// metrics_user = "metrics2"
301/// operator_users = ["keymetrics2", "ns1~keymetrics2"]
302///
303/// ## The SSH-accessible system user "ssh-share-down" is used for the
304/// ## download of shares of a shared secret (divided by Shamir's Secret Sharing).
305/// [[users]]
306///
307/// [users.system_only_share_download]
308/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"
309/// system_user = "ssh-share-down"
310///
311/// ## The SSH-accessible system user "ssh-share-up" is used for the
312/// ## upload of shares of a shared secret (divided by Shamir's Secret Sharing).
313/// [[users]]
314///
315/// [users.system_only_share_upload]
316/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"
317/// system_user = "ssh-share-up"
318///
319/// ## The SSH-accessible system user "ssh-wireguard-down" is used for the
320/// ## download of WireGuard configuration, used on the host.
321/// [[users]]
322///
323/// [users.system_only_wireguard_download]
324/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"
325/// system_user = "ssh-wireguard-down"
326/// "#;
327/// #
328/// #    let mut buffer = std::fs::File::create(&config_file)?;
329/// #    buffer.write_all(config_string.as_bytes())?;
330/// # }
331/// # SignstarConfig::new_from_file(
332/// #    Some(&config_file),
333/// # )?;
334/// # Ok(())
335/// # }
336/// ```
337#[derive(Clone, Debug, Default, Deserialize, Serialize)]
338pub struct SignstarConfig {
339    iteration: u32,
340    admin_secret_handling: AdministrativeSecretHandling,
341    non_admin_secret_handling: NonAdministrativeSecretHandling,
342    connections: HashSet<Connection>,
343    users: HashSet<UserMapping>,
344}
345
346impl SignstarConfig {
347    /// Creates a new [`SignstarConfig`] from an optional configuration file path.
348    ///
349    /// If no configuration file path is provided, attempts to return the first configuration file
350    /// location found using [`get_config_file`].
351    ///
352    /// # Errors
353    ///
354    /// Returns an error if
355    ///
356    /// - no configuration file path is provided and [`get_config_file`] is unable to find any,
357    /// - reading the contents of the configuration file to string fails,
358    /// - deserializing the contents of the configuration file as a [`SignstarConfig`],
359    /// - or the [`SignstarConfig`] fails to validate.
360    ///
361    /// # Examples
362    ///
363    /// ```
364    /// # use std::io::Write;
365    ///
366    /// use signstar_config::SignstarConfig;
367    ///
368    /// # fn main() -> testresult::TestResult {
369    /// let config_file = testdir::testdir!().join("signstar_config_new.conf");
370    /// {
371    ///     #[rustfmt::skip]
372    ///     let config_string = r#"
373    /// iteration = 1
374    /// admin_secret_handling = "shamirs-secret-sharing"
375    /// non_admin_secret_handling = "systemd-creds"
376    /// [[connections]]
377    /// url = "https://localhost:8443/api/v1/"
378    /// tls_security = "Unsafe"
379    ///
380    /// [[users]]
381    /// nethsm_only_admin = "admin"
382    ///
383    /// [[users]]
384    /// [users.system_nethsm_backup]
385    /// nethsm_user = "backup1"
386    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
387    /// system_user = "ssh-backup1"
388    ///
389    /// [[users]]
390    ///
391    /// [users.system_nethsm_metrics]
392    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
393    /// system_user = "ssh-metrics1"
394    ///
395    /// [users.system_nethsm_metrics.nethsm_users]
396    /// metrics_user = "metrics1"
397    /// operator_users = ["operator1metrics1"]
398    ///
399    /// [[users]]
400    /// [users.system_nethsm_operator_signing]
401    /// nethsm_user = "operator1"
402    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
403    /// system_user = "ssh-operator1"
404    /// tag = "tag1"
405    ///
406    /// [users.system_nethsm_operator_signing.nethsm_key_setup]
407    /// key_id = "key1"
408    /// key_type = "Curve25519"
409    /// key_mechanisms = ["EdDsaSignature"]
410    /// signature_type = "EdDsa"
411    ///
412    /// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
413    /// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
414    /// version = "4"
415    ///
416    /// [[users]]
417    /// [users.system_nethsm_operator_signing]
418    /// nethsm_user = "operator2"
419    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
420    /// system_user = "ssh-operator2"
421    /// tag = "tag2"
422    ///
423    /// [users.system_nethsm_operator_signing.nethsm_key_setup]
424    /// key_id = "key2"
425    /// key_type = "Curve25519"
426    /// key_mechanisms = ["EdDsaSignature"]
427    /// signature_type = "EdDsa"
428    ///
429    /// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
430    /// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
431    /// version = "4"
432    ///
433    /// [[users]]
434    ///
435    /// [users.hermetic_system_nethsm_metrics]
436    /// system_user = "local-metrics1"
437    ///
438    /// [users.hermetic_system_nethsm_metrics.nethsm_users]
439    /// metrics_user = "metrics2"
440    /// operator_users = ["operator2metrics1"]
441    ///
442    /// [[users]]
443    /// [users.system_only_share_download]
444    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"
445    /// system_user = "ssh-share-down"
446    ///
447    /// [[users]]
448    /// [users.system_only_share_upload]
449    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"
450    /// system_user = "ssh-share-up"
451    ///
452    /// [[users]]
453    /// [users.system_only_wireguard_download]
454    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"
455    /// system_user = "ssh-wireguard-down"
456    /// "#;
457    ///     let mut buffer = std::fs::File::create(&config_file)?;
458    ///     buffer.write_all(config_string.as_bytes())?;
459    /// }
460    /// SignstarConfig::new_from_file(Some(&config_file))?;
461    /// # Ok(())
462    /// # }
463    /// ```
464    pub fn new_from_file(path: Option<&Path>) -> Result<Self, crate::Error> {
465        let path = if let Some(path) = path {
466            path.to_path_buf()
467        } else {
468            let Some(path) = get_config_file() else {
469                return Err(Error::ConfigIsMissing.into());
470            };
471            path
472        };
473
474        let config: Self =
475            toml::from_str(
476                &read_to_string(&path).map_err(|source| crate::Error::IoPath {
477                    path: path.clone(),
478                    context: "reading it to string",
479                    source,
480                })?,
481            )
482            .map_err(|source| crate::Error::TomlRead {
483                path,
484                context: "reading it as a Signstar config",
485                source: Box::new(source),
486            })?;
487        config.validate()?;
488
489        Ok(config)
490    }
491
492    /// Creates a new [`SignstarConfig`].
493    ///
494    /// # Errors
495    ///
496    /// Returns an error if the configuration file can not be loaded.
497    ///
498    /// # Examples
499    ///
500    /// ```
501    /// use std::collections::HashSet;
502    ///
503    /// use nethsm::{Connection, UserRole};
504    /// use signstar_config::{
505    ///     AdministrativeSecretHandling,
506    ///     SignstarConfig,
507    ///     NonAdministrativeSecretHandling,
508    ///     UserMapping,
509    /// };
510    ///
511    /// # fn main() -> testresult::TestResult {
512    /// SignstarConfig::new(
513    ///     1,
514    ///     AdministrativeSecretHandling::ShamirsSecretSharing,
515    ///     NonAdministrativeSecretHandling::SystemdCreds,
516    ///     HashSet::from([Connection::new(
517    ///         "https://localhost:8443/api/v1/".parse()?,
518    ///         "Unsafe".parse()?,
519    ///     )]),
520    ///     HashSet::from([
521    ///         UserMapping::NetHsmOnlyAdmin("admin".parse()?),
522    ///         UserMapping::SystemOnlyShareDownload {
523    ///             system_user: "ssh-share-down".parse()?,
524    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
525    ///         },
526    ///         UserMapping::SystemOnlyShareUpload {
527    ///             system_user: "ssh-share-up".parse()?,
528    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
529    ///         }]),
530    /// )?;
531    /// # Ok(())
532    /// # }
533    /// ```
534    pub fn new(
535        iteration: u32,
536        admin_secret_handling: AdministrativeSecretHandling,
537        non_admin_secret_handling: NonAdministrativeSecretHandling,
538        connections: HashSet<Connection>,
539        users: HashSet<UserMapping>,
540    ) -> Result<Self, crate::Error> {
541        let config = Self {
542            iteration,
543            admin_secret_handling,
544            non_admin_secret_handling,
545            connections,
546            users,
547        };
548        config.validate()?;
549        Ok(config)
550    }
551
552    /// Writes a [`SignstarConfig`] to file.
553    ///
554    /// # Errors
555    ///
556    /// Returns an error if
557    ///
558    /// - the parent directory for the configuration file cannot be created,
559    /// - the configuration file cannot be created,
560    /// - `self` cannot be serialized into a TOML string,
561    /// - or the TOML string cannot be written to the configuration file.
562    ///
563    /// # Examples
564    ///
565    /// ```
566    /// use std::collections::HashSet;
567    ///
568    /// use nethsm::{Connection,CryptographicKeyContext, OpenPgpUserIdList, SigningKeySetup, UserRole};
569    /// use signstar_config::{
570    ///     AdministrativeSecretHandling,
571    ///     SignstarConfig,
572    ///     NetHsmMetricsUsers,
573    ///     NonAdministrativeSecretHandling,
574    ///     UserMapping,
575    /// };
576    ///
577    /// # fn main() -> testresult::TestResult {
578    /// let config = SignstarConfig::new(
579    ///     1,
580    ///     AdministrativeSecretHandling::ShamirsSecretSharing,
581    ///     NonAdministrativeSecretHandling::SystemdCreds,
582    ///     HashSet::from([Connection::new(
583    ///         "https://localhost:8443/api/v1/".parse()?,
584    ///         "Unsafe".parse()?,
585    ///     )]),
586    ///     HashSet::from([UserMapping::NetHsmOnlyAdmin("admin".parse()?),
587    ///         UserMapping::SystemNetHsmBackup {
588    ///             nethsm_user: "backup1".parse()?,
589    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
590    ///             system_user: "ssh-backup1".parse()?,
591    ///         },
592    ///         UserMapping::SystemNetHsmMetrics {
593    ///             nethsm_users: NetHsmMetricsUsers::new("metrics1".parse()?, vec!["operator2metrics1".parse()?])?,
594    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIioJ9uvAxUPunFh89T+ENo7OQerqHE8SQ+2v4VWbfUZ user@host".parse()?,
595    ///             system_user: "ssh-metrics1".parse()?,
596    ///         },
597    ///         UserMapping::SystemNetHsmOperatorSigning {
598    ///             nethsm_user: "operator1".parse()?,
599    ///             nethsm_key_setup: SigningKeySetup::new(
600    ///                 "key1".parse()?,
601    ///                 "Curve25519".parse()?,
602    ///                 vec!["EdDsaSignature".parse()?],
603    ///                 None,
604    ///                 "EdDsa".parse()?,
605    ///                 CryptographicKeyContext::OpenPgp{
606    ///                     user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
607    ///                     version: "4".parse()?,
608    ///                 })?,
609    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
610    ///             system_user: "ssh-operator1".parse()?,
611    ///             tag: "tag1".to_string(),
612    ///         },
613    ///         UserMapping::HermeticSystemNetHsmMetrics {
614    ///             nethsm_users: NetHsmMetricsUsers::new("metrics2".parse()?, vec!["operator1metrics1".parse()?])?,
615    ///             system_user: "local-metrics1".parse()?,
616    ///         },
617    ///         UserMapping::SystemOnlyShareDownload {
618    ///             system_user: "ssh-share-down".parse()?,
619    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
620    ///         },
621    ///         UserMapping::SystemOnlyShareUpload {
622    ///             system_user: "ssh-share-up".parse()?,
623    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
624    ///         },
625    ///         UserMapping::SystemOnlyWireGuardDownload {
626    ///             system_user: "ssh-wireguard-down".parse()?,
627    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
628    ///         },
629    ///     ]),
630    /// )?;
631    ///
632    /// let config_file = testdir::testdir!().join("signstar_config_store.conf");
633    /// config.store(Some(&config_file))?;
634    /// # println!("{}", std::fs::read_to_string(&config_file)?);
635    /// # Ok(())
636    /// # }
637    /// ```
638    pub fn store(&self, path: Option<&Path>) -> Result<(), crate::Error> {
639        let path = if let Some(path) = path {
640            path.to_path_buf()
641        } else {
642            get_run_override_config_file_path()
643        };
644
645        if let Some(parent) = path.parent() {
646            create_dir_all(parent).map_err(|source| crate::Error::IoPath {
647                path: parent.to_path_buf(),
648                context: "creating the parent directory for a Signstar configuration",
649                source,
650            })?;
651        }
652        let mut output = File::create(&path).map_err(|source| crate::Error::IoPath {
653            path: path.clone(),
654            context: "creating a Signstar configuration file",
655            source,
656        })?;
657
658        write!(
659            output,
660            "{}",
661            toml::to_string_pretty(self).map_err(|source| crate::Error::TomlWrite {
662                path: path.clone(),
663                context: "creating a Signstar configuration",
664                source,
665            })?
666        )
667        .map_err(|source| crate::Error::IoPath {
668            path: path.clone(),
669            context: "writing to the Signstar configuration file",
670            source,
671        })
672    }
673
674    /// Returns an Iterator over the available [`Connection`]s.
675    pub fn iter_connections(&self) -> impl Iterator<Item = &Connection> {
676        self.connections.iter()
677    }
678
679    /// Returns an Iterator over the available [`UserMapping`]s.
680    pub fn iter_user_mappings(&self) -> impl Iterator<Item = &UserMapping> {
681        self.users.iter()
682    }
683
684    /// Returns the iteration.
685    pub fn get_iteration(&self) -> u32 {
686        self.iteration
687    }
688
689    /// Returns the [`AdministrativeSecretHandling`].
690    pub fn get_administrative_secret_handling(&self) -> AdministrativeSecretHandling {
691        self.admin_secret_handling
692    }
693
694    /// Returns the [`NonAdministrativeSecretHandling`].
695    pub fn get_non_administrative_secret_handling(&self) -> NonAdministrativeSecretHandling {
696        self.non_admin_secret_handling
697    }
698
699    /// Returns an [`ExtendedUserMapping`] for a system user of `name` if it exists.
700    ///
701    /// Returns [`None`] if no user of `name` can is found.
702    pub fn get_extended_mapping_for_user(&self, name: &str) -> Option<ExtendedUserMapping> {
703        for user_mapping in self.users.iter() {
704            if user_mapping
705                .get_system_user()
706                .is_some_and(|system_user| system_user.as_ref() == name)
707            {
708                return Some(ExtendedUserMapping::new(
709                    self.admin_secret_handling,
710                    self.non_admin_secret_handling,
711                    self.connections.clone(),
712                    user_mapping.clone(),
713                ));
714            }
715        }
716        None
717    }
718
719    /// Validates the components of the [`SignstarConfig`].
720    fn validate(&self) -> Result<(), crate::Error> {
721        // ensure there are no duplicate system users
722        {
723            let mut system_users = HashSet::new();
724            for system_user_id in self
725                .users
726                .iter()
727                .filter_map(|mapping| mapping.get_system_user())
728            {
729                if !system_users.insert(system_user_id.clone()) {
730                    return Err(Error::DuplicateSystemUserId {
731                        system_user_id: system_user_id.clone(),
732                    }
733                    .into());
734                }
735            }
736        }
737
738        // ensure there are no duplicate NetHsm users
739        {
740            let mut nethsm_users = HashSet::new();
741            for nethsm_user_id in self
742                .users
743                .iter()
744                .flat_map(|mapping| mapping.get_nethsm_users())
745            {
746                if !nethsm_users.insert(nethsm_user_id.clone()) {
747                    return Err(Error::DuplicateNetHsmUserId {
748                        nethsm_user_id: nethsm_user_id.clone(),
749                    }
750                    .into());
751                }
752            }
753        }
754
755        // ensure that there is at least one system-wide administrator
756        if self
757            .users
758            .iter()
759            .filter_map(|mapping| {
760                if let UserMapping::NetHsmOnlyAdmin(user_id) = mapping {
761                    if !user_id.is_namespaced() {
762                        Some(user_id)
763                    } else {
764                        None
765                    }
766                } else {
767                    None
768                }
769            })
770            .next()
771            .is_none()
772        {
773            return Err(Error::MissingAdministrator { namespaces: None }.into());
774        }
775
776        // ensure that there is an Administrator in each used namespace
777        {
778            // namespaces for all users, that are not in the Administrator role
779            let namespaces_users = self
780                .users
781                .iter()
782                .filter(|mapping| !matches!(mapping, UserMapping::NetHsmOnlyAdmin(_)))
783                .flat_map(|mapping| mapping.get_namespaces())
784                .collect::<HashSet<NamespaceId>>();
785            // namespaces for all users, that are in the Administrator role
786            let namespaces_admins = self
787                .users
788                .iter()
789                .filter(|mapping| matches!(mapping, UserMapping::NetHsmOnlyAdmin(_)))
790                .flat_map(|mapping| mapping.get_namespaces())
791                .collect::<HashSet<NamespaceId>>();
792
793            let namespaces = namespaces_users
794                .difference(&namespaces_admins)
795                .cloned()
796                .collect::<Vec<NamespaceId>>();
797            if !namespaces.is_empty() {
798                return Err(Error::MissingAdministrator {
799                    namespaces: Some(namespaces),
800                }
801                .into());
802            }
803        }
804
805        if self.admin_secret_handling == AdministrativeSecretHandling::ShamirsSecretSharing {
806            // ensure there is at least one system user for downloading shares of a shared
807            // secret
808            if !self
809                .users
810                .iter()
811                .any(|mapping| matches!(mapping, UserMapping::SystemOnlyShareDownload { .. }))
812            {
813                return Err(Error::MissingShareDownloadSystemUser.into());
814            }
815
816            // ensure there is at least one system user for uploading shares of a shared secret
817            if !self
818                .users
819                .iter()
820                .any(|mapping| matches!(mapping, UserMapping::SystemOnlyShareUpload { .. }))
821            {
822                return Err(Error::MissingShareUploadSystemUser.into());
823            }
824        } else {
825            // ensure there is no system user setup for uploading or downloading of shares of a
826            // shared secret
827            let share_users: Vec<SystemUserId> = self
828                .users
829                .iter()
830                .filter_map(|mapping| match mapping {
831                    UserMapping::SystemOnlyShareUpload {
832                        system_user,
833                        ssh_authorized_key: _,
834                    }
835                    | UserMapping::SystemOnlyShareDownload {
836                        system_user,
837                        ssh_authorized_key: _,
838                    } => Some(system_user.clone()),
839                    _ => None,
840                })
841                .collect();
842            if !share_users.is_empty() {
843                return Err(Error::NoSssButShareUsers { share_users }.into());
844            }
845        }
846
847        // ensure there are no duplicate authorized SSH keys in the set of uploading shareholders
848        // and the rest (minus downloading shareholders)
849        {
850            let mut public_keys = HashSet::new();
851            for ssh_authorized_key in self
852                .users
853                .iter()
854                .filter(|mapping| {
855                    !matches!(
856                        mapping,
857                        UserMapping::SystemOnlyShareDownload {
858                            system_user: _,
859                            ssh_authorized_key: _,
860                        }
861                    )
862                })
863                .flat_map(|mapping| mapping.get_ssh_authorized_key())
864                // we know a valid Entry can be created from AuthorizedKeyEntry, because its
865                // constructor ensures it, hence we discard Errors
866                .filter_map(|authorized_key| {
867                    ssh_key::authorized_keys::Entry::try_from(authorized_key).ok()
868                })
869            {
870                if !public_keys.insert(ssh_authorized_key.public_key().clone()) {
871                    return Err(Error::DuplicateSshPublicKey {
872                        ssh_public_key: ssh_authorized_key.public_key().to_string(),
873                    }
874                    .into());
875                }
876            }
877        }
878
879        // ensure there are no duplicate authorized SSH keys in the set of downloading shareholders
880        // and the rest (minus uploading shareholders)
881        {
882            let mut public_keys = HashSet::new();
883            for ssh_authorized_key in self
884                .users
885                .iter()
886                .filter(|mapping| {
887                    !matches!(
888                        mapping,
889                        UserMapping::SystemOnlyShareUpload {
890                            system_user: _,
891                            ssh_authorized_key: _,
892                        }
893                    )
894                })
895                .flat_map(|mapping| mapping.get_ssh_authorized_key())
896                // we know a valid Entry can be created from AuthorizedKeyEntry, because its
897                // constructor ensures it, hence we discard Errors
898                .filter_map(|authorized_key| {
899                    ssh_key::authorized_keys::Entry::try_from(authorized_key).ok()
900                })
901            {
902                if !public_keys.insert(ssh_authorized_key.public_key().clone()) {
903                    return Err(Error::DuplicateSshPublicKey {
904                        ssh_public_key: ssh_authorized_key.public_key().to_string(),
905                    }
906                    .into());
907                }
908            }
909        }
910
911        // ensure that only one-to-one relationships between users in the Operator role and keys
912        // exist (system-wide and per-namespace)
913        {
914            // ensure that KeyIds are not reused system-wide
915            let mut set = HashSet::new();
916            for key_id in self
917                .users
918                .iter()
919                .flat_map(|mapping| mapping.get_key_ids(None))
920            {
921                if !set.insert(key_id.clone()) {
922                    return Err(Error::DuplicateKeyId {
923                        key_id,
924                        namespace: None,
925                    }
926                    .into());
927                }
928            }
929
930            // ensure that KeyIds are not reused per namespace
931            for namespace in self
932                .users
933                .iter()
934                .flat_map(|mapping| mapping.get_namespaces())
935            {
936                let mut set = HashSet::new();
937                for key_id in self
938                    .users
939                    .iter()
940                    .flat_map(|mapping| mapping.get_key_ids(Some(&namespace)))
941                {
942                    if !set.insert(key_id.clone()) {
943                        return Err(Error::DuplicateKeyId {
944                            key_id,
945                            namespace: Some(namespace),
946                        }
947                        .into());
948                    }
949                }
950            }
951        }
952
953        // ensure unique tags system-wide and per namespace
954        {
955            // ensure that tags are unique system-wide
956            let mut set = HashSet::new();
957            for tag in self.users.iter().flat_map(|mapping| mapping.get_tags(None)) {
958                if !set.insert(tag) {
959                    return Err(Error::DuplicateTag {
960                        tag: tag.to_string(),
961                        namespace: None,
962                    }
963                    .into());
964                }
965            }
966
967            // ensure that tags are unique in each namespace
968            for namespace in self
969                .users
970                .iter()
971                .flat_map(|mapping| mapping.get_namespaces())
972            {
973                let mut set = HashSet::new();
974                for tag in self
975                    .users
976                    .iter()
977                    .flat_map(|mapping| mapping.get_tags(Some(&namespace)))
978                {
979                    if !set.insert(tag) {
980                        return Err(Error::DuplicateTag {
981                            tag: tag.to_string(),
982                            namespace: Some(namespace),
983                        }
984                        .into());
985                    }
986                }
987            }
988        }
989
990        Ok(())
991    }
992}
993
994#[cfg(test)]
995mod tests {
996    use core::panic;
997    use std::path::PathBuf;
998
999    use rstest::rstest;
1000    use testresult::TestResult;
1001
1002    use super::*;
1003
1004    #[rstest]
1005    fn signstar_config_new_from_file(
1006        #[files("signstar-config-*.toml")]
1007        #[base_dir = "tests/fixtures/working/"]
1008        config_file: PathBuf,
1009    ) -> TestResult {
1010        SignstarConfig::new_from_file(Some(&config_file))?;
1011
1012        Ok(())
1013    }
1014
1015    #[rstest]
1016    fn signstar_config_duplicate_system_user(
1017        #[files("signstar-config-*.toml")]
1018        #[base_dir = "tests/fixtures/duplicate-system-user/"]
1019        config_file: PathBuf,
1020    ) -> TestResult {
1021        println!("{config_file:?}");
1022        match SignstarConfig::new_from_file(Some(&config_file)) {
1023            Err(crate::Error::Config(Error::DuplicateSystemUserId { .. })) => Ok(()),
1024            Ok(_) => panic!("Did not trigger any Error!"),
1025            Err(error) => panic!("Did not trigger the correct Error: {:?}!", error),
1026        }
1027    }
1028
1029    #[rstest]
1030    fn signstar_config_duplicate_nethsm_user(
1031        #[files("signstar-config-*.toml")]
1032        #[base_dir = "tests/fixtures/duplicate-nethsm-user/"]
1033        config_file: PathBuf,
1034    ) -> TestResult {
1035        if let Err(crate::Error::Config(Error::DuplicateNetHsmUserId { .. })) =
1036            SignstarConfig::new_from_file(Some(&config_file))
1037        {
1038            Ok(())
1039        } else {
1040            panic!("Did not trigger the correct Error!")
1041        }
1042    }
1043
1044    #[rstest]
1045    fn signstar_config_missing_administrator(
1046        #[files("signstar-config-*.toml")]
1047        #[base_dir = "tests/fixtures/missing-administrator/"]
1048        config_file: PathBuf,
1049    ) -> TestResult {
1050        if let Err(crate::Error::Config(Error::MissingAdministrator { .. })) =
1051            SignstarConfig::new_from_file(Some(&config_file))
1052        {
1053            Ok(())
1054        } else {
1055            panic!("Did not trigger the correct Error!")
1056        }
1057    }
1058
1059    #[rstest]
1060    fn signstar_config_missing_namespace_administrators(
1061        #[files("signstar-config-*.toml")]
1062        #[base_dir = "tests/fixtures/missing-namespace-administrator/"]
1063        config_file: PathBuf,
1064    ) -> TestResult {
1065        if let Err(crate::Error::Config(Error::MissingAdministrator { .. })) =
1066            SignstarConfig::new_from_file(Some(&config_file))
1067        {
1068            Ok(())
1069        } else {
1070            panic!("Did not trigger the correct Error!")
1071        }
1072    }
1073
1074    #[rstest]
1075    fn signstar_config_duplicate_authorized_keys_share_uploader(
1076        #[files("signstar-config-*.toml")]
1077        #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-uploader/"]
1078        config_file: PathBuf,
1079    ) -> TestResult {
1080        println!("Using configuration {config_file:?}");
1081        let config_file_string = config_file
1082            .clone()
1083            .into_os_string()
1084            .into_string()
1085            .map_err(|e| format!("Can't convert {config_file:?}:\n{e:?}"))?;
1086        // when using plaintext or systemd-creds for administrative credentials, there are no share
1087        // uploaders
1088        if config_file_string.ends_with("ntext.toml")
1089            || config_file_string.ends_with("emd-creds.toml")
1090        {
1091            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1092            Ok(())
1093        } else if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1094            SignstarConfig::new_from_file(Some(&config_file))
1095        {
1096            Ok(())
1097        } else {
1098            panic!("Did not trigger the correct Error!")
1099        }
1100    }
1101
1102    #[rstest]
1103    fn signstar_config_duplicate_authorized_keys_share_downloader(
1104        #[files("signstar-config-*.toml")]
1105        #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-downloader/"]
1106        config_file: PathBuf,
1107    ) -> TestResult {
1108        println!("Using configuration {config_file:?}");
1109        let config_file_string = config_file
1110            .clone()
1111            .into_os_string()
1112            .into_string()
1113            .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1114        // when using plaintext or systemd-creds for administrative credentials, there are no share
1115        // downloaders
1116        if config_file_string.ends_with("ntext.toml")
1117            || config_file_string.ends_with("systemd-creds.toml")
1118        {
1119            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1120            Ok(())
1121        } else if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1122            SignstarConfig::new_from_file(Some(&config_file))
1123        {
1124            Ok(())
1125        } else {
1126            panic!("Did not trigger the correct Error!")
1127        }
1128    }
1129
1130    #[rstest]
1131    fn signstar_config_duplicate_authorized_keys_users(
1132        #[files("signstar-config-*.toml")]
1133        #[base_dir = "tests/fixtures/duplicate-authorized-keys-users/"]
1134        config_file: PathBuf,
1135    ) -> TestResult {
1136        if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1137            SignstarConfig::new_from_file(Some(&config_file))
1138        {
1139            Ok(())
1140        } else {
1141            panic!("Did not trigger the correct Error!")
1142        }
1143    }
1144
1145    #[rstest]
1146    fn signstar_config_missing_share_download_user(
1147        #[files("signstar-config-*.toml")]
1148        #[base_dir = "tests/fixtures/missing-share-download-user/"]
1149        config_file: PathBuf,
1150    ) -> TestResult {
1151        println!("Using configuration {config_file:?}");
1152        let config_file_string = config_file
1153            .clone()
1154            .into_os_string()
1155            .into_string()
1156            .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1157        // when using plaintext or systemd-creds for administrative credentials, there are no share
1158        // downloaders
1159        if config_file_string.ends_with("plaintext.toml")
1160            || config_file_string.ends_with("systemd-creds.toml")
1161        {
1162            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1163            Ok(())
1164        } else if let Err(crate::Error::Config(Error::MissingShareDownloadSystemUser)) =
1165            SignstarConfig::new_from_file(Some(&config_file))
1166        {
1167            Ok(())
1168        } else {
1169            panic!("Did not trigger the correct Error!")
1170        }
1171    }
1172
1173    #[rstest]
1174    fn signstar_config_missing_share_upload_user(
1175        #[files("signstar-config-*.toml")]
1176        #[base_dir = "tests/fixtures/missing-share-upload-user/"]
1177        config_file: PathBuf,
1178    ) -> TestResult {
1179        println!("Using configuration {config_file:?}");
1180        let config_file_string = config_file
1181            .clone()
1182            .into_os_string()
1183            .into_string()
1184            .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1185        // when using plaintext or systemd-creds for administrative credentials, there are no share
1186        // downloaders
1187        if config_file_string.ends_with("plaintext.toml")
1188            || config_file_string.ends_with("systemd-creds.toml")
1189        {
1190            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1191            Ok(())
1192        } else if let Err(crate::Error::Config(Error::MissingShareUploadSystemUser)) =
1193            SignstarConfig::new_from_file(Some(&config_file))
1194        {
1195            Ok(())
1196        } else {
1197            panic!("Did not trigger the correct Error!")
1198        }
1199    }
1200
1201    #[rstest]
1202    fn signstar_config_no_sss_but_shares(
1203        #[files("signstar-config-*.toml")]
1204        #[base_dir = "tests/fixtures/no-sss-but-shares/"]
1205        config_file: PathBuf,
1206    ) -> TestResult {
1207        println!("Using configuration {config_file:?}");
1208        let config_file_string = config_file
1209            .clone()
1210            .into_os_string()
1211            .into_string()
1212            .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1213        // when using shamir's secret sharing for administrative credentials, there ought to be
1214        // share downloaders and uploaders
1215        if config_file_string.ends_with("irs-secret-sharing.toml") {
1216            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1217            Ok(())
1218        } else if let Err(crate::Error::Config(Error::NoSssButShareUsers { .. })) =
1219            SignstarConfig::new_from_file(Some(&config_file))
1220        {
1221            Ok(())
1222        } else {
1223            panic!("Did not trigger the correct Error!")
1224        }
1225    }
1226
1227    #[rstest]
1228    fn signstar_config_duplicate_key_id(
1229        #[files("signstar-config-*.toml")]
1230        #[base_dir = "tests/fixtures/duplicate-key-id/"]
1231        config_file: PathBuf,
1232    ) -> TestResult {
1233        if let Err(crate::Error::Config(Error::DuplicateKeyId { .. })) =
1234            SignstarConfig::new_from_file(Some(&config_file))
1235        {
1236            Ok(())
1237        } else {
1238            panic!("Did not trigger the correct Error!")
1239        }
1240    }
1241
1242    #[rstest]
1243    fn signstar_config_duplicate_key_id_in_namespace(
1244        #[files("signstar-config-*.toml")]
1245        #[base_dir = "tests/fixtures/duplicate-key-id-in-namespace/"]
1246        config_file: PathBuf,
1247    ) -> TestResult {
1248        if let Err(crate::Error::Config(Error::DuplicateKeyId { .. })) =
1249            SignstarConfig::new_from_file(Some(&config_file))
1250        {
1251            Ok(())
1252        } else {
1253            panic!("Did not trigger the correct Error!")
1254        }
1255    }
1256
1257    #[rstest]
1258    fn signstar_config_duplicate_tag(
1259        #[files("signstar-config-*.toml")]
1260        #[base_dir = "tests/fixtures/duplicate-tag/"]
1261        config_file: PathBuf,
1262    ) -> TestResult {
1263        if let Err(crate::Error::Config(Error::DuplicateTag { .. })) =
1264            SignstarConfig::new_from_file(Some(&config_file))
1265        {
1266            Ok(())
1267        } else {
1268            panic!("Did not trigger the correct Error!")
1269        }
1270    }
1271
1272    #[rstest]
1273    fn signstar_config_duplicate_tag_in_namespace(
1274        #[files("signstar-config-*.toml")]
1275        #[base_dir = "tests/fixtures/duplicate-tag-in-namespace/"]
1276        config_file: PathBuf,
1277    ) -> TestResult {
1278        if let Err(crate::Error::Config(Error::DuplicateTag { .. })) =
1279            SignstarConfig::new_from_file(Some(&config_file))
1280        {
1281            Ok(())
1282        } else {
1283            panic!("Did not trigger the correct Error!")
1284        }
1285    }
1286
1287    #[rstest]
1288    #[case("ssh-backup1")]
1289    #[case("ssh-metrics1")]
1290    #[case("ssh-operator1")]
1291    #[case("ssh-operator2")]
1292    #[case("ns1-ssh-operator1")]
1293    #[case("ns1-ssh-operator2")]
1294    #[case("local-metrics1")]
1295    #[case("ssh-wireguard-down")]
1296    fn signstar_config_get_extended_usermapping_succeeds(
1297        #[files("signstar-config-*.toml")]
1298        #[base_dir = "tests/fixtures/working/"]
1299        config_file: PathBuf,
1300        #[case] name: &str,
1301    ) -> TestResult {
1302        let config = SignstarConfig::new_from_file(Some(&config_file))?;
1303        if config.get_extended_mapping_for_user(name).is_none() {
1304            panic!("The user with name {name} is supposed to exist in the Signstar config");
1305        }
1306
1307        Ok(())
1308    }
1309
1310    #[rstest]
1311    fn signstar_config_get_extended_usermapping_fails(
1312        #[files("signstar-config-*.toml")]
1313        #[base_dir = "tests/fixtures/working/"]
1314        config_file: PathBuf,
1315    ) -> TestResult {
1316        let config = SignstarConfig::new_from_file(Some(&config_file))?;
1317        if config.get_extended_mapping_for_user("foo").is_some() {
1318            panic!("The user \"foo\" should not exist in the Signstar config");
1319        }
1320
1321        Ok(())
1322    }
1323}