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                .filter_map(|authorized_key| {
887                    ssh_key::authorized_keys::Entry::try_from(authorized_key).ok()
888                })
889            {
890                if !public_keys.insert(ssh_authorized_key.public_key().clone()) {
891                    return Err(Error::DuplicateSshPublicKey {
892                        ssh_public_key: ssh_authorized_key.public_key().to_string(),
893                    }
894                    .into());
895                }
896            }
897        }
898
899        // ensure there are no duplicate authorized SSH keys in the set of downloading shareholders
900        // and the rest (minus uploading shareholders)
901        {
902            let mut public_keys = HashSet::new();
903            for ssh_authorized_key in self
904                .users
905                .iter()
906                .filter(|mapping| {
907                    !matches!(
908                        mapping,
909                        UserMapping::SystemOnlyShareUpload {
910                            system_user: _,
911                            ssh_authorized_key: _,
912                        }
913                    )
914                })
915                .flat_map(|mapping| mapping.get_ssh_authorized_key())
916                // we know a valid Entry can be created from AuthorizedKeyEntry, because its
917                // constructor ensures it, hence we discard Errors
918                .filter_map(|authorized_key| {
919                    ssh_key::authorized_keys::Entry::try_from(authorized_key).ok()
920                })
921            {
922                if !public_keys.insert(ssh_authorized_key.public_key().clone()) {
923                    return Err(Error::DuplicateSshPublicKey {
924                        ssh_public_key: ssh_authorized_key.public_key().to_string(),
925                    }
926                    .into());
927                }
928            }
929        }
930
931        // ensure that only one-to-one relationships between users in the Operator role and keys
932        // exist (system-wide and per-namespace)
933        {
934            // ensure that KeyIds are not reused system-wide
935            let mut set = HashSet::new();
936            for key_id in self
937                .users
938                .iter()
939                .flat_map(|mapping| mapping.get_nethsm_key_ids(None))
940            {
941                if !set.insert(key_id.clone()) {
942                    return Err(Error::DuplicateKeyId {
943                        key_id,
944                        namespace: None,
945                    }
946                    .into());
947                }
948            }
949
950            // ensure that KeyIds are not reused per namespace
951            for namespace in self
952                .users
953                .iter()
954                .flat_map(|mapping| mapping.get_nethsm_namespaces())
955            {
956                let mut set = HashSet::new();
957                for key_id in self
958                    .users
959                    .iter()
960                    .flat_map(|mapping| mapping.get_nethsm_key_ids(Some(&namespace)))
961                {
962                    if !set.insert(key_id.clone()) {
963                        return Err(Error::DuplicateKeyId {
964                            key_id,
965                            namespace: Some(namespace),
966                        }
967                        .into());
968                    }
969                }
970            }
971        }
972
973        // ensure unique tags system-wide and per namespace
974        {
975            // ensure that tags are unique system-wide
976            let mut set = HashSet::new();
977            for tag in self
978                .users
979                .iter()
980                .flat_map(|mapping| mapping.get_nethsm_tags(None))
981            {
982                if !set.insert(tag) {
983                    return Err(Error::DuplicateTag {
984                        tag: tag.to_string(),
985                        namespace: None,
986                    }
987                    .into());
988                }
989            }
990
991            // ensure that tags are unique in each namespace
992            for namespace in self
993                .users
994                .iter()
995                .flat_map(|mapping| mapping.get_nethsm_namespaces())
996            {
997                let mut set = HashSet::new();
998                for tag in self
999                    .users
1000                    .iter()
1001                    .flat_map(|mapping| mapping.get_nethsm_tags(Some(&namespace)))
1002                {
1003                    if !set.insert(tag) {
1004                        return Err(Error::DuplicateTag {
1005                            tag: tag.to_string(),
1006                            namespace: Some(namespace),
1007                        }
1008                        .into());
1009                    }
1010                }
1011            }
1012        }
1013
1014        Ok(())
1015    }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use core::panic;
1021    use std::path::PathBuf;
1022
1023    use rstest::rstest;
1024    use testresult::TestResult;
1025
1026    use super::*;
1027
1028    #[rstest]
1029    fn signstar_config_new_from_file(
1030        #[files("signstar-config-*.toml")]
1031        #[base_dir = "tests/fixtures/working/"]
1032        config_file: PathBuf,
1033    ) -> TestResult {
1034        SignstarConfig::new_from_file(Some(&config_file))?;
1035
1036        Ok(())
1037    }
1038
1039    #[rstest]
1040    fn signstar_config_duplicate_system_user(
1041        #[files("signstar-config-*.toml")]
1042        #[base_dir = "tests/fixtures/duplicate-system-user/"]
1043        config_file: PathBuf,
1044    ) -> TestResult {
1045        println!("{config_file:?}");
1046        match SignstarConfig::new_from_file(Some(&config_file)) {
1047            Err(crate::Error::Config(Error::DuplicateSystemUserId { .. })) => Ok(()),
1048            Ok(_) => panic!("Did not trigger any Error!"),
1049            Err(error) => panic!("Did not trigger the correct Error: {:?}!", error),
1050        }
1051    }
1052
1053    #[rstest]
1054    fn signstar_config_duplicate_nethsm_user(
1055        #[files("signstar-config-*.toml")]
1056        #[base_dir = "tests/fixtures/duplicate-nethsm-user/"]
1057        config_file: PathBuf,
1058    ) -> TestResult {
1059        if let Err(crate::Error::Config(Error::DuplicateNetHsmUserId { .. })) =
1060            SignstarConfig::new_from_file(Some(&config_file))
1061        {
1062            Ok(())
1063        } else {
1064            panic!("Did not trigger the correct Error!")
1065        }
1066    }
1067
1068    #[rstest]
1069    fn signstar_config_missing_administrator(
1070        #[files("signstar-config-*.toml")]
1071        #[base_dir = "tests/fixtures/missing-administrator/"]
1072        config_file: PathBuf,
1073    ) -> TestResult {
1074        if let Err(crate::Error::Config(Error::MissingAdministrator { .. })) =
1075            SignstarConfig::new_from_file(Some(&config_file))
1076        {
1077            Ok(())
1078        } else {
1079            panic!("Did not trigger the correct Error!")
1080        }
1081    }
1082
1083    #[rstest]
1084    fn signstar_config_missing_namespace_administrators(
1085        #[files("signstar-config-*.toml")]
1086        #[base_dir = "tests/fixtures/missing-namespace-administrator/"]
1087        config_file: PathBuf,
1088    ) -> TestResult {
1089        if let Err(crate::Error::Config(Error::MissingAdministrator { .. })) =
1090            SignstarConfig::new_from_file(Some(&config_file))
1091        {
1092            Ok(())
1093        } else {
1094            panic!("Did not trigger the correct Error!")
1095        }
1096    }
1097
1098    #[rstest]
1099    fn signstar_config_duplicate_authorized_keys_share_uploader(
1100        #[files("signstar-config-*.toml")]
1101        #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-uploader/"]
1102        config_file: PathBuf,
1103    ) -> TestResult {
1104        println!("Using configuration {config_file:?}");
1105        let config_file_string = config_file
1106            .clone()
1107            .into_os_string()
1108            .into_string()
1109            .map_err(|e| format!("Can't convert {config_file:?}:\n{e:?}"))?;
1110        // when using plaintext or systemd-creds for administrative credentials, there are no share
1111        // uploaders
1112        if config_file_string.ends_with("ntext.toml")
1113            || config_file_string.ends_with("emd-creds.toml")
1114        {
1115            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1116            Ok(())
1117        } else if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1118            SignstarConfig::new_from_file(Some(&config_file))
1119        {
1120            Ok(())
1121        } else {
1122            panic!("Did not trigger the correct Error!")
1123        }
1124    }
1125
1126    #[rstest]
1127    fn signstar_config_duplicate_authorized_keys_share_downloader(
1128        #[files("signstar-config-*.toml")]
1129        #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-downloader/"]
1130        config_file: PathBuf,
1131    ) -> TestResult {
1132        println!("Using configuration {config_file:?}");
1133        let config_file_string = config_file
1134            .clone()
1135            .into_os_string()
1136            .into_string()
1137            .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1138        // when using plaintext or systemd-creds for administrative credentials, there are no share
1139        // downloaders
1140        if config_file_string.ends_with("ntext.toml")
1141            || config_file_string.ends_with("systemd-creds.toml")
1142        {
1143            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1144            Ok(())
1145        } else if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1146            SignstarConfig::new_from_file(Some(&config_file))
1147        {
1148            Ok(())
1149        } else {
1150            panic!("Did not trigger the correct Error!")
1151        }
1152    }
1153
1154    #[rstest]
1155    fn signstar_config_duplicate_authorized_keys_users(
1156        #[files("signstar-config-*.toml")]
1157        #[base_dir = "tests/fixtures/duplicate-authorized-keys-users/"]
1158        config_file: PathBuf,
1159    ) -> TestResult {
1160        if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1161            SignstarConfig::new_from_file(Some(&config_file))
1162        {
1163            Ok(())
1164        } else {
1165            panic!("Did not trigger the correct Error!")
1166        }
1167    }
1168
1169    #[rstest]
1170    fn signstar_config_missing_share_download_user(
1171        #[files("signstar-config-*.toml")]
1172        #[base_dir = "tests/fixtures/missing-share-download-user/"]
1173        config_file: PathBuf,
1174    ) -> TestResult {
1175        println!("Using configuration {config_file:?}");
1176        let config_file_string = config_file
1177            .clone()
1178            .into_os_string()
1179            .into_string()
1180            .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1181        // when using plaintext or systemd-creds for administrative credentials, there are no share
1182        // downloaders
1183        if config_file_string.ends_with("plaintext.toml")
1184            || config_file_string.ends_with("systemd-creds.toml")
1185        {
1186            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1187            Ok(())
1188        } else if let Err(crate::Error::Config(Error::MissingShareDownloadSystemUser)) =
1189            SignstarConfig::new_from_file(Some(&config_file))
1190        {
1191            Ok(())
1192        } else {
1193            panic!("Did not trigger the correct Error!")
1194        }
1195    }
1196
1197    #[rstest]
1198    fn signstar_config_missing_share_upload_user(
1199        #[files("signstar-config-*.toml")]
1200        #[base_dir = "tests/fixtures/missing-share-upload-user/"]
1201        config_file: PathBuf,
1202    ) -> TestResult {
1203        println!("Using configuration {config_file:?}");
1204        let config_file_string = config_file
1205            .clone()
1206            .into_os_string()
1207            .into_string()
1208            .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1209        // when using plaintext or systemd-creds for administrative credentials, there are no share
1210        // downloaders
1211        if config_file_string.ends_with("plaintext.toml")
1212            || config_file_string.ends_with("systemd-creds.toml")
1213        {
1214            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1215            Ok(())
1216        } else if let Err(crate::Error::Config(Error::MissingShareUploadSystemUser)) =
1217            SignstarConfig::new_from_file(Some(&config_file))
1218        {
1219            Ok(())
1220        } else {
1221            panic!("Did not trigger the correct Error!")
1222        }
1223    }
1224
1225    #[rstest]
1226    fn signstar_config_no_sss_but_shares(
1227        #[files("signstar-config-*.toml")]
1228        #[base_dir = "tests/fixtures/no-sss-but-shares/"]
1229        config_file: PathBuf,
1230    ) -> TestResult {
1231        println!("Using configuration {config_file:?}");
1232        let config_file_string = config_file
1233            .clone()
1234            .into_os_string()
1235            .into_string()
1236            .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1237        // when using shamir's secret sharing for administrative credentials, there ought to be
1238        // share downloaders and uploaders
1239        if config_file_string.ends_with("irs-secret-sharing.toml") {
1240            let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1241            Ok(())
1242        } else if let Err(crate::Error::Config(Error::NoSssButShareUsers { .. })) =
1243            SignstarConfig::new_from_file(Some(&config_file))
1244        {
1245            Ok(())
1246        } else {
1247            panic!("Did not trigger the correct Error!")
1248        }
1249    }
1250
1251    #[rstest]
1252    fn signstar_config_duplicate_key_id(
1253        #[files("signstar-config-*.toml")]
1254        #[base_dir = "tests/fixtures/duplicate-key-id/"]
1255        config_file: PathBuf,
1256    ) -> TestResult {
1257        if let Err(crate::Error::Config(Error::DuplicateKeyId { .. })) =
1258            SignstarConfig::new_from_file(Some(&config_file))
1259        {
1260            Ok(())
1261        } else {
1262            panic!("Did not trigger the correct Error!")
1263        }
1264    }
1265
1266    #[rstest]
1267    fn signstar_config_duplicate_key_id_in_namespace(
1268        #[files("signstar-config-*.toml")]
1269        #[base_dir = "tests/fixtures/duplicate-key-id-in-namespace/"]
1270        config_file: PathBuf,
1271    ) -> TestResult {
1272        if let Err(crate::Error::Config(Error::DuplicateKeyId { .. })) =
1273            SignstarConfig::new_from_file(Some(&config_file))
1274        {
1275            Ok(())
1276        } else {
1277            panic!("Did not trigger the correct Error!")
1278        }
1279    }
1280
1281    #[rstest]
1282    fn signstar_config_duplicate_tag(
1283        #[files("signstar-config-*.toml")]
1284        #[base_dir = "tests/fixtures/duplicate-tag/"]
1285        config_file: PathBuf,
1286    ) -> TestResult {
1287        if let Err(crate::Error::Config(Error::DuplicateTag { .. })) =
1288            SignstarConfig::new_from_file(Some(&config_file))
1289        {
1290            Ok(())
1291        } else {
1292            panic!("Did not trigger the correct Error!")
1293        }
1294    }
1295
1296    #[rstest]
1297    fn signstar_config_duplicate_tag_in_namespace(
1298        #[files("signstar-config-*.toml")]
1299        #[base_dir = "tests/fixtures/duplicate-tag-in-namespace/"]
1300        config_file: PathBuf,
1301    ) -> TestResult {
1302        if let Err(crate::Error::Config(Error::DuplicateTag { .. })) =
1303            SignstarConfig::new_from_file(Some(&config_file))
1304        {
1305            Ok(())
1306        } else {
1307            panic!("Did not trigger the correct Error!")
1308        }
1309    }
1310
1311    #[rstest]
1312    #[case("ssh-backup1")]
1313    #[case("ssh-metrics1")]
1314    #[case("ssh-operator1")]
1315    #[case("ssh-operator2")]
1316    #[case("ns1-ssh-operator1")]
1317    #[case("ns1-ssh-operator2")]
1318    #[case("local-metrics1")]
1319    #[case("ssh-wireguard-down")]
1320    fn signstar_config_get_extended_usermapping_succeeds(
1321        #[files("signstar-config-*.toml")]
1322        #[base_dir = "tests/fixtures/working/"]
1323        config_file: PathBuf,
1324        #[case] name: &str,
1325    ) -> TestResult {
1326        let config = SignstarConfig::new_from_file(Some(&config_file))?;
1327        if config.get_extended_mapping_for_user(name).is_none() {
1328            panic!("The user with name {name} is supposed to exist in the Signstar config");
1329        }
1330
1331        Ok(())
1332    }
1333
1334    #[rstest]
1335    fn signstar_config_get_extended_usermapping_fails(
1336        #[files("signstar-config-*.toml")]
1337        #[base_dir = "tests/fixtures/working/"]
1338        config_file: PathBuf,
1339    ) -> TestResult {
1340        let config = SignstarConfig::new_from_file(Some(&config_file))?;
1341        if config.get_extended_mapping_for_user("foo").is_some() {
1342            panic!("The user \"foo\" should not exist in the Signstar config");
1343        }
1344
1345        Ok(())
1346    }
1347}