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