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