Skip to main content

signstar_config/config/file/
mod.rs

1//! Configuration file handling.
2
3#[cfg(all(feature = "nethsm", feature = "yubihsm2"))]
4pub mod impl_all;
5#[cfg(all(feature = "nethsm", not(feature = "yubihsm2")))]
6pub mod impl_nethsm;
7#[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
8pub mod impl_none;
9#[cfg(all(feature = "yubihsm2", not(feature = "nethsm")))]
10pub mod impl_yubihsm2;
11
12#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
13use std::collections::BTreeSet;
14use std::{
15    collections::HashSet,
16    fs::read_to_string,
17    path::{Path, PathBuf},
18    str::FromStr,
19};
20
21use garde::Validate;
22use log::info;
23#[cfg(feature = "nethsm")]
24use nethsm::Connection;
25use serde::{Deserialize, Serialize};
26#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
27use signstar_crypto::{AdministrativeSecretHandling, NonAdministrativeSecretHandling};
28#[cfg(feature = "yubihsm2")]
29use signstar_yubihsm2::Connection as YubiHsm2Connection;
30use strum::{AsRefStr, VariantNames};
31
32#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
33use crate::config::{ConfigAuthorizedKeyEntries, ConfigSystemUserIds};
34#[cfg(feature = "nethsm")]
35use crate::nethsm::{NetHsmConfig, NetHsmUserMapping};
36#[cfg(feature = "yubihsm2")]
37use crate::yubihsm2::{YubiHsm2Config, YubiHsm2UserMapping};
38use crate::{
39    config::{ConfigSystemUserData, Error, SystemConfig, SystemUserData},
40    state::{StateOrigin, StateOriginInfo},
41};
42
43/// Backend specific data for a user mapping.
44#[derive(Clone, Debug, Eq, PartialEq)]
45pub enum UserBackendConnection {
46    /// The connection configuration for a user of a NetHSM backend.
47    ///
48    /// # Note
49    ///
50    /// Only supported when using the `nethsm` feature.
51    #[cfg(feature = "nethsm")]
52    NetHsm {
53        /// Administrative credentials handling.
54        admin_secret_handling: AdministrativeSecretHandling,
55
56        /// Non-administrative credentials handling.
57        non_admin_secret_handling: NonAdministrativeSecretHandling,
58
59        /// The available connections to the NetHSM backend.
60        connections: BTreeSet<Connection>,
61
62        /// A specific NetHSM user mapping.
63        mapping: NetHsmUserMapping,
64    },
65
66    /// The connection configuration for a user of a YubiHSM2 backend.
67    ///
68    /// # Note
69    ///
70    /// Only supported when using the `yubihsm2` feature.
71    #[cfg(feature = "yubihsm2")]
72    YubiHsm2 {
73        /// Administrative credentials handling.
74        admin_secret_handling: AdministrativeSecretHandling,
75
76        /// Non-administrative credentials handling.
77        non_admin_secret_handling: NonAdministrativeSecretHandling,
78
79        /// The available connections to the YubiHSM2 backend.
80        connections: BTreeSet<YubiHsm2Connection>,
81
82        /// A specific YubiHSM2 user mapping.
83        mapping: YubiHsm2UserMapping,
84    },
85}
86
87/// A filter for the retrieval of lists of [`UserBackendConnection`] from a [`Config`].
88#[derive(Clone, Copy, Debug)]
89pub enum UserBackendConnectionFilter {
90    /// Target all backend users.
91    All,
92
93    /// Only target administrative backend users.
94    Admin,
95
96    /// Only target non-administrative backend users.
97    NonAdmin,
98}
99
100/// Validates overlapping assumptions of two configuration objects.
101///
102/// Ensures that `config_a` and `config_b` have no overlapping system user IDs or SSH
103/// authorized_keys.
104///
105/// # Errors
106///
107/// Returns an error if there are
108///
109/// - duplicate system users
110/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
111#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
112fn validate_confs<T, U>(config_a: &T, config_b: &U) -> garde::Result
113where
114    T: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
115    U: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
116{
117    // Collect duplicate system user IDs.
118    let duplicate_system_user_ids = {
119        let system_config_user_ids = config_a.system_user_ids();
120        let config_user_ids = config_b.system_user_ids();
121        let duplicates = system_config_user_ids
122            .intersection(&config_user_ids)
123            .map(|system_user_id| system_user_id.to_string())
124            .collect::<HashSet<_>>();
125
126        if duplicates.is_empty() {
127            None
128        } else {
129            let mut duplicates = Vec::from_iter(duplicates);
130            duplicates.sort();
131            Some(format!(
132                "the duplicate system user ID{} {}",
133                if duplicates.len() > 1 { "s" } else { "" },
134                duplicates.join(", ")
135            ))
136        }
137    };
138
139    // Collect all duplicate SSH public keys in authorized_keys.
140    let duplicate_public_keys = {
141        let system_config_public_keys: HashSet<_> = config_a
142            .authorized_key_entries()
143            .iter()
144            .cloned()
145            .map(|authorized_key| authorized_key.as_ref().public_key())
146            .collect();
147        let config_public_keys: HashSet<_> = config_b
148            .authorized_key_entries()
149            .iter()
150            .cloned()
151            .map(|authorized_key| authorized_key.as_ref().public_key())
152            .collect();
153        let duplicates: HashSet<_> = system_config_public_keys
154            .intersection(&config_public_keys)
155            .cloned()
156            .map(|public_key| {
157                let mut public_key = public_key.clone();
158                // Unset the comment as it may be set to different values.
159                public_key.set_comment("");
160                format!("\"{}\"", public_key.to_string())
161            })
162            .collect();
163
164        if duplicates.is_empty() {
165            None
166        } else {
167            let mut duplicates = Vec::from_iter(duplicates);
168            duplicates.sort();
169            Some(format!(
170                "the duplicate SSH public key{} {}",
171                if duplicates.len() > 1 { "s" } else { "" },
172                duplicates.join(", ")
173            ))
174        }
175    };
176
177    let messages = [duplicate_system_user_ids, duplicate_public_keys];
178    let error_messages = {
179        let mut error_messages = Vec::new();
180
181        for message in messages.iter().flatten() {
182            error_messages.push(message.as_str());
183        }
184
185        error_messages
186    };
187
188    match error_messages.len() {
189        0 => Ok(()),
190        1 => Err(garde::Error::new(format!(
191            "contains {}",
192            error_messages.join("\n")
193        ))),
194        _ => Err(garde::Error::new(format!(
195            "contains multiple issues:\n⤷ {}",
196            error_messages.join("\n⤷ ")
197        ))),
198    }
199}
200
201/// Validates a required config object against an optional one.
202///
203/// Ensures that the the two configuration objects have no overlapping system user IDs or SSH
204/// authorized_keys.
205///
206/// # Errors
207///
208/// Returns an error if there are
209///
210/// - duplicate system users
211/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
212#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
213fn validate_config_against_optional_config<T, U>(
214    config_a: &Option<T>,
215) -> impl FnOnce(&U, &()) -> garde::Result + '_
216where
217    T: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
218    U: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
219{
220    move |config_b, _| {
221        let Some(config_a) = config_a else {
222            return Ok(());
223        };
224
225        validate_confs(config_a, config_b)
226    }
227}
228
229/// Validates two optional config objects against each other.
230///
231/// Ensures that - if both config objects are present - they have no overlapping system user IDs or
232/// SSH authorized_keys.
233///
234/// # Errors
235///
236/// Returns an error if there are
237///
238/// - duplicate system users
239/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
240#[cfg(all(feature = "nethsm", feature = "yubihsm2"))]
241fn validate_two_optional_configs<T, U>(
242    backend_config_a: &Option<T>,
243) -> impl FnOnce(&Option<U>, &()) -> garde::Result + '_
244where
245    T: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
246    U: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
247{
248    move |backend_config_b, _| {
249        if let Some(backend_config_a) = backend_config_a
250            && let Some(backend_config_b) = backend_config_b
251        {
252            validate_confs(backend_config_a, backend_config_b)?;
253        }
254
255        Ok(())
256    }
257}
258
259/// The supported configuration file formats.
260#[derive(AsRefStr, Clone, Copy, Debug, Default, strum::Display, VariantNames)]
261#[strum(serialize_all = "lowercase")]
262enum ConfigFileFormat {
263    #[default]
264    Yaml,
265}
266
267/// The configuration of a Signstar system.
268///
269/// Tracks system-wide configuration items, as well as configurations for specific backends.
270#[derive(Clone, Debug, Default, Deserialize, Serialize, Validate)]
271#[serde(rename_all = "snake_case")]
272pub struct Config {
273    /// System configuration object.
274    // Validate against NetHsmConfig if support is compiled in.
275    #[cfg_attr(
276        feature = "nethsm",
277        garde(custom(validate_config_against_optional_config(&self.nethsm)))
278    )]
279    // Validate against YubiHsm2Config if support is compiled in.
280    #[cfg_attr(
281        feature = "yubihsm2",
282        garde(custom(validate_config_against_optional_config(&self.yubihsm2)))
283    )]
284    #[garde(dive)]
285    system: SystemConfig,
286
287    /// Optional configuration object for NetHSM backends.
288    ///
289    /// # Note
290    ///
291    /// Only supported when using the `nethsm` feature.
292    #[cfg(feature = "nethsm")]
293    // Validate against YubiHsm2Config if support is compiled in.
294    #[cfg_attr(
295        all(feature = "nethsm", feature = "yubihsm2"),
296        garde(custom(validate_two_optional_configs(&self.yubihsm2)))
297    )]
298    #[garde(dive)]
299    #[serde(skip_serializing_if = "Option::is_none")]
300    nethsm: Option<NetHsmConfig>,
301
302    /// Optional configuration object for YubiHSM2 backends.
303    ///
304    /// # Note
305    ///
306    /// Only supported when using the `yubihsm2` feature.
307    #[cfg(feature = "yubihsm2")]
308    // Validate against NetHsmConfig if support is compiled in.
309    #[cfg_attr(
310        all(feature = "nethsm", feature = "yubihsm2"),
311        garde(custom(validate_two_optional_configs(&self.nethsm)))
312    )]
313    #[garde(dive)]
314    #[serde(skip_serializing_if = "Option::is_none")]
315    yubihsm2: Option<YubiHsm2Config>,
316}
317
318impl Config {
319    /// The default config directory below "/usr/".
320    pub const DEFAULT_CONFIG_DIR: &str = "/usr/share/signstar/";
321
322    /// The override config directory below "/run/".
323    pub const RUN_OVERRIDE_CONFIG_DIR: &str = "/run/signstar/";
324
325    /// The override config directory below "/etc/".
326    pub const ETC_OVERRIDE_CONFIG_DIR: &str = "/etc/signstar/";
327
328    /// The configuration file name (without file type suffix).
329    pub const CONFIG_NAME: &str = "config";
330
331    /// Returns the default location of the Signstar configuration file on a system.
332    pub fn default_system_path() -> PathBuf {
333        PathBuf::from(Self::DEFAULT_CONFIG_DIR).join(PathBuf::from(format!(
334            "{}.{}",
335            Self::CONFIG_NAME,
336            ConfigFileFormat::default()
337        )))
338    }
339
340    /// Returns the first found path of a Signstar configuratino on the system.
341    ///
342    /// # Errors
343    ///
344    /// Returns an error if no configuration file is found.
345    pub fn first_existing_system_path() -> Result<PathBuf, crate::Error> {
346        let path = Self::list_config_file_paths()
347            .into_iter()
348            .find(|path| path.is_file());
349        path.ok_or(Error::ConfigIsMissing.into())
350    }
351
352    /// Returns the list of supported directory paths in which configuration files may reside.
353    ///
354    /// The returned list of paths is sorted in increasing precedence.
355    pub fn list_config_dirs() -> Vec<PathBuf> {
356        [
357            Self::DEFAULT_CONFIG_DIR,
358            Self::RUN_OVERRIDE_CONFIG_DIR,
359            Self::ETC_OVERRIDE_CONFIG_DIR,
360        ]
361        .iter()
362        .map(PathBuf::from)
363        .collect()
364    }
365
366    /// Returns the list of supported configuration file paths.
367    ///
368    /// The returned list of paths is sorted in increasing precedence.
369    pub fn list_config_file_paths() -> Vec<PathBuf> {
370        Self::list_config_dirs()
371            .into_iter()
372            .map(|dir| {
373                dir.join(
374                    PathBuf::from(Self::CONFIG_NAME)
375                        .with_added_extension(ConfigFileFormat::default().as_ref()),
376                )
377            })
378            .collect()
379    }
380
381    /// Creates a new [`Config`] from a string slice containing YAML data.
382    ///
383    /// # Errors
384    ///
385    /// Returns an error if deserialization or validation fails.
386    fn from_yaml_str(s: &str) -> Result<Self, crate::Error> {
387        let config: Self = serde_saphyr::from_str(s).map_err(|source| Error::YamlDeserialize {
388            context: "creating a Signstar configuration object".to_string(),
389            source,
390        })?;
391
392        config
393            .validate()
394            .map_err(|source| crate::Error::Validation {
395                context: "validating a Signstar configuration object".to_string(),
396                source,
397            })?;
398
399        Ok(config)
400    }
401
402    /// Creates a new [`Config`] from a file containing YAML data.
403    ///
404    /// # Errors
405    ///
406    /// Returns an error if
407    /// - the file does not exist
408    /// - deserialization or validation fails.
409    fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self, crate::Error> {
410        let path = path.as_ref();
411        info!("Reading Signstar configuration file {path:?}");
412
413        let config_data = read_to_string(path).map_err(|source| crate::Error::IoPath {
414            path: path.to_path_buf(),
415            context: "reading it to string",
416            source,
417        })?;
418        Self::from_yaml_str(&config_data)
419    }
420
421    /// Creates a new [`Config`] from a file `path`.
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if
426    ///
427    /// - `path` has no file extension
428    /// - `path` does not use one of the supported file extensions
429    /// - creating a [`Config`] from the data fails
430    /// - validating a [`Config`] created from the data fails
431    pub fn from_file_path(path: impl AsRef<Path>) -> Result<Self, crate::Error> {
432        let path = path.as_ref();
433        let extension = {
434            let Some(extension) = path.extension() else {
435                return Err(Error::MissingFileExtension {
436                    path: path.to_path_buf(),
437                }
438                .into());
439            };
440            extension.to_string_lossy().to_string()
441        };
442
443        if !ConfigFileFormat::VARIANTS.contains(&extension.as_ref()) {
444            return Err(Error::UnsupportedFileExtension {
445                path: path.to_path_buf(),
446                extension,
447            }
448            .into());
449        }
450
451        Self::from_yaml_file(path)
452    }
453
454    /// Creates a new [`Config`] from the first found Signstar configuration file path on the
455    /// system.
456    ///
457    /// # Note
458    ///
459    /// Uses [`Config::first_existing_system_path`] to determine the first existing Signstar
460    /// configuration file path.
461    ///
462    /// # Errors
463    ///
464    /// Returns an error if [`Config`] creation from the found path fails.
465    pub fn from_system_path() -> Result<Self, crate::Error> {
466        Self::from_yaml_file(Self::first_existing_system_path()?)
467    }
468
469    /// Serializes `self` as a YAML string.
470    ///
471    /// # Errors
472    ///
473    /// Returns an error if serialization fails.
474    pub fn to_yaml_string(&self) -> Result<String, crate::Error> {
475        serde_saphyr::to_string(&self).map_err(|source| {
476            Error::YamlSerialize {
477                context: "serializing Signstar config",
478                source,
479            }
480            .into()
481        })
482    }
483
484    /// Returns a reference to the [`SystemConfig`].
485    pub fn system(&self) -> &SystemConfig {
486        &self.system
487    }
488
489    /// Returns a reference to the [`NetHsmConfig`].
490    #[cfg(feature = "nethsm")]
491    pub fn nethsm(&self) -> Option<&NetHsmConfig> {
492        self.nethsm.as_ref()
493    }
494
495    /// Returns a reference to the [`YubiHsm2Config`].
496    #[cfg(feature = "yubihsm2")]
497    pub fn yubihsm2(&self) -> Option<&YubiHsm2Config> {
498        self.yubihsm2.as_ref()
499    }
500}
501
502impl FromStr for Config {
503    type Err = crate::Error;
504
505    /// Creates a new [`Config`] from a string slice containing valid YAML.
506    ///
507    /// # Errors
508    ///
509    /// Returns an error if no [`Config`] can be created from `s`.
510    fn from_str(s: &str) -> Result<Self, Self::Err> {
511        Config::from_yaml_str(s)
512    }
513}
514
515/// A builder for [`Config`].
516#[derive(Clone, Debug)]
517pub struct ConfigBuilder(Config);
518
519impl ConfigBuilder {
520    /// Adds a [`NetHsmConfig`] to the builder.
521    #[cfg(feature = "nethsm")]
522    pub fn set_nethsm_config(mut self, nethsm: NetHsmConfig) -> Self {
523        self.0.nethsm = Some(nethsm);
524        self
525    }
526
527    /// Adds a [`YubiHsm2Config`] to the builder.
528    #[cfg(feature = "yubihsm2")]
529    pub fn set_yubihsm2_config(mut self, yubihsm2: YubiHsm2Config) -> Self {
530        self.0.yubihsm2 = Some(yubihsm2);
531        self
532    }
533
534    /// Creates a [`Config`] from the builder.
535    ///
536    /// # Errors
537    ///
538    /// Returns an error if validation for the [`Config`] fails.
539    pub fn finish(self) -> Result<Config, crate::Error> {
540        self.0
541            .validate()
542            .map_err(|source| crate::Error::Validation {
543                context: "validating a configuration object".to_string(),
544                source,
545            })?;
546
547        Ok(self.0)
548    }
549}
550
551/// The state of system users according to a Signstar configuration.
552#[derive(Clone, Debug, Eq, PartialEq)]
553pub struct SystemUserConfigState<'a> {
554    pub(crate) system_user_data: HashSet<SystemUserData<'a>>,
555}
556
557impl<'a> SystemUserConfigState<'a> {
558    /// The name of the origin for the state.
559    pub const STATE_NAME: &'static str = "config";
560}
561
562impl<'a> From<&'a Config> for SystemUserConfigState<'a> {
563    fn from(value: &'a Config) -> Self {
564        Self {
565            system_user_data: value.system_user_data(),
566        }
567    }
568}
569
570impl<'a> StateOriginInfo for SystemUserConfigState<'a> {
571    fn state_name(&self) -> &str {
572        Self::STATE_NAME
573    }
574
575    fn state_origin(&self) -> StateOrigin {
576        StateOrigin::Config
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use std::{collections::BTreeSet, num::NonZeroUsize, thread::current};
583
584    use insta::{assert_snapshot, with_settings};
585    #[cfg(feature = "nethsm")]
586    use nethsm::ConnectionSecurity;
587    use pretty_assertions::assert_eq;
588    use rstest::{fixture, rstest};
589    use signstar_crypto::{AdministrativeSecretHandling, NonAdministrativeSecretHandling};
590    #[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
591    use signstar_crypto::{
592        key::{CryptographicKeyContext, KeyMechanism, KeyType, SignatureType, SigningKeySetup},
593        openpgp::OpenPgpUserIdList,
594    };
595    #[cfg(feature = "yubihsm2")]
596    use signstar_yubihsm2::object::Domain;
597    use tempfile::{NamedTempFile, TempDir};
598    use testresult::TestResult;
599
600    use super::*;
601    use crate::config::{AuthorizedKeyEntry, SystemUserId, SystemUserMapping};
602    #[cfg(feature = "nethsm")]
603    use crate::nethsm::NetHsmMetricsUsers;
604
605    const SNAPSHOT_PATH: &str = "fixtures/file/";
606
607    /// Creates a default [`SystemConfig`] for testing purposes.
608    #[fixture]
609    fn default_system_config() -> TestResult<SystemConfig> {
610        Ok(SystemConfig::new(
611            1,
612            AdministrativeSecretHandling::ShamirsSecretSharing {
613                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
614                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
615            },
616            NonAdministrativeSecretHandling::SystemdCreds,
617            BTreeSet::from_iter([
618                SystemUserMapping::ShareHolder {
619                    system_user: "share-holder1".parse()?,
620                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
621                },
622                SystemUserMapping::ShareHolder {
623                    system_user: "share-holder2".parse()?,
624                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
625                },
626                SystemUserMapping::ShareHolder {
627                    system_user: "share-holder3".parse()?,
628                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
629                },
630                SystemUserMapping::WireGuardDownload {
631                    system_user: "wireguard-downloader".parse()?,
632                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
633                },
634            ]),
635        )?)
636    }
637
638    /// List of raw data required to create [`SystemUserData`] for each item in the default
639    /// [`SystemConfig`].
640    #[fixture]
641    fn raw_user_data_system() -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
642        Ok(vec![
643                (
644                    "share-holder1".parse()?,
645                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?),
646                ),
647                (
648                    "share-holder2".parse()?,
649                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?),
650                ),
651                (
652                    "share-holder3".parse()?,
653                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?),
654                ),
655                (
656                    "wireguard-downloader".parse()?,
657                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?),
658                ),
659            ])
660    }
661
662    /// Creates a default [`NetHsmConfig`] for testing purposes.
663    #[cfg(feature = "nethsm")]
664    #[fixture]
665    fn default_nethsm_config() -> TestResult<NetHsmConfig> {
666        Ok(NetHsmConfig::new(
667            BTreeSet::from_iter([
668                Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
669                Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
670            ]),
671            BTreeSet::from_iter([
672                NetHsmUserMapping::Admin("admin".parse()?),
673                NetHsmUserMapping::Backup{
674                    backend_user: "backup".parse()?,
675                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
676                    system_user: "nethsm-backup-user".parse()?,
677                },
678                NetHsmUserMapping::HermeticMetrics {
679                    backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
680                    system_user: "nethsm-hermetic-metrics-user".parse()?,
681                },
682                NetHsmUserMapping::Metrics {
683                    backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
684                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
685                    system_user: "nethsm-metrics-user".parse()?,
686                },
687                NetHsmUserMapping::Signing {
688                    backend_user: "signing".parse()?,
689                    signing_key_id: "signing1".parse()?,
690                    key_setup: SigningKeySetup::new(
691                        KeyType::Curve25519,
692                        vec![KeyMechanism::EdDsaSignature],
693                        None,
694                        SignatureType::EdDsa,
695                        CryptographicKeyContext::OpenPgp {
696                            user_ids: OpenPgpUserIdList::new(vec![
697                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
698                            ])?,
699                            version: "v4".parse()?,
700                        },
701                    )?,
702                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
703                    system_user: "nethsm-signing-user".parse()?,
704                    tag: "signing1".to_string(),
705                }
706            ]),
707        )?)
708    }
709
710    /// List of raw data required to create [`SystemUserData`] for each item in the default
711    /// [`NetHsmConfig`].
712    #[cfg(feature = "nethsm")]
713    #[fixture]
714    fn raw_user_data_nethsm() -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
715        Ok(vec![
716                (
717                    SystemUserId::root(),
718                    None,
719                ),
720                (
721                    "nethsm-backup-user".parse()?,
722                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?),
723                ),
724                (
725                    "nethsm-hermetic-metrics-user".parse()?,
726                    None,
727                ),
728                (
729                    "nethsm-metrics-user".parse()?,
730                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?),
731                ),
732                (
733                    "nethsm-signing-user".parse()?,
734                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?),
735                ),
736            ])
737    }
738
739    /// Creates a default [`YubiHsm2Config`] for testing purposes.
740    #[cfg(feature = "yubihsm2")]
741    #[fixture]
742    fn default_yubihsm2_config() -> TestResult<YubiHsm2Config> {
743        Ok(YubiHsm2Config::new(
744            BTreeSet::from_iter([
745                YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
746                YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
747            ]),
748            BTreeSet::from_iter([
749                YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
750                YubiHsm2UserMapping::AuditLog {
751                    authentication_key_id: "3".parse()?,
752                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
753                    system_user: "yubihsm2-metrics-user".parse()?,
754                },
755                YubiHsm2UserMapping::Backup{
756                    authentication_key_id: "2".parse()?,
757                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
758                    system_user: "yubihsm2-backup-user".parse()?,
759                    wrapping_key_id: "1".parse()?,
760                },
761                YubiHsm2UserMapping::HermeticAuditLog {
762                    authentication_key_id: "4".parse()?,
763                    system_user: "yubihsm2-hermetic-metrics-user".parse()?,
764                },
765                YubiHsm2UserMapping::Signing {
766                    authentication_key_id: "5".parse()?,
767                    signing_key_id: "1".parse()?,
768                    key_setup: SigningKeySetup::new(
769                        KeyType::Curve25519,
770                        vec![KeyMechanism::EdDsaSignature],
771                        None,
772                        SignatureType::EdDsa,
773                        CryptographicKeyContext::OpenPgp {
774                            user_ids: OpenPgpUserIdList::new(vec![
775                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
776                            ])?,
777                            version: "v4".parse()?,
778                        },
779                    )?,
780                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
781                    system_user: "yubihsm2-signing-user".parse()?,
782                    domain: Domain::One,
783                }
784            ]),
785        )?)
786    }
787
788    /// List of raw data required to create [`SystemUserData`] for each item in the default
789    /// [`YubiHsm2Config`].
790    #[cfg(feature = "yubihsm2")]
791    #[fixture]
792    fn raw_user_data_yubihsm2() -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
793        Ok(vec![
794                (
795                    SystemUserId::root(),
796                    None,
797                ),
798                (
799                    "yubihsm2-metrics-user".parse()?,
800                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?),
801                ),
802                (
803                    "yubihsm2-backup-user".parse()?,
804                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?),
805                ),
806                (
807                    "yubihsm2-hermetic-metrics-user".parse()?,
808                    None,
809                ),
810                (
811                    "yubihsm2-signing-user".parse()?,
812                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?),
813                ),
814            ])
815    }
816
817    /// Ensures, that [`Config::default_system_path`] always returns the same path.
818    #[test]
819    fn config_default_system_path() {
820        assert_eq!(
821            Config::default_system_path(),
822            PathBuf::from("/usr/share/signstar/config.yaml")
823        )
824    }
825
826    /// Ensures, that [`Config::list_config_file_paths`] always returns the same list of paths.
827    #[test]
828    fn config_list_config_file_paths() {
829        assert_eq!(
830            Config::list_config_file_paths(),
831            vec![
832                PathBuf::from("/usr/share/signstar/config.yaml"),
833                PathBuf::from("/run/signstar/config.yaml"),
834                PathBuf::from("/etc/signstar/config.yaml"),
835            ]
836        )
837    }
838
839    /// Ensures, that [`Config::from_file_path`] fails on missing file extensions.
840    #[rstest]
841    fn config_from_file_path_fails_on_missing_file_extension() -> TestResult {
842        let temp_dir = TempDir::new()?;
843
844        match Config::from_file_path(temp_dir.path().join("config")) {
845            Ok(config) => panic!(
846                "Should have failed to create a Config object, but succeeded instead: {config:?}"
847            ),
848            Err(crate::Error::Config(Error::MissingFileExtension { .. })) => {}
849            Err(error) => panic!(
850                "Should have failed with a ConfigError::MissingFileExtension, but failed with a different error instead: {error}"
851            ),
852        }
853
854        Ok(())
855    }
856
857    /// Ensures, that [`Config::from_file_path`] fails on unsupported file extensions.
858    #[rstest]
859    fn config_from_file_path_fails_on_unsupported_file_extension() -> TestResult {
860        let temp_file = NamedTempFile::with_suffix(".toml")?;
861
862        match Config::from_file_path(temp_file.path()) {
863            Ok(config) => panic!(
864                "Should have failed to create a Config object, but succeeded instead: {config:?}"
865            ),
866            Err(crate::Error::Config(Error::UnsupportedFileExtension { .. })) => {}
867            Err(error) => panic!(
868                "Should have failed with a ConfigError::UnsupportedFileExtension, but failed with a different error instead: {error}"
869            ),
870        }
871
872        Ok(())
873    }
874
875    /// Tests, that are only available when using no backend.
876    #[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
877    mod no_backend {
878        use std::collections::HashSet;
879
880        use pretty_assertions::assert_eq;
881
882        use super::*;
883        use crate::config::{
884            ConfigAuthorizedKeyEntries,
885            ConfigSystemUserIds,
886            SystemUserData,
887            traits::ConfigSystemUserData,
888        };
889
890        /// Creates a default [`Config`] for testing purposes.
891        #[fixture]
892        fn default_config(default_system_config: TestResult<SystemConfig>) -> TestResult<Config> {
893            Ok(ConfigBuilder::new(default_system_config?).finish()?)
894        }
895
896        /// Create a [`Config`] using [`ConfigBuilder`].
897        #[rstest]
898        fn config_builder_new(default_system_config: TestResult<SystemConfig>) -> TestResult {
899            let _config = ConfigBuilder::new(default_system_config?).finish()?;
900
901            Ok(())
902        }
903
904        /// Ensures that a reference to the [`SystemConfig`] can be retrieved from [`Config`].
905        #[rstest]
906        fn config_system(default_system_config: TestResult<SystemConfig>) -> TestResult {
907            let system_config = default_system_config?;
908            let config = ConfigBuilder::new(system_config.clone()).finish()?;
909            assert_eq!(config.system(), &system_config);
910
911            Ok(())
912        }
913
914        /// Ensures, that a [`Config`] object leads to a specific YAML output.
915        ///
916        /// In this particular case, only a [`SystemConfig`] object are present.
917        #[rstest]
918        fn config_to_yaml_string(default_system_config: TestResult<SystemConfig>) -> TestResult {
919            let config = ConfigBuilder::new(default_system_config?).finish()?;
920            let config_str = config.to_yaml_string()?;
921
922            with_settings!({
923                description => "Configuration with only system-wide configuration",
924                snapshot_path => SNAPSHOT_PATH,
925                prepend_module_to_snapshot => false,
926            }, {
927                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
928            });
929
930            Ok(())
931        }
932
933        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
934        /// the same YAML string.
935        ///
936        /// The configuration file only describes a [`SystemConfig`] object.
937        #[rstest]
938        fn roundtrip_yaml_config(
939            #[files("../fixtures/config/no_backend/*.yaml")] path: PathBuf,
940        ) -> TestResult {
941            let config_string = read_to_string(&path)?;
942            let config = Config::from_file_path(&path)?;
943
944            assert_eq!(config.to_yaml_string()?, config_string);
945
946            Ok(())
947        }
948
949        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
950        /// correctly.
951        #[rstest]
952        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
953            let config = default_config?;
954            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
955                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
956                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
957                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
958                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
959            ]);
960
961            assert_eq!(
962                config.authorized_key_entries(),
963                expected.iter().collect::<HashSet<_>>()
964            );
965            Ok(())
966        }
967
968        /// Ensures, that [`Config::system_user_data`] returns [`SystemUserData`] entries correctly.
969        #[rstest]
970        fn config_system_user_data(
971            default_config: TestResult<Config>,
972            raw_user_data_system: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
973        ) -> TestResult {
974            let config = default_config?;
975            let raw_user_data = raw_user_data_system?;
976            let expected: HashSet<SystemUserData> = HashSet::from_iter([
977                SystemUserData::HostShareholder {
978                    system_user: &raw_user_data[0].0,
979                    ssh_authorized_key: raw_user_data[0]
980                        .1
981                        .as_ref()
982                        .expect("to have SSH authorized key"),
983                },
984                SystemUserData::HostShareholder {
985                    system_user: &raw_user_data[1].0,
986                    ssh_authorized_key: raw_user_data[1]
987                        .1
988                        .as_ref()
989                        .expect("to have SSH authorized key"),
990                },
991                SystemUserData::HostShareholder {
992                    system_user: &raw_user_data[2].0,
993                    ssh_authorized_key: raw_user_data[2]
994                        .1
995                        .as_ref()
996                        .expect("to have SSH authorized key"),
997                },
998                SystemUserData::HostDownloadNetworkConfig {
999                    system_user: &raw_user_data[3].0,
1000                    ssh_authorized_key: raw_user_data[3]
1001                        .1
1002                        .as_ref()
1003                        .expect("to have SSH authorized key"),
1004                },
1005            ]);
1006
1007            assert_eq!(config.system_user_data(), expected);
1008            Ok(())
1009        }
1010
1011        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
1012        #[rstest]
1013        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
1014            let config = default_config?;
1015            let expected: HashSet<SystemUserId> = HashSet::from_iter([
1016                "share-holder1".parse()?,
1017                "share-holder2".parse()?,
1018                "share-holder3".parse()?,
1019                "wireguard-downloader".parse()?,
1020            ]);
1021
1022            assert_eq!(
1023                config.system_user_ids(),
1024                expected.iter().collect::<HashSet<_>>()
1025            );
1026            Ok(())
1027        }
1028
1029        /// Ensures, that [`SystemUserConfigState`] can be created from [`Config`].
1030        #[rstest]
1031        fn system_user_config_state_from_config(default_config: TestResult<Config>) -> TestResult {
1032            let config = default_config?;
1033            let state = SystemUserConfigState::from(&config);
1034
1035            assert_eq!(state.system_user_data, config.system_user_data(),);
1036            Ok(())
1037        }
1038    }
1039
1040    /// Tests, that are only available when using the NetHSM (and no other) backend.
1041    #[cfg(all(feature = "nethsm", not(feature = "yubihsm2")))]
1042    mod nethsm_backend {
1043        use pretty_assertions::assert_eq;
1044
1045        use super::*;
1046        use crate::config::{
1047            SystemUserData,
1048            traits::{ConfigSystemUserData, MappingAuthorizedKeyEntry, MappingSystemUserId},
1049        };
1050
1051        /// Creates a default [`Config`] for testing purposes.
1052        #[fixture]
1053        fn default_config(
1054            default_system_config: TestResult<SystemConfig>,
1055            default_nethsm_config: TestResult<NetHsmConfig>,
1056        ) -> TestResult<Config> {
1057            Ok(ConfigBuilder::new(default_system_config?)
1058                .set_nethsm_config(default_nethsm_config?)
1059                .finish()?)
1060        }
1061
1062        /// List of raw data required to create [`SystemUserData`] for each item in the default
1063        /// [`Config`].
1064        #[fixture]
1065        fn raw_user_data(
1066            raw_user_data_system: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1067            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1068        ) -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
1069            let mut data = raw_user_data_system?;
1070            data.extend(raw_user_data_nethsm?);
1071            Ok(data)
1072        }
1073
1074        /// Ensures that [`MappingSystemUserId`] for [`UserBackendConnection`] works as intended.
1075        #[rstest]
1076        fn user_backend_connection_system_user_id(
1077            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1078        ) -> TestResult {
1079            let raw_user_data_nethsm = raw_user_data_nethsm?;
1080            let data = UserBackendConnection::NetHsm {
1081                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
1082                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
1083                connections: BTreeSet::from_iter([Connection::new(
1084                    "https://nethsm1.example.org/".parse()?,
1085                    ConnectionSecurity::Unsafe,
1086                )]),
1087                mapping: NetHsmUserMapping::Backup {
1088                    backend_user: "backup".parse()?,
1089                    ssh_authorized_key: raw_user_data_nethsm[1]
1090                        .1
1091                        .clone()
1092                        .expect("to have an SSH authorized key"),
1093                    system_user: raw_user_data_nethsm[1].0.clone(),
1094                },
1095            };
1096            assert_eq!(data.system_user_id(), Some(&raw_user_data_nethsm[1].0));
1097
1098            Ok(())
1099        }
1100
1101        /// Ensures that [`MappingAuthorizedKeyEntry`] for [`UserBackendConnection`] works as
1102        /// intended.
1103        #[rstest]
1104        fn user_backend_connection_authorized_key_entry(
1105            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1106        ) -> TestResult {
1107            let raw_user_data_nethsm = raw_user_data_nethsm?;
1108            let data = UserBackendConnection::NetHsm {
1109                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
1110                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
1111                connections: BTreeSet::from_iter([Connection::new(
1112                    "https://nethsm1.example.org/".parse()?,
1113                    ConnectionSecurity::Unsafe,
1114                )]),
1115                mapping: NetHsmUserMapping::Backup {
1116                    backend_user: "backup".parse()?,
1117                    ssh_authorized_key: raw_user_data_nethsm[1]
1118                        .1
1119                        .clone()
1120                        .expect("to have an SSH authorized key"),
1121                    system_user: raw_user_data_nethsm[1].0.clone(),
1122                },
1123            };
1124            assert_eq!(
1125                data.authorized_key_entry(),
1126                Some(
1127                    raw_user_data_nethsm[1]
1128                        .1
1129                        .as_ref()
1130                        .expect("to have an SSH authorized key")
1131                )
1132            );
1133
1134            Ok(())
1135        }
1136
1137        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
1138        /// configuration components.
1139        ///
1140        /// Here, a custom [`NetHsmConfig`] is staged together with a default [`SystemConfig`]
1141        /// (created by [`default_system_config`]) to create a failure scenario.
1142        #[rstest]
1143        #[case::two_duplicate_system_users_two_duplicate_ssh_public_keys(
1144            "Configuration with system-wide and NetHSM configuration has two duplicate system users and two duplicate SSH public keys",
1145            NetHsmConfig::new(
1146                BTreeSet::from_iter([
1147                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1148                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1149                ]),
1150                BTreeSet::from_iter([
1151                    NetHsmUserMapping::Admin("admin".parse()?),
1152                    NetHsmUserMapping::Backup{
1153                        backend_user: "backup".parse()?,
1154                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1155                        system_user: "share-holder1".parse()?,
1156                    },
1157                    NetHsmUserMapping::HermeticMetrics {
1158                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1159                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1160                    },
1161                    NetHsmUserMapping::Metrics {
1162                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1163                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
1164                        system_user: "share-holder2".parse()?,
1165                    },
1166                    NetHsmUserMapping::Signing {
1167                        backend_user: "signing".parse()?,
1168                        signing_key_id: "signing1".parse()?,
1169                        key_setup: SigningKeySetup::new(
1170                            KeyType::Curve25519,
1171                            vec![KeyMechanism::EdDsaSignature],
1172                            None,
1173                            SignatureType::EdDsa,
1174                            CryptographicKeyContext::OpenPgp {
1175                                user_ids: OpenPgpUserIdList::new(vec![
1176                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1177                                ])?,
1178                                version: "v4".parse()?,
1179                            },
1180                        )?,
1181                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1182                        system_user: "nethsm-signing-user".parse()?,
1183                        tag: "signing1".to_string(),
1184                    }
1185                ]),
1186            )?
1187        )]
1188        #[case::one_duplicate_system_user_two_duplicate_ssh_public_keys(
1189            "Configuration with system-wide and NetHSM configuration has one duplicate system user and two duplicate SSH public keys",
1190            NetHsmConfig::new(
1191                BTreeSet::from_iter([
1192                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1193                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1194                ]),
1195                BTreeSet::from_iter([
1196                    NetHsmUserMapping::Admin("admin".parse()?),
1197                    NetHsmUserMapping::Backup{
1198                        backend_user: "backup".parse()?,
1199                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1200                        system_user: "share-holder1".parse()?,
1201                    },
1202                    NetHsmUserMapping::HermeticMetrics {
1203                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1204                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1205                    },
1206                    NetHsmUserMapping::Metrics {
1207                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1208                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
1209                        system_user: "nethsm-metrics-user".parse()?,
1210                    },
1211                    NetHsmUserMapping::Signing {
1212                        backend_user: "signing".parse()?,
1213                        signing_key_id: "signing1".parse()?,
1214                        key_setup: SigningKeySetup::new(
1215                            KeyType::Curve25519,
1216                            vec![KeyMechanism::EdDsaSignature],
1217                            None,
1218                            SignatureType::EdDsa,
1219                            CryptographicKeyContext::OpenPgp {
1220                                user_ids: OpenPgpUserIdList::new(vec![
1221                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1222                                ])?,
1223                                version: "v4".parse()?,
1224                            },
1225                        )?,
1226                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1227                        system_user: "nethsm-signing-user".parse()?,
1228                        tag: "signing1".to_string(),
1229                    }
1230                ]),
1231            )?
1232        )]
1233        #[case::one_duplicate_system_user_one_duplicate_ssh_public_key(
1234            "Configuration with system-wide and NetHSM configuration has one duplicate system user and one duplicate SSH public key",
1235            NetHsmConfig::new(
1236                BTreeSet::from_iter([
1237                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1238                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1239                ]),
1240                BTreeSet::from_iter([
1241                    NetHsmUserMapping::Admin("admin".parse()?),
1242                    NetHsmUserMapping::Backup{
1243                        backend_user: "backup".parse()?,
1244                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1245                        system_user: "share-holder1".parse()?,
1246                    },
1247                    NetHsmUserMapping::HermeticMetrics {
1248                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1249                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1250                    },
1251                    NetHsmUserMapping::Metrics {
1252                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1253                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1254                        system_user: "nethsm-metrics-user".parse()?,
1255                    },
1256                    NetHsmUserMapping::Signing {
1257                        backend_user: "signing".parse()?,
1258                        signing_key_id: "signing1".parse()?,
1259                        key_setup: SigningKeySetup::new(
1260                            KeyType::Curve25519,
1261                            vec![KeyMechanism::EdDsaSignature],
1262                            None,
1263                            SignatureType::EdDsa,
1264                            CryptographicKeyContext::OpenPgp {
1265                                user_ids: OpenPgpUserIdList::new(vec![
1266                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1267                                ])?,
1268                                version: "v4".parse()?,
1269                            },
1270                        )?,
1271                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1272                        system_user: "nethsm-signing-user".parse()?,
1273                        tag: "signing1".to_string(),
1274                    }
1275                ]),
1276            )?
1277        )]
1278        #[case::one_duplicate_ssh_public_key(
1279            "Configuration with system-wide and NetHSM configuration has one duplicate SSH public key",
1280            NetHsmConfig::new(
1281                BTreeSet::from_iter([
1282                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1283                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1284                ]),
1285                BTreeSet::from_iter([
1286                    NetHsmUserMapping::Admin("admin".parse()?),
1287                    NetHsmUserMapping::Backup{
1288                        backend_user: "backup".parse()?,
1289                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1290                        system_user: "nethsm-backup-user".parse()?,
1291                    },
1292                    NetHsmUserMapping::HermeticMetrics {
1293                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1294                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1295                    },
1296                    NetHsmUserMapping::Metrics {
1297                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1298                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1299                        system_user: "nethsm-metrics-user".parse()?,
1300                    },
1301                    NetHsmUserMapping::Signing {
1302                        backend_user: "signing".parse()?,
1303                        signing_key_id: "signing1".parse()?,
1304                        key_setup: SigningKeySetup::new(
1305                            KeyType::Curve25519,
1306                            vec![KeyMechanism::EdDsaSignature],
1307                            None,
1308                            SignatureType::EdDsa,
1309                            CryptographicKeyContext::OpenPgp {
1310                                user_ids: OpenPgpUserIdList::new(vec![
1311                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1312                                ])?,
1313                                version: "v4".parse()?,
1314                            },
1315                        )?,
1316                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1317                        system_user: "nethsm-signing-user".parse()?,
1318                        tag: "signing1".to_string(),
1319                    }
1320                ]),
1321            )?
1322        )]
1323        #[case::one_duplicate_system_user(
1324            "Configuration with system-wide and NetHSM configuration has one duplicate system user",
1325            NetHsmConfig::new(
1326                BTreeSet::from_iter([
1327                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1328                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1329                ]),
1330                BTreeSet::from_iter([
1331                    NetHsmUserMapping::Admin("admin".parse()?),
1332                    NetHsmUserMapping::Backup{
1333                        backend_user: "backup".parse()?,
1334                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1335                        system_user: "share-holder1".parse()?,
1336                    },
1337                    NetHsmUserMapping::HermeticMetrics {
1338                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1339                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1340                    },
1341                    NetHsmUserMapping::Metrics {
1342                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1343                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1344                        system_user: "nethsm-metrics-user".parse()?,
1345                    },
1346                    NetHsmUserMapping::Signing {
1347                        backend_user: "signing".parse()?,
1348                        signing_key_id: "signing1".parse()?,
1349                        key_setup: SigningKeySetup::new(
1350                            KeyType::Curve25519,
1351                            vec![KeyMechanism::EdDsaSignature],
1352                            None,
1353                            SignatureType::EdDsa,
1354                            CryptographicKeyContext::OpenPgp {
1355                                user_ids: OpenPgpUserIdList::new(vec![
1356                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1357                                ])?,
1358                                version: "v4".parse()?,
1359                            },
1360                        )?,
1361                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1362                        system_user: "nethsm-signing-user".parse()?,
1363                        tag: "signing1".to_string(),
1364                    }
1365                ]),
1366            )?
1367        )]
1368        fn config_builder_fails_validation(
1369            default_system_config: TestResult<SystemConfig>,
1370            #[case] description: &str,
1371            #[case] nethsm_config: NetHsmConfig,
1372        ) -> TestResult {
1373            let error_message = match ConfigBuilder::new(default_system_config?)
1374                .set_nethsm_config(nethsm_config)
1375                .finish()
1376            {
1377                Err(error) => error.to_string(),
1378                Ok(config) => panic!(
1379                    "Expected to fail with Error::Validation, but succeeded instead: {}",
1380                    config.to_yaml_string()?
1381                ),
1382            };
1383
1384            with_settings!({
1385                description => description,
1386                snapshot_path => SNAPSHOT_PATH,
1387                prepend_module_to_snapshot => false,
1388            }, {
1389                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
1390            });
1391
1392            Ok(())
1393        }
1394
1395        /// Ensures, that [`Config::nethsm`] returns the original input.
1396        #[rstest]
1397        fn config_nethsm(
1398            default_system_config: TestResult<SystemConfig>,
1399            default_nethsm_config: TestResult<NetHsmConfig>,
1400        ) -> TestResult {
1401            let nethsm_config = default_nethsm_config?;
1402
1403            let config = ConfigBuilder::new(default_system_config?)
1404                .set_nethsm_config(nethsm_config.clone())
1405                .finish()?;
1406
1407            assert_eq!(
1408                &nethsm_config,
1409                config.nethsm().expect("a NetHsmConfig reference")
1410            );
1411
1412            Ok(())
1413        }
1414
1415        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
1416        #[rstest]
1417        #[case::nethsm_signing(
1418            "nethsm-signing-user",
1419            Some(UserBackendConnection::NetHsm {
1420                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1421                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1422                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1423                },
1424                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1425                connections: BTreeSet::from_iter([
1426                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1427                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1428                ]),
1429                mapping: NetHsmUserMapping::Signing {
1430                    backend_user: "signing".parse()?,
1431                    signing_key_id: "signing1".parse()?,
1432                    key_setup: SigningKeySetup::new(
1433                        KeyType::Curve25519,
1434                        vec![KeyMechanism::EdDsaSignature],
1435                        None,
1436                        SignatureType::EdDsa,
1437                        CryptographicKeyContext::OpenPgp {
1438                            user_ids: OpenPgpUserIdList::new(vec![
1439                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1440                            ])?,
1441                            version: "v4".parse()?,
1442                        },
1443                    )?,
1444                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1445                    system_user: "nethsm-signing-user".parse()?,
1446                    tag: "signing1".to_string(),
1447                }
1448            })
1449        )]
1450        #[case::none("foo", None)]
1451        fn config_user_backend_connection(
1452            default_config: TestResult<Config>,
1453            #[case] system_user: &str,
1454            #[case] expected_connection: Option<UserBackendConnection>,
1455        ) -> TestResult {
1456            let config = default_config?;
1457            assert_eq!(
1458                expected_connection,
1459                config.user_backend_connection(&system_user.parse()?)
1460            );
1461
1462            Ok(())
1463        }
1464
1465        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
1466        /// [`UserBackendConnection`] items according to a [`UserBackendConnectionFilter`].
1467        #[rstest]
1468        #[case::filter_all(
1469            UserBackendConnectionFilter::All,
1470            vec![
1471                UserBackendConnection::NetHsm {
1472                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1473                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1474                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1475                    },
1476                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1477                    connections: BTreeSet::from_iter([
1478                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1479                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1480                    ]),
1481                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
1482                },
1483                UserBackendConnection::NetHsm {
1484                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1485                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1486                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1487                    },
1488                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1489                    connections: BTreeSet::from_iter([
1490                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1491                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1492                    ]),
1493                    mapping: NetHsmUserMapping::Backup{
1494                        backend_user: "backup".parse()?,
1495                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1496                        system_user: "nethsm-backup-user".parse()?,
1497                    }
1498                },
1499                UserBackendConnection::NetHsm {
1500                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1501                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1502                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1503                    },
1504                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1505                    connections: BTreeSet::from_iter([
1506                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1507                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1508                    ]),
1509                    mapping: NetHsmUserMapping::HermeticMetrics {
1510                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1511                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1512                    }
1513                },
1514                UserBackendConnection::NetHsm {
1515                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1516                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1517                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1518                    },
1519                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1520                    connections: BTreeSet::from_iter([
1521                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1522                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1523                    ]),
1524                    mapping: NetHsmUserMapping::Metrics {
1525                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1526                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1527                        system_user: "nethsm-metrics-user".parse()?,
1528                    }
1529                },
1530                UserBackendConnection::NetHsm {
1531                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1532                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1533                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1534                    },
1535                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1536                    connections: BTreeSet::from_iter([
1537                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1538                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1539                    ]),
1540                    mapping: NetHsmUserMapping::Signing {
1541                        backend_user: "signing".parse()?,
1542                        signing_key_id: "signing1".parse()?,
1543                        key_setup: SigningKeySetup::new(
1544                            KeyType::Curve25519,
1545                            vec![KeyMechanism::EdDsaSignature],
1546                            None,
1547                            SignatureType::EdDsa,
1548                            CryptographicKeyContext::OpenPgp {
1549                                user_ids: OpenPgpUserIdList::new(vec![
1550                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1551                                ])?,
1552                                version: "v4".parse()?,
1553                            },
1554                        )?,
1555                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1556                        system_user: "nethsm-signing-user".parse()?,
1557                        tag: "signing1".to_string(),
1558                    }
1559                },
1560            ],
1561        )]
1562        #[case::filter_admin(
1563            UserBackendConnectionFilter::Admin,
1564            vec![
1565                UserBackendConnection::NetHsm {
1566                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1567                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1568                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1569                    },
1570                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1571                    connections: BTreeSet::from_iter([
1572                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1573                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1574                    ]),
1575                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
1576                },
1577            ],
1578        )]
1579        #[case::filter_non_admin(
1580            UserBackendConnectionFilter::NonAdmin,
1581            vec![
1582                UserBackendConnection::NetHsm {
1583                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1584                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1585                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1586                    },
1587                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1588                    connections: BTreeSet::from_iter([
1589                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1590                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1591                    ]),
1592                    mapping: NetHsmUserMapping::Backup{
1593                        backend_user: "backup".parse()?,
1594                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1595                        system_user: "nethsm-backup-user".parse()?,
1596                    }
1597                },
1598                UserBackendConnection::NetHsm {
1599                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1600                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1601                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1602                    },
1603                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1604                    connections: BTreeSet::from_iter([
1605                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1606                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1607                    ]),
1608                    mapping: NetHsmUserMapping::HermeticMetrics {
1609                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1610                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1611                    }
1612                },
1613                UserBackendConnection::NetHsm {
1614                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1615                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1616                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1617                    },
1618                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1619                    connections: BTreeSet::from_iter([
1620                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1621                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1622                    ]),
1623                    mapping: NetHsmUserMapping::Metrics {
1624                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1625                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1626                        system_user: "nethsm-metrics-user".parse()?,
1627                    }
1628                },
1629                UserBackendConnection::NetHsm {
1630                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1631                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1632                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1633                    },
1634                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1635                    connections: BTreeSet::from_iter([
1636                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1637                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1638                    ]),
1639                    mapping: NetHsmUserMapping::Signing {
1640                        backend_user: "signing".parse()?,
1641                        signing_key_id: "signing1".parse()?,
1642                        key_setup: SigningKeySetup::new(
1643                            KeyType::Curve25519,
1644                            vec![KeyMechanism::EdDsaSignature],
1645                            None,
1646                            SignatureType::EdDsa,
1647                            CryptographicKeyContext::OpenPgp {
1648                                user_ids: OpenPgpUserIdList::new(vec![
1649                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1650                                ])?,
1651                                version: "v4".parse()?,
1652                            },
1653                        )?,
1654                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1655                        system_user: "nethsm-signing-user".parse()?,
1656                        tag: "signing1".to_string(),
1657                    }
1658                },
1659            ],
1660        )]
1661        fn config_user_backend_connections(
1662            default_config: TestResult<Config>,
1663            #[case] filter: UserBackendConnectionFilter,
1664            #[case] expected_connections: Vec<UserBackendConnection>,
1665        ) -> TestResult {
1666            let config = default_config?;
1667
1668            assert_eq!(
1669                expected_connections,
1670                config.user_backend_connections(filter)
1671            );
1672
1673            Ok(())
1674        }
1675
1676        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
1677        /// correctly.
1678        #[rstest]
1679        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
1680            let config = default_config?;
1681            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
1682                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
1683                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
1684                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1685                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1686                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1687                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1688                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1689            ]);
1690
1691            assert_eq!(
1692                config.authorized_key_entries(),
1693                expected.iter().collect::<HashSet<_>>()
1694            );
1695            Ok(())
1696        }
1697
1698        /// Ensures, that [`Config::system_user_data`] returns [`SystemUserData`] entries correctly.
1699        #[rstest]
1700        fn config_system_user_data(
1701            default_config: TestResult<Config>,
1702            raw_user_data: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1703        ) -> TestResult {
1704            let config = default_config?;
1705            let raw_user_data = raw_user_data?;
1706            let expected: HashSet<SystemUserData> = HashSet::from_iter([
1707                SystemUserData::HostShareholder {
1708                    system_user: &raw_user_data[0].0,
1709                    ssh_authorized_key: raw_user_data[0]
1710                        .1
1711                        .as_ref()
1712                        .expect("to have SSH authorized key"),
1713                },
1714                SystemUserData::HostShareholder {
1715                    system_user: &raw_user_data[1].0,
1716                    ssh_authorized_key: raw_user_data[1]
1717                        .1
1718                        .as_ref()
1719                        .expect("to have SSH authorized key"),
1720                },
1721                SystemUserData::HostShareholder {
1722                    system_user: &raw_user_data[2].0,
1723                    ssh_authorized_key: raw_user_data[2]
1724                        .1
1725                        .as_ref()
1726                        .expect("to have SSH authorized key"),
1727                },
1728                SystemUserData::HostDownloadNetworkConfig {
1729                    system_user: &raw_user_data[3].0,
1730                    ssh_authorized_key: raw_user_data[3]
1731                        .1
1732                        .as_ref()
1733                        .expect("to have SSH authorized key"),
1734                },
1735                SystemUserData::BackendAdmin {
1736                    system_user: raw_user_data[4].0.clone(),
1737                },
1738                SystemUserData::BackendBackup {
1739                    system_user: &raw_user_data[5].0,
1740                    ssh_authorized_key: raw_user_data[5]
1741                        .1
1742                        .as_ref()
1743                        .expect("to have SSH authorized key"),
1744                },
1745                SystemUserData::BackendHermeticMetrics {
1746                    system_user: &raw_user_data[6].0,
1747                },
1748                SystemUserData::BackendMetrics {
1749                    system_user: &raw_user_data[7].0,
1750                    ssh_authorized_key: raw_user_data[7]
1751                        .1
1752                        .as_ref()
1753                        .expect("to have SSH authorized key"),
1754                },
1755                SystemUserData::BackendSign {
1756                    system_user: &raw_user_data[8].0,
1757                    ssh_authorized_key: raw_user_data[8]
1758                        .1
1759                        .as_ref()
1760                        .expect("to have SSH authorized key"),
1761                },
1762            ]);
1763
1764            assert_eq!(config.system_user_data(), expected);
1765            Ok(())
1766        }
1767
1768        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
1769        #[rstest]
1770        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
1771            let config = default_config?;
1772            let expected: HashSet<SystemUserId> = HashSet::from_iter([
1773                "share-holder1".parse()?,
1774                "share-holder2".parse()?,
1775                "share-holder3".parse()?,
1776                "wireguard-downloader".parse()?,
1777                "nethsm-backup-user".parse()?,
1778                "nethsm-hermetic-metrics-user".parse()?,
1779                "nethsm-metrics-user".parse()?,
1780                "nethsm-signing-user".parse()?,
1781            ]);
1782
1783            assert_eq!(
1784                config.system_user_ids(),
1785                expected.iter().collect::<HashSet<_>>()
1786            );
1787            Ok(())
1788        }
1789
1790        /// Ensures, that a [`Config`] object leads to a specific YAML output.
1791        ///
1792        /// In this particular case, a [`SystemConfig`] and a [`NetHsmConfig`] object are present.
1793        #[rstest]
1794        fn config_to_yaml_string(
1795            default_system_config: TestResult<SystemConfig>,
1796            default_nethsm_config: TestResult<NetHsmConfig>,
1797        ) -> TestResult {
1798            let config = ConfigBuilder::new(default_system_config?)
1799                .set_nethsm_config(default_nethsm_config?)
1800                .finish()?;
1801            let config_str = config.to_yaml_string()?;
1802
1803            with_settings!({
1804                description => "Configuration with system-wide and NetHSM configuration",
1805                snapshot_path => SNAPSHOT_PATH,
1806                prepend_module_to_snapshot => false,
1807            }, {
1808                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
1809            });
1810
1811            Ok(())
1812        }
1813
1814        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
1815        /// the same YAML string.
1816        ///
1817        /// The configuration file describes a [`SystemConfig`] and a [`NetHsmConfig`] object.
1818        #[rstest]
1819        fn roundtrip_yaml_config(
1820            #[files("../fixtures/config/nethsm_backend/*.yaml")] path: PathBuf,
1821        ) -> TestResult {
1822            let config_string = read_to_string(&path)?;
1823            let config = Config::from_file_path(&path)?;
1824
1825            assert_eq!(config.to_yaml_string()?, config_string);
1826
1827            Ok(())
1828        }
1829
1830        /// Ensures, that [`AdministrativeSecretHandling`] and
1831        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
1832        /// [`UserBackendConnection`].
1833        #[rstest]
1834        fn user_backend_connection_secret_handling(
1835            default_config: TestResult<Config>,
1836        ) -> TestResult {
1837            let config = default_config?;
1838            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
1839                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1840                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1841            };
1842            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
1843
1844            let user_backend_connection = config
1845                .user_backend_connection(&"nethsm-signing-user".parse()?)
1846                .expect("there to be a mapping of the requested name");
1847
1848            assert_eq!(
1849                user_backend_connection.admin_secret_handling(),
1850                admin_secret_handling
1851            );
1852            assert_eq!(
1853                user_backend_connection.non_admin_secret_handling(),
1854                non_admin_secret_handling
1855            );
1856
1857            Ok(())
1858        }
1859
1860        /// Ensures, that [`SystemUserConfigState`] can be created from [`Config`].
1861        #[rstest]
1862        fn system_user_config_state_from_config(default_config: TestResult<Config>) -> TestResult {
1863            let config = default_config?;
1864            let state = SystemUserConfigState::from(&config);
1865
1866            assert_eq!(state.system_user_data, config.system_user_data(),);
1867            Ok(())
1868        }
1869    }
1870
1871    /// Tests, that are only available when using the YubiHSM2 (and no other) backend.
1872    #[cfg(all(feature = "yubihsm2", not(feature = "nethsm")))]
1873    mod yubihsm2_backend {
1874        use pretty_assertions::assert_eq;
1875
1876        use super::*;
1877        use crate::config::{
1878            SystemUserData,
1879            traits::{ConfigSystemUserData, MappingAuthorizedKeyEntry, MappingSystemUserId},
1880        };
1881
1882        /// Creates a default [`Config`] for testing purposes.
1883        #[fixture]
1884        fn default_config(
1885            default_system_config: TestResult<SystemConfig>,
1886            default_yubihsm2_config: TestResult<YubiHsm2Config>,
1887        ) -> TestResult<Config> {
1888            Ok(ConfigBuilder::new(default_system_config?)
1889                .set_yubihsm2_config(default_yubihsm2_config?)
1890                .finish()?)
1891        }
1892
1893        /// List of raw data required to create [`SystemUserData`] for each item in the default
1894        /// [`Config`].
1895        #[fixture]
1896        fn raw_user_data(
1897            raw_user_data_system: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1898            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1899        ) -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
1900            let mut data = raw_user_data_system?;
1901            data.extend(raw_user_data_yubihsm2?);
1902            Ok(data)
1903        }
1904
1905        /// Ensures that [`MappingSystemUserId`] for [`UserBackendConnection`] works as intended.
1906        #[rstest]
1907        fn user_backend_connection_system_user_id(
1908            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1909        ) -> TestResult {
1910            let raw_user_data_yubihsm2 = raw_user_data_yubihsm2?;
1911            let data = UserBackendConnection::YubiHsm2 {
1912                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
1913                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
1914                connections: BTreeSet::from_iter([YubiHsm2Connection::Usb {
1915                    serial_number: "0123456789".parse()?,
1916                }]),
1917                mapping: YubiHsm2UserMapping::AuditLog {
1918                    authentication_key_id: "1".parse()?,
1919                    ssh_authorized_key: raw_user_data_yubihsm2[1]
1920                        .1
1921                        .clone()
1922                        .expect("to have an SSH authorized key"),
1923                    system_user: raw_user_data_yubihsm2[1].0.clone(),
1924                },
1925            };
1926            assert_eq!(data.system_user_id(), Some(&raw_user_data_yubihsm2[1].0));
1927
1928            Ok(())
1929        }
1930
1931        /// Ensures that [`MappingAuthorizedKeyEntry`] for [`UserBackendConnection`] works as
1932        /// intended.
1933        #[rstest]
1934        fn user_backend_connection_authorized_key_entry(
1935            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1936        ) -> TestResult {
1937            let raw_user_data_yubihsm2 = raw_user_data_yubihsm2?;
1938            let data = UserBackendConnection::YubiHsm2 {
1939                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
1940                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
1941                connections: BTreeSet::from_iter([YubiHsm2Connection::Usb {
1942                    serial_number: "0123456789".parse()?,
1943                }]),
1944                mapping: YubiHsm2UserMapping::AuditLog {
1945                    authentication_key_id: "1".parse()?,
1946                    ssh_authorized_key: raw_user_data_yubihsm2[1]
1947                        .1
1948                        .clone()
1949                        .expect("to have an SSH authorized key"),
1950                    system_user: raw_user_data_yubihsm2[1].0.clone(),
1951                },
1952            };
1953            assert_eq!(
1954                data.authorized_key_entry(),
1955                Some(
1956                    raw_user_data_yubihsm2[1]
1957                        .1
1958                        .as_ref()
1959                        .expect("to have an SSH authorized key")
1960                )
1961            );
1962
1963            Ok(())
1964        }
1965
1966        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
1967        /// configuration components.
1968        ///
1969        /// Here, a custom [`YubiHsm2Config`] is staged together with a default [`SystemConfig`]
1970        /// (created by [`default_system_config`]) to create a failure scenario.
1971        #[rstest]
1972        #[case::two_duplicate_system_users_two_duplicate_ssh_public_keys(
1973            "Configuration with system-wide and YubiHSM2 configuration has two duplicate system users and two duplicate SSH public keys",
1974            YubiHsm2Config::new(
1975                BTreeSet::from_iter([
1976                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1977                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1978                ]),
1979                BTreeSet::from_iter([
1980                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1981                    YubiHsm2UserMapping::AuditLog {
1982                        authentication_key_id: "3".parse()?,
1983                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1984                        system_user: "share-holder2".parse()?,
1985                    },
1986                    YubiHsm2UserMapping::Backup{
1987                        authentication_key_id: "2".parse()?,
1988                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1989                        system_user: "share-holder1".parse()?,
1990                        wrapping_key_id: "1".parse()?,
1991                    },
1992                    YubiHsm2UserMapping::HermeticAuditLog {
1993                        authentication_key_id: "4".parse()?,
1994                        system_user: "yubihsm2-hermetic-metrics".parse()?,
1995                    },
1996                    YubiHsm2UserMapping::Signing {
1997                        authentication_key_id: "5".parse()?,
1998                        signing_key_id: "1".parse()?,
1999                        key_setup: SigningKeySetup::new(
2000                            KeyType::Curve25519,
2001                            vec![KeyMechanism::EdDsaSignature],
2002                            None,
2003                            SignatureType::EdDsa,
2004                            CryptographicKeyContext::OpenPgp {
2005                                user_ids: OpenPgpUserIdList::new(vec![
2006                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2007                                ])?,
2008                                version: "v4".parse()?,
2009                            },
2010                        )?,
2011                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2012                        system_user: "yubihsm2-signing-user".parse()?,
2013                        domain: Domain::One,
2014                    }
2015                ]),
2016            )?
2017         )]
2018        #[case::one_duplicate_system_user_two_duplicate_ssh_public_keys(
2019            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user and two duplicate SSH public keys",
2020            YubiHsm2Config::new(
2021                BTreeSet::from_iter([
2022                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2023                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2024                ]),
2025                BTreeSet::from_iter([
2026                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2027                    YubiHsm2UserMapping::AuditLog {
2028                        authentication_key_id: "3".parse()?,
2029                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2030                        system_user: "yubihsm2-metrics-user".parse()?,
2031                    },
2032                    YubiHsm2UserMapping::Backup{
2033                        authentication_key_id: "2".parse()?,
2034                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2035                        system_user: "share-holder1".parse()?,
2036                        wrapping_key_id: "1".parse()?,
2037                    },
2038                    YubiHsm2UserMapping::HermeticAuditLog {
2039                        authentication_key_id: "4".parse()?,
2040                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2041                    },
2042                    YubiHsm2UserMapping::Signing {
2043                        authentication_key_id: "5".parse()?,
2044                        signing_key_id: "1".parse()?,
2045                        key_setup: SigningKeySetup::new(
2046                            KeyType::Curve25519,
2047                            vec![KeyMechanism::EdDsaSignature],
2048                            None,
2049                            SignatureType::EdDsa,
2050                            CryptographicKeyContext::OpenPgp {
2051                                user_ids: OpenPgpUserIdList::new(vec![
2052                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2053                                ])?,
2054                                version: "v4".parse()?,
2055                            },
2056                        )?,
2057                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2058                        system_user: "yubihsm2-signing-user".parse()?,
2059                        domain: Domain::One,
2060                    }
2061                ]),
2062            )?
2063         )]
2064        #[case::one_duplicate_system_user_one_duplicate_ssh_public_key(
2065            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user and one duplicate SSH public key",
2066            YubiHsm2Config::new(
2067                BTreeSet::from_iter([
2068                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2069                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2070                ]),
2071                BTreeSet::from_iter([
2072                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2073                    YubiHsm2UserMapping::AuditLog {
2074                        authentication_key_id: "3".parse()?,
2075                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2076                        system_user: "yubihsm2-metrics-user".parse()?,
2077                    },
2078                    YubiHsm2UserMapping::Backup{
2079                        authentication_key_id: "2".parse()?,
2080                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2081                        system_user: "share-holder1".parse()?,
2082                        wrapping_key_id: "1".parse()?,
2083                    },
2084                    YubiHsm2UserMapping::HermeticAuditLog {
2085                        authentication_key_id: "4".parse()?,
2086                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2087                    },
2088                    YubiHsm2UserMapping::Signing {
2089                        authentication_key_id: "5".parse()?,
2090                        signing_key_id: "1".parse()?,
2091                        key_setup: SigningKeySetup::new(
2092                            KeyType::Curve25519,
2093                            vec![KeyMechanism::EdDsaSignature],
2094                            None,
2095                            SignatureType::EdDsa,
2096                            CryptographicKeyContext::OpenPgp {
2097                                user_ids: OpenPgpUserIdList::new(vec![
2098                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2099                                ])?,
2100                                version: "v4".parse()?,
2101                            },
2102                        )?,
2103                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2104                        system_user: "yubihsm2-signing-user".parse()?,
2105                        domain: Domain::One,
2106                    }
2107                ]),
2108            )?
2109         )]
2110        #[case::one_duplicate_ssh_public_key(
2111            "Configuration with system-wide and YubiHSM2 configuration has one duplicate SSH public key",
2112            YubiHsm2Config::new(
2113                BTreeSet::from_iter([
2114                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2115                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2116                ]),
2117                BTreeSet::from_iter([
2118                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2119                    YubiHsm2UserMapping::AuditLog {
2120                        authentication_key_id: "3".parse()?,
2121                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2122                        system_user: "yubihsm2-metrics-user".parse()?,
2123                    },
2124                    YubiHsm2UserMapping::Backup{
2125                        authentication_key_id: "2".parse()?,
2126                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2127                        system_user: "yubihsm2-backup-user".parse()?,
2128                        wrapping_key_id: "1".parse()?,
2129                    },
2130                    YubiHsm2UserMapping::HermeticAuditLog {
2131                        authentication_key_id: "4".parse()?,
2132                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2133                    },
2134                    YubiHsm2UserMapping::Signing {
2135                        authentication_key_id: "5".parse()?,
2136                        signing_key_id: "1".parse()?,
2137                        key_setup: SigningKeySetup::new(
2138                            KeyType::Curve25519,
2139                            vec![KeyMechanism::EdDsaSignature],
2140                            None,
2141                            SignatureType::EdDsa,
2142                            CryptographicKeyContext::OpenPgp {
2143                                user_ids: OpenPgpUserIdList::new(vec![
2144                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2145                                ])?,
2146                                version: "v4".parse()?,
2147                            },
2148                        )?,
2149                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2150                        system_user: "yubihsm2-signing-user".parse()?,
2151                        domain: Domain::One,
2152                    }
2153                ]),
2154            )?
2155         )]
2156        #[case::one_duplicate_system_user(
2157            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user",
2158            YubiHsm2Config::new(
2159                BTreeSet::from_iter([
2160                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2161                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2162                ]),
2163                BTreeSet::from_iter([
2164                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2165                    YubiHsm2UserMapping::AuditLog {
2166                        authentication_key_id: "3".parse()?,
2167                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2168                        system_user: "yubihsm2-metrics-user".parse()?,
2169                    },
2170                    YubiHsm2UserMapping::Backup{
2171                        authentication_key_id: "2".parse()?,
2172                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2173                        system_user: "share-holder1".parse()?,
2174                        wrapping_key_id: "1".parse()?,
2175                    },
2176                    YubiHsm2UserMapping::HermeticAuditLog {
2177                        authentication_key_id: "4".parse()?,
2178                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2179                    },
2180                    YubiHsm2UserMapping::Signing {
2181                        authentication_key_id: "5".parse()?,
2182                        signing_key_id: "1".parse()?,
2183                        key_setup: SigningKeySetup::new(
2184                            KeyType::Curve25519,
2185                            vec![KeyMechanism::EdDsaSignature],
2186                            None,
2187                            SignatureType::EdDsa,
2188                            CryptographicKeyContext::OpenPgp {
2189                                user_ids: OpenPgpUserIdList::new(vec![
2190                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2191                                ])?,
2192                                version: "v4".parse()?,
2193                            },
2194                        )?,
2195                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2196                        system_user: "yubihsm2-signing-user".parse()?,
2197                        domain: Domain::One,
2198                    }
2199                ]),
2200            )?
2201         )]
2202        fn config_builder_fails_validation(
2203            default_system_config: TestResult<SystemConfig>,
2204            #[case] description: &str,
2205            #[case] yubihsm2_config: YubiHsm2Config,
2206        ) -> TestResult {
2207            let error_message = match ConfigBuilder::new(default_system_config?)
2208                .set_yubihsm2_config(yubihsm2_config)
2209                .finish()
2210            {
2211                Err(error) => error.to_string(),
2212                Ok(config) => panic!(
2213                    "Expected to fail with Error::Validation, but succeeded instead: {}",
2214                    config.to_yaml_string()?
2215                ),
2216            };
2217
2218            with_settings!({
2219                description => description,
2220                snapshot_path => SNAPSHOT_PATH,
2221                prepend_module_to_snapshot => false,
2222            }, {
2223                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
2224            });
2225
2226            Ok(())
2227        }
2228
2229        /// Ensures, that [`Config::yubihsm2`] returns the original input.
2230        #[rstest]
2231        fn config_yubihsm2(
2232            default_system_config: TestResult<SystemConfig>,
2233            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2234        ) -> TestResult {
2235            let yubihsm2_config = default_yubihsm2_config?;
2236
2237            let config = ConfigBuilder::new(default_system_config?)
2238                .set_yubihsm2_config(yubihsm2_config.clone())
2239                .finish()?;
2240
2241            assert_eq!(
2242                &yubihsm2_config,
2243                config.yubihsm2().expect("a YubiHsm2Config reference")
2244            );
2245
2246            Ok(())
2247        }
2248
2249        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
2250        #[rstest]
2251        #[case::yubihsm2_signing(
2252            "yubihsm2-signing-user",
2253            Some(UserBackendConnection::YubiHsm2 {
2254                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2255                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2256                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2257                },
2258                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2259                connections: BTreeSet::from_iter([
2260                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2261                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2262                ]),
2263                mapping: YubiHsm2UserMapping::Signing {
2264                    authentication_key_id: "5".parse()?,
2265                    signing_key_id: "1".parse()?,
2266                    key_setup: SigningKeySetup::new(
2267                        KeyType::Curve25519,
2268                        vec![KeyMechanism::EdDsaSignature],
2269                        None,
2270                        SignatureType::EdDsa,
2271                        CryptographicKeyContext::OpenPgp {
2272                            user_ids: OpenPgpUserIdList::new(vec![
2273                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2274                            ])?,
2275                            version: "v4".parse()?,
2276                        },
2277                    )?,
2278                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2279                    system_user: "yubihsm2-signing-user".parse()?,
2280                    domain: Domain::One,
2281                }
2282            })
2283        )]
2284        #[case::none("foo", None)]
2285        fn config_user_backend_connection(
2286            default_config: TestResult<Config>,
2287            #[case] system_user: &str,
2288            #[case] expected_connection: Option<UserBackendConnection>,
2289        ) -> TestResult {
2290            let config = default_config?;
2291            assert_eq!(
2292                expected_connection,
2293                config.user_backend_connection(&system_user.parse()?)
2294            );
2295
2296            Ok(())
2297        }
2298
2299        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
2300        /// [`UserBackendConnection`] items according to a [`UserBackendConnectionFilter`].
2301        #[rstest]
2302        #[case::filter_all(
2303            UserBackendConnectionFilter::All,
2304            vec![
2305                UserBackendConnection::YubiHsm2 {
2306                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2307                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2308                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2309                    },
2310                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2311                    connections: BTreeSet::from_iter([
2312                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2313                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2314                    ]),
2315                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2316                },
2317                UserBackendConnection::YubiHsm2 {
2318                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2319                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2320                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2321                    },
2322                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2323                    connections: BTreeSet::from_iter([
2324                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2325                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2326                    ]),
2327                    mapping: YubiHsm2UserMapping::AuditLog {
2328                        authentication_key_id: "3".parse()?,
2329                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2330                        system_user: "yubihsm2-metrics-user".parse()?,
2331                    },
2332                },
2333                UserBackendConnection::YubiHsm2 {
2334                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2335                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2336                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2337                    },
2338                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2339                    connections: BTreeSet::from_iter([
2340                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2341                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2342                    ]),
2343                    mapping: YubiHsm2UserMapping::Backup{
2344                        authentication_key_id: "2".parse()?,
2345                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2346                        system_user: "yubihsm2-backup-user".parse()?,
2347                        wrapping_key_id: "1".parse()?,
2348                    },
2349                },
2350                UserBackendConnection::YubiHsm2 {
2351                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2352                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2353                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2354                    },
2355                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2356                    connections: BTreeSet::from_iter([
2357                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2358                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2359                    ]),
2360                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
2361                        authentication_key_id: "4".parse()?,
2362                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2363                    },
2364                },
2365                UserBackendConnection::YubiHsm2 {
2366                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2367                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2368                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2369                    },
2370                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2371                    connections: BTreeSet::from_iter([
2372                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2373                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2374                    ]),
2375                    mapping: YubiHsm2UserMapping::Signing {
2376                        authentication_key_id: "5".parse()?,
2377                        signing_key_id: "1".parse()?,
2378                        key_setup: SigningKeySetup::new(
2379                            KeyType::Curve25519,
2380                            vec![KeyMechanism::EdDsaSignature],
2381                            None,
2382                            SignatureType::EdDsa,
2383                            CryptographicKeyContext::OpenPgp {
2384                                user_ids: OpenPgpUserIdList::new(vec![
2385                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2386                                ])?,
2387                                version: "v4".parse()?,
2388                            },
2389                        )?,
2390                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2391                        system_user: "yubihsm2-signing-user".parse()?,
2392                        domain: Domain::One,
2393                    }
2394                },
2395            ],
2396        )]
2397        #[case::filter_admin(
2398            UserBackendConnectionFilter::Admin,
2399            vec![
2400                UserBackendConnection::YubiHsm2 {
2401                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2402                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2403                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2404                    },
2405                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2406                    connections: BTreeSet::from_iter([
2407                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2408                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2409                    ]),
2410                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2411                },
2412            ],
2413        )]
2414        #[case::filter_non_admin(
2415            UserBackendConnectionFilter::NonAdmin,
2416            vec![
2417                UserBackendConnection::YubiHsm2 {
2418                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2419                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2420                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2421                    },
2422                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2423                    connections: BTreeSet::from_iter([
2424                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2425                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2426                    ]),
2427                    mapping: YubiHsm2UserMapping::AuditLog {
2428                        authentication_key_id: "3".parse()?,
2429                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2430                        system_user: "yubihsm2-metrics-user".parse()?,
2431                    },
2432                },
2433                UserBackendConnection::YubiHsm2 {
2434                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2435                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2436                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2437                    },
2438                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2439                    connections: BTreeSet::from_iter([
2440                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2441                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2442                    ]),
2443                    mapping: YubiHsm2UserMapping::Backup{
2444                        authentication_key_id: "2".parse()?,
2445                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2446                        system_user: "yubihsm2-backup-user".parse()?,
2447                        wrapping_key_id: "1".parse()?,
2448                    },
2449                },
2450                UserBackendConnection::YubiHsm2 {
2451                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2452                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2453                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2454                    },
2455                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2456                    connections: BTreeSet::from_iter([
2457                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2458                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2459                    ]),
2460                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
2461                        authentication_key_id: "4".parse()?,
2462                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2463                    },
2464                },
2465                UserBackendConnection::YubiHsm2 {
2466                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2467                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2468                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2469                    },
2470                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2471                    connections: BTreeSet::from_iter([
2472                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2473                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2474                    ]),
2475                    mapping: YubiHsm2UserMapping::Signing {
2476                        authentication_key_id: "5".parse()?,
2477                        signing_key_id: "1".parse()?,
2478                        key_setup: SigningKeySetup::new(
2479                            KeyType::Curve25519,
2480                            vec![KeyMechanism::EdDsaSignature],
2481                            None,
2482                            SignatureType::EdDsa,
2483                            CryptographicKeyContext::OpenPgp {
2484                                user_ids: OpenPgpUserIdList::new(vec![
2485                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2486                                ])?,
2487                                version: "v4".parse()?,
2488                            },
2489                        )?,
2490                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2491                        system_user: "yubihsm2-signing-user".parse()?,
2492                        domain: Domain::One,
2493                    }
2494                },
2495            ],
2496        )]
2497        fn config_user_backend_connections(
2498            default_config: TestResult<Config>,
2499            #[case] filter: UserBackendConnectionFilter,
2500            #[case] expected_connections: Vec<UserBackendConnection>,
2501        ) -> TestResult {
2502            let config = default_config?;
2503
2504            assert_eq!(
2505                expected_connections,
2506                config.user_backend_connections(filter)
2507            );
2508
2509            Ok(())
2510        }
2511
2512        /// Ensures, that a [`Config`] object leads to a specific YAML output.
2513        ///
2514        /// In this particular case, a [`SystemConfig`] and a [`YubiHsm2Config`] object are present.
2515        #[rstest]
2516        fn config_to_yaml_string(
2517            default_system_config: TestResult<SystemConfig>,
2518            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2519        ) -> TestResult {
2520            let config = ConfigBuilder::new(default_system_config?)
2521                .set_yubihsm2_config(default_yubihsm2_config?)
2522                .finish()?;
2523            let config_str = config.to_yaml_string()?;
2524
2525            with_settings!({
2526                description => "Configuration with system-wide and YubiHSM2 configuration",
2527                snapshot_path => SNAPSHOT_PATH,
2528                prepend_module_to_snapshot => false,
2529            }, {
2530                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
2531            });
2532
2533            Ok(())
2534        }
2535
2536        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
2537        /// correctly.
2538        #[rstest]
2539        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
2540            let config = default_config?;
2541            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
2542                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
2543                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2544                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2545                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2546                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2547                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2548                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2549            ]);
2550
2551            assert_eq!(
2552                config.authorized_key_entries(),
2553                expected.iter().collect::<HashSet<_>>()
2554            );
2555            Ok(())
2556        }
2557
2558        /// Ensures, that [`Config::system_user_data`] returns [`SystemUserData`] entries correctly.
2559        #[rstest]
2560        fn config_system_user_data(
2561            default_config: TestResult<Config>,
2562            raw_user_data: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2563        ) -> TestResult {
2564            let config = default_config?;
2565            let raw_user_data = raw_user_data?;
2566            let expected: HashSet<SystemUserData> = HashSet::from_iter([
2567                SystemUserData::HostShareholder {
2568                    system_user: &raw_user_data[0].0,
2569                    ssh_authorized_key: raw_user_data[0]
2570                        .1
2571                        .as_ref()
2572                        .expect("to have SSH authorized key"),
2573                },
2574                SystemUserData::HostShareholder {
2575                    system_user: &raw_user_data[1].0,
2576                    ssh_authorized_key: raw_user_data[1]
2577                        .1
2578                        .as_ref()
2579                        .expect("to have SSH authorized key"),
2580                },
2581                SystemUserData::HostShareholder {
2582                    system_user: &raw_user_data[2].0,
2583                    ssh_authorized_key: raw_user_data[2]
2584                        .1
2585                        .as_ref()
2586                        .expect("to have SSH authorized key"),
2587                },
2588                SystemUserData::HostDownloadNetworkConfig {
2589                    system_user: &raw_user_data[3].0,
2590                    ssh_authorized_key: raw_user_data[3]
2591                        .1
2592                        .as_ref()
2593                        .expect("to have SSH authorized key"),
2594                },
2595                SystemUserData::BackendAdmin {
2596                    system_user: raw_user_data[4].0.clone(),
2597                },
2598                SystemUserData::BackendMetrics {
2599                    system_user: &raw_user_data[5].0,
2600                    ssh_authorized_key: raw_user_data[5]
2601                        .1
2602                        .as_ref()
2603                        .expect("to have SSH authorized key"),
2604                },
2605                SystemUserData::BackendBackup {
2606                    system_user: &raw_user_data[6].0,
2607                    ssh_authorized_key: raw_user_data[6]
2608                        .1
2609                        .as_ref()
2610                        .expect("to have SSH authorized key"),
2611                },
2612                SystemUserData::BackendHermeticMetrics {
2613                    system_user: &raw_user_data[7].0,
2614                },
2615                SystemUserData::BackendSign {
2616                    system_user: &raw_user_data[8].0,
2617                    ssh_authorized_key: raw_user_data[8]
2618                        .1
2619                        .as_ref()
2620                        .expect("to have SSH authorized key"),
2621                },
2622            ]);
2623
2624            assert_eq!(config.system_user_data(), expected);
2625            Ok(())
2626        }
2627
2628        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
2629        #[rstest]
2630        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
2631            let config = default_config?;
2632            let expected: HashSet<SystemUserId> = HashSet::from_iter([
2633                "share-holder1".parse()?,
2634                "share-holder2".parse()?,
2635                "share-holder3".parse()?,
2636                "wireguard-downloader".parse()?,
2637                "yubihsm2-metrics-user".parse()?,
2638                "yubihsm2-backup-user".parse()?,
2639                "yubihsm2-hermetic-metrics-user".parse()?,
2640                "yubihsm2-signing-user".parse()?,
2641            ]);
2642
2643            assert_eq!(
2644                config.system_user_ids(),
2645                expected.iter().collect::<HashSet<_>>()
2646            );
2647            Ok(())
2648        }
2649
2650        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
2651        /// the same YAML string.
2652        ///
2653        /// The configuration file describes a [`SystemConfig`] and a [`YubiHsm2Config`] object.
2654        #[rstest]
2655        #[cfg(not(feature = "_yubihsm2-mockhsm"))]
2656        fn roundtrip_yaml_config(
2657            #[files("../fixtures/config/yubihsm2_backend/*.yaml")] path: PathBuf,
2658        ) -> TestResult {
2659            let config_string = read_to_string(&path)?;
2660            let config = Config::from_file_path(&path)?;
2661
2662            assert_eq!(config.to_yaml_string()?, config_string);
2663
2664            Ok(())
2665        }
2666
2667        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
2668        /// the same YAML string.
2669        ///
2670        /// The configuration file describes a [`SystemConfig`] and a [`YubiHsm2Config`] object.
2671        #[rstest]
2672        #[cfg(feature = "_yubihsm2-mockhsm")]
2673        fn roundtrip_yaml_config_mockhsm(
2674            #[files("../fixtures/config/yubihsm2_mockhsm_backend/*.yaml")] path: PathBuf,
2675        ) -> TestResult {
2676            let config_string = read_to_string(&path)?;
2677            let config = Config::from_file_path(&path)?;
2678
2679            assert_eq!(config.to_yaml_string()?, config_string);
2680
2681            Ok(())
2682        }
2683
2684        /// Ensures, that [`AdministrativeSecretHandling`] and
2685        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
2686        /// [`UserBackendConnection`].
2687        #[rstest]
2688        fn user_backend_connection_secret_handling(
2689            default_config: TestResult<Config>,
2690        ) -> TestResult {
2691            let config = default_config?;
2692            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
2693                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2694                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2695            };
2696            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
2697
2698            let user_backend_connection = config
2699                .user_backend_connection(&"yubihsm2-signing-user".parse()?)
2700                .expect("there to be a mapping of the requested name");
2701
2702            assert_eq!(
2703                user_backend_connection.admin_secret_handling(),
2704                admin_secret_handling
2705            );
2706            assert_eq!(
2707                user_backend_connection.non_admin_secret_handling(),
2708                non_admin_secret_handling
2709            );
2710
2711            Ok(())
2712        }
2713
2714        /// Ensures, that [`SystemUserConfigState`] can be created from [`Config`].
2715        #[rstest]
2716        fn system_user_config_state_from_config(default_config: TestResult<Config>) -> TestResult {
2717            let config = default_config?;
2718            let state = SystemUserConfigState::from(&config);
2719
2720            assert_eq!(state.system_user_data, config.system_user_data(),);
2721            Ok(())
2722        }
2723    }
2724
2725    /// Tests, that are only available when using all available backends.
2726    #[cfg(all(feature = "nethsm", feature = "yubihsm2"))]
2727    mod all_backends {
2728        use pretty_assertions::assert_eq;
2729
2730        use super::*;
2731        use crate::config::{
2732            MappingAuthorizedKeyEntry,
2733            MappingSystemUserId,
2734            SystemUserData,
2735            traits::ConfigSystemUserData,
2736        };
2737
2738        /// Creates a default [`Config`] for testing purposes.
2739        #[fixture]
2740        fn default_config(
2741            default_system_config: TestResult<SystemConfig>,
2742            default_nethsm_config: TestResult<NetHsmConfig>,
2743            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2744        ) -> TestResult<Config> {
2745            Ok(ConfigBuilder::new(default_system_config?)
2746                .set_nethsm_config(default_nethsm_config?)
2747                .set_yubihsm2_config(default_yubihsm2_config?)
2748                .finish()?)
2749        }
2750
2751        /// List of raw data required to create [`SystemUserData`] for each item in the default
2752        /// [`Config`].
2753        #[fixture]
2754        fn raw_user_data(
2755            raw_user_data_system: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2756            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2757            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2758        ) -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
2759            let mut data = raw_user_data_system?;
2760            data.extend(raw_user_data_nethsm?);
2761            data.extend(raw_user_data_yubihsm2?);
2762            Ok(data)
2763        }
2764
2765        /// Ensures that [`MappingSystemUserId`] for [`UserBackendConnection`] works as intended.
2766        #[rstest]
2767        fn user_backend_connection_system_user_id(
2768            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2769            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2770        ) -> TestResult {
2771            let raw_user_data_nethsm = raw_user_data_nethsm?;
2772            let data = UserBackendConnection::NetHsm {
2773                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
2774                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
2775                connections: BTreeSet::from_iter([Connection::new(
2776                    "https://nethsm1.example.org/".parse()?,
2777                    ConnectionSecurity::Unsafe,
2778                )]),
2779                mapping: NetHsmUserMapping::Backup {
2780                    backend_user: "backup".parse()?,
2781                    ssh_authorized_key: raw_user_data_nethsm[1]
2782                        .1
2783                        .clone()
2784                        .expect("to have an SSH authorized key"),
2785                    system_user: raw_user_data_nethsm[1].0.clone(),
2786                },
2787            };
2788            assert_eq!(data.system_user_id(), Some(&raw_user_data_nethsm[1].0));
2789
2790            let raw_user_data_yubihsm2 = raw_user_data_yubihsm2?;
2791            let data = UserBackendConnection::YubiHsm2 {
2792                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
2793                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
2794                connections: BTreeSet::from_iter([YubiHsm2Connection::Usb {
2795                    serial_number: "0123456789".parse()?,
2796                }]),
2797                mapping: YubiHsm2UserMapping::AuditLog {
2798                    authentication_key_id: "1".parse()?,
2799                    ssh_authorized_key: raw_user_data_yubihsm2[1]
2800                        .1
2801                        .clone()
2802                        .expect("to have an SSH authorized key"),
2803                    system_user: raw_user_data_yubihsm2[1].0.clone(),
2804                },
2805            };
2806            assert_eq!(data.system_user_id(), Some(&raw_user_data_yubihsm2[1].0));
2807
2808            Ok(())
2809        }
2810
2811        /// Ensures that [`MappingAuthorizedKeyEntry`] for [`UserBackendConnection`] works as
2812        /// intended.
2813        #[rstest]
2814        fn user_backend_connection_authorized_key_entry(
2815            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2816            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2817        ) -> TestResult {
2818            let raw_user_data_nethsm = raw_user_data_nethsm?;
2819            let data = UserBackendConnection::NetHsm {
2820                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
2821                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
2822                connections: BTreeSet::from_iter([Connection::new(
2823                    "https://nethsm1.example.org/".parse()?,
2824                    ConnectionSecurity::Unsafe,
2825                )]),
2826                mapping: NetHsmUserMapping::Backup {
2827                    backend_user: "backup".parse()?,
2828                    ssh_authorized_key: raw_user_data_nethsm[1]
2829                        .1
2830                        .clone()
2831                        .expect("to have an SSH authorized key"),
2832                    system_user: raw_user_data_nethsm[1].0.clone(),
2833                },
2834            };
2835            assert_eq!(
2836                data.authorized_key_entry(),
2837                Some(
2838                    raw_user_data_nethsm[1]
2839                        .1
2840                        .as_ref()
2841                        .expect("to have an SSH authorized key")
2842                )
2843            );
2844
2845            let raw_user_data_yubihsm2 = raw_user_data_yubihsm2?;
2846            let data = UserBackendConnection::YubiHsm2 {
2847                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
2848                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
2849                connections: BTreeSet::from_iter([YubiHsm2Connection::Usb {
2850                    serial_number: "0123456789".parse()?,
2851                }]),
2852                mapping: YubiHsm2UserMapping::AuditLog {
2853                    authentication_key_id: "1".parse()?,
2854                    ssh_authorized_key: raw_user_data_yubihsm2[1]
2855                        .1
2856                        .clone()
2857                        .expect("to have an SSH authorized key"),
2858                    system_user: raw_user_data_yubihsm2[1].0.clone(),
2859                },
2860            };
2861            assert_eq!(
2862                data.authorized_key_entry(),
2863                Some(
2864                    raw_user_data_yubihsm2[1]
2865                        .1
2866                        .as_ref()
2867                        .expect("to have an SSH authorized key")
2868                )
2869            );
2870
2871            Ok(())
2872        }
2873
2874        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
2875        /// configuration components.
2876        ///
2877        /// Here, custom [`NetHsmConfig`] and [`YubiHsm2Config`] objects are staged together with a
2878        /// default [`SystemConfig`] (created by [`default_system_config`]) to create a failure
2879        /// scenario.
2880        #[rstest]
2881        #[case::backend_overlap_duplicate_system_users_two_duplicate_ssh_public_keys(
2882            "Configuration with system-wide, NetHSM and YubiHSM2 configuration has two duplicate system users and two duplicate SSH public keys in the backends",
2883            NetHsmConfig::new(
2884                BTreeSet::from_iter([
2885                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2886                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2887                ]),
2888                BTreeSet::from_iter([
2889                    NetHsmUserMapping::Admin("admin".parse()?),
2890                    NetHsmUserMapping::Backup{
2891                        backend_user: "backup".parse()?,
2892                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2893                        system_user: "backup-user".parse()?,
2894                    },
2895                    NetHsmUserMapping::HermeticMetrics {
2896                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2897                        system_user: "nethsm-hermetic-metrics-user".parse()?,
2898                    },
2899                    NetHsmUserMapping::Metrics {
2900                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2901                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2902                        system_user: "metrics-user".parse()?,
2903                    },
2904                    NetHsmUserMapping::Signing {
2905                        backend_user: "signing".parse()?,
2906                        signing_key_id: "signing1".parse()?,
2907                        key_setup: SigningKeySetup::new(
2908                            KeyType::Curve25519,
2909                            vec![KeyMechanism::EdDsaSignature],
2910                            None,
2911                            SignatureType::EdDsa,
2912                            CryptographicKeyContext::OpenPgp {
2913                                user_ids: OpenPgpUserIdList::new(vec![
2914                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2915                                ])?,
2916                                version: "v4".parse()?,
2917                            },
2918                        )?,
2919                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
2920                        system_user: "nethsm-signing-user".parse()?,
2921                        tag: "nethsm-signing1".to_string(),
2922                    }
2923                ]),
2924            )?,
2925            YubiHsm2Config::new(
2926                BTreeSet::from_iter([
2927                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2928                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2929                ]),
2930                BTreeSet::from_iter([
2931                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2932                    YubiHsm2UserMapping::AuditLog {
2933                        authentication_key_id: "3".parse()?,
2934                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2935                        system_user: "metrics-user".parse()?,
2936                    },
2937                    YubiHsm2UserMapping::Backup {
2938                        authentication_key_id: "2".parse()?,
2939                        wrapping_key_id: "2".parse()?,
2940                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2941                        system_user: "backup-user".parse()?,
2942                    },
2943                    YubiHsm2UserMapping::HermeticAuditLog {
2944                        authentication_key_id: "4".parse()?,
2945                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2946                    },
2947                    YubiHsm2UserMapping::Signing {
2948                        authentication_key_id: "5".parse()?,
2949                        key_setup: SigningKeySetup::new(
2950                            KeyType::Curve25519,
2951                            vec![KeyMechanism::EdDsaSignature],
2952                            None,
2953                            SignatureType::EdDsa,
2954                            CryptographicKeyContext::OpenPgp {
2955                                user_ids: OpenPgpUserIdList::new(vec![
2956                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2957                                ])?,
2958                                version: "v4".parse()?,
2959                            },
2960                        )?,
2961                        signing_key_id: "1".parse()?,
2962                        domain: Domain::One,
2963                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2964                        system_user: "yubihsm2-signing-user".parse()? }
2965                ]),
2966            )?,
2967        )]
2968        #[case::backend_overlap_one_duplicate_system_user(
2969            "Configuration with system-wide, NetHSM and YubiHSM2 configuration has one duplicate system user in the backends",
2970            NetHsmConfig::new(
2971                BTreeSet::from_iter([
2972                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2973                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2974                ]),
2975                BTreeSet::from_iter([
2976                    NetHsmUserMapping::Admin("admin".parse()?),
2977                    NetHsmUserMapping::Backup{
2978                        backend_user: "backup".parse()?,
2979                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2980                        system_user: "backup-user".parse()?,
2981                    },
2982                    NetHsmUserMapping::HermeticMetrics {
2983                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2984                        system_user: "nethsm-hermetic-metrics-user".parse()?,
2985                    },
2986                    NetHsmUserMapping::Metrics {
2987                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2988                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2989                        system_user: "nethsm-metrics-user".parse()?,
2990                    },
2991                    NetHsmUserMapping::Signing {
2992                        backend_user: "signing".parse()?,
2993                        signing_key_id: "signing1".parse()?,
2994                        key_setup: SigningKeySetup::new(
2995                            KeyType::Curve25519,
2996                            vec![KeyMechanism::EdDsaSignature],
2997                            None,
2998                            SignatureType::EdDsa,
2999                            CryptographicKeyContext::OpenPgp {
3000                                user_ids: OpenPgpUserIdList::new(vec![
3001                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3002                                ])?,
3003                                version: "v4".parse()?,
3004                            },
3005                        )?,
3006                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3007                        system_user: "nethsm-signing-user".parse()?,
3008                        tag: "nethsm-signing1".to_string(),
3009                    }
3010                ]),
3011            )?,
3012            YubiHsm2Config::new(
3013                BTreeSet::from_iter([
3014                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3015                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3016                ]),
3017                BTreeSet::from_iter([
3018                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
3019                    YubiHsm2UserMapping::AuditLog {
3020                        authentication_key_id: "3".parse()?,
3021                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3022                        system_user: "yubihsm2-metrics-user".parse()?,
3023                    },
3024                    YubiHsm2UserMapping::Backup {
3025                        authentication_key_id: "2".parse()?,
3026                        wrapping_key_id: "2".parse()?,
3027                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3028                        system_user: "backup-user".parse()?,
3029                    },
3030                    YubiHsm2UserMapping::HermeticAuditLog {
3031                        authentication_key_id: "4".parse()?,
3032                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
3033                    },
3034                    YubiHsm2UserMapping::Signing {
3035                        authentication_key_id: "5".parse()?,
3036                        key_setup: SigningKeySetup::new(
3037                            KeyType::Curve25519,
3038                            vec![KeyMechanism::EdDsaSignature],
3039                            None,
3040                            SignatureType::EdDsa,
3041                            CryptographicKeyContext::OpenPgp {
3042                                user_ids: OpenPgpUserIdList::new(vec![
3043                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3044                                ])?,
3045                                version: "v4".parse()?,
3046                            },
3047                        )?,
3048                        signing_key_id: "1".parse()?,
3049                        domain: Domain::One,
3050                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3051                        system_user: "yubihsm2-signing-user".parse()? }
3052                ]),
3053            )?,
3054        )]
3055        #[case::system_overlap_duplicate_system_users_two_duplicate_ssh_public_keys(
3056            "Configuration with system-wide, NetHSM and YubiHSM2 configuration has two duplicate system users and two duplicate SSH public keys in the system and the backends",
3057            NetHsmConfig::new(
3058                BTreeSet::from_iter([
3059                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3060                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3061                ]),
3062                BTreeSet::from_iter([
3063                    NetHsmUserMapping::Admin("admin".parse()?),
3064                    NetHsmUserMapping::Backup{
3065                        backend_user: "backup".parse()?,
3066                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
3067                        system_user: "share-holder1".parse()?,
3068                    },
3069                    NetHsmUserMapping::Metrics {
3070                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
3071                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3072                        system_user: "share-holder2".parse()?,
3073                    },
3074                    NetHsmUserMapping::HermeticMetrics {
3075                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
3076                        system_user: "nethsm-hermetic-metrics-user".parse()?,
3077                    },
3078                    NetHsmUserMapping::Signing {
3079                        backend_user: "signing".parse()?,
3080                        signing_key_id: "signing1".parse()?,
3081                        key_setup: SigningKeySetup::new(
3082                            KeyType::Curve25519,
3083                            vec![KeyMechanism::EdDsaSignature],
3084                            None,
3085                            SignatureType::EdDsa,
3086                            CryptographicKeyContext::OpenPgp {
3087                                user_ids: OpenPgpUserIdList::new(vec![
3088                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3089                                ])?,
3090                                version: "v4".parse()?,
3091                            },
3092                        )?,
3093                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3094                        system_user: "nethsm-signing-user".parse()?,
3095                        tag: "nethsm-signing1".to_string(),
3096                    }
3097                ]),
3098            )?,
3099            YubiHsm2Config::new(
3100                BTreeSet::from_iter([
3101                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3102                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3103                ]),
3104                BTreeSet::from_iter([
3105                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
3106                    YubiHsm2UserMapping::Backup {
3107                        authentication_key_id: "2".parse()?,
3108                        wrapping_key_id: "2".parse()?,
3109                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
3110                        system_user: "share-holder1".parse()?,
3111                    },
3112                    YubiHsm2UserMapping::AuditLog {
3113                        authentication_key_id: "3".parse()?,
3114                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3115                        system_user: "share-holder2".parse()?,
3116                    },
3117                    YubiHsm2UserMapping::HermeticAuditLog {
3118                        authentication_key_id: "4".parse()?,
3119                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
3120                    },
3121                    YubiHsm2UserMapping::Signing {
3122                        authentication_key_id: "5".parse()?,
3123                        key_setup: SigningKeySetup::new(
3124                            KeyType::Curve25519,
3125                            vec![KeyMechanism::EdDsaSignature],
3126                            None,
3127                            SignatureType::EdDsa,
3128                            CryptographicKeyContext::OpenPgp {
3129                                user_ids: OpenPgpUserIdList::new(vec![
3130                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3131                                ])?,
3132                                version: "v4".parse()?,
3133                            },
3134                        )?,
3135                        signing_key_id: "1".parse()?,
3136                        domain: Domain::One,
3137                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3138                        system_user: "yubihsm2-signing-user".parse()? }
3139                ]),
3140            )?,
3141        )]
3142        fn config_fails_validation(
3143            default_system_config: TestResult<SystemConfig>,
3144            #[case] description: &str,
3145            #[case] nethsm_config: NetHsmConfig,
3146            #[case] yubihsm2_config: YubiHsm2Config,
3147        ) -> TestResult {
3148            let error_message = match ConfigBuilder::new(default_system_config?)
3149                .set_nethsm_config(nethsm_config)
3150                .set_yubihsm2_config(yubihsm2_config)
3151                .finish()
3152            {
3153                Err(error) => error.to_string(),
3154                Ok(config) => panic!(
3155                    "Expected to fail with Error::Validation, but succeeded instead: {}",
3156                    config.to_yaml_string()?
3157                ),
3158            };
3159
3160            with_settings!({
3161                description => description,
3162                snapshot_path => SNAPSHOT_PATH,
3163                prepend_module_to_snapshot => false,
3164            }, {
3165                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
3166            });
3167
3168            Ok(())
3169        }
3170
3171        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
3172        #[rstest]
3173        #[case::nethsm_signing(
3174            "nethsm-signing-user",
3175            Some(UserBackendConnection::NetHsm {
3176                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3177                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3178                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3179                },
3180                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3181                connections: BTreeSet::from_iter([
3182                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3183                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3184                ]),
3185                mapping: NetHsmUserMapping::Signing {
3186                    backend_user: "signing".parse()?,
3187                    signing_key_id: "signing1".parse()?,
3188                    key_setup: SigningKeySetup::new(
3189                        KeyType::Curve25519,
3190                        vec![KeyMechanism::EdDsaSignature],
3191                        None,
3192                        SignatureType::EdDsa,
3193                        CryptographicKeyContext::OpenPgp {
3194                            user_ids: OpenPgpUserIdList::new(vec![
3195                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3196                            ])?,
3197                            version: "v4".parse()?,
3198                        },
3199                    )?,
3200                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3201                    system_user: "nethsm-signing-user".parse()?,
3202                    tag: "signing1".to_string(),
3203                }
3204            })
3205        )]
3206        #[case::yubihsm2_signing(
3207            "yubihsm2-signing-user",
3208            Some(UserBackendConnection::YubiHsm2 {
3209                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3210                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3211                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3212                },
3213                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3214                connections: BTreeSet::from_iter([
3215                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3216                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3217                ]),
3218                mapping: YubiHsm2UserMapping::Signing {
3219                    authentication_key_id: "5".parse()?,
3220                    signing_key_id: "1".parse()?,
3221                    key_setup: SigningKeySetup::new(
3222                        KeyType::Curve25519,
3223                        vec![KeyMechanism::EdDsaSignature],
3224                        None,
3225                        SignatureType::EdDsa,
3226                        CryptographicKeyContext::OpenPgp {
3227                            user_ids: OpenPgpUserIdList::new(vec![
3228                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3229                            ])?,
3230                            version: "v4".parse()?,
3231                        },
3232                    )?,
3233                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3234                    system_user: "yubihsm2-signing-user".parse()?,
3235                    domain: Domain::One,
3236                }
3237            })
3238        )]
3239        #[case::none("foo", None)]
3240        fn config_user_backend_connection(
3241            default_config: TestResult<Config>,
3242            #[case] system_user: &str,
3243            #[case] expected_connection: Option<UserBackendConnection>,
3244        ) -> TestResult {
3245            let config = default_config?;
3246            assert_eq!(
3247                expected_connection,
3248                config.user_backend_connection(&system_user.parse()?)
3249            );
3250
3251            Ok(())
3252        }
3253
3254        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
3255        /// [`UserBackendConnection`] items according to a [`UserBackendConnectionFilter`].
3256        #[rstest]
3257        #[case::filter_all(
3258            UserBackendConnectionFilter::All,
3259            vec![
3260                UserBackendConnection::NetHsm {
3261                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3262                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3263                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3264                    },
3265                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3266                    connections: BTreeSet::from_iter([
3267                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3268                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3269                    ]),
3270                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
3271                },
3272                UserBackendConnection::NetHsm {
3273                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3274                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3275                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3276                    },
3277                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3278                    connections: BTreeSet::from_iter([
3279                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3280                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3281                    ]),
3282                    mapping: NetHsmUserMapping::Backup{
3283                        backend_user: "backup".parse()?,
3284                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
3285                        system_user: "nethsm-backup-user".parse()?,
3286                    }
3287                },
3288                UserBackendConnection::NetHsm {
3289                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3290                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3291                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3292                    },
3293                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3294                    connections: BTreeSet::from_iter([
3295                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3296                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3297                    ]),
3298                    mapping: NetHsmUserMapping::HermeticMetrics {
3299                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
3300                        system_user: "nethsm-hermetic-metrics-user".parse()?,
3301                    }
3302                },
3303                UserBackendConnection::NetHsm {
3304                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3305                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3306                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3307                    },
3308                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3309                    connections: BTreeSet::from_iter([
3310                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3311                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3312                    ]),
3313                    mapping: NetHsmUserMapping::Metrics {
3314                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
3315                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
3316                        system_user: "nethsm-metrics-user".parse()?,
3317                    }
3318                },
3319                UserBackendConnection::NetHsm {
3320                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3321                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3322                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3323                    },
3324                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3325                    connections: BTreeSet::from_iter([
3326                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3327                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3328                    ]),
3329                    mapping: NetHsmUserMapping::Signing {
3330                        backend_user: "signing".parse()?,
3331                        signing_key_id: "signing1".parse()?,
3332                        key_setup: SigningKeySetup::new(
3333                            KeyType::Curve25519,
3334                            vec![KeyMechanism::EdDsaSignature],
3335                            None,
3336                            SignatureType::EdDsa,
3337                            CryptographicKeyContext::OpenPgp {
3338                                user_ids: OpenPgpUserIdList::new(vec![
3339                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3340                                ])?,
3341                                version: "v4".parse()?,
3342                            },
3343                        )?,
3344                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3345                        system_user: "nethsm-signing-user".parse()?,
3346                        tag: "signing1".to_string(),
3347                    }
3348                },
3349                UserBackendConnection::YubiHsm2 {
3350                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3351                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3352                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3353                    },
3354                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3355                    connections: BTreeSet::from_iter([
3356                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3357                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3358                    ]),
3359                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
3360                },
3361                UserBackendConnection::YubiHsm2 {
3362                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3363                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3364                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3365                    },
3366                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3367                    connections: BTreeSet::from_iter([
3368                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3369                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3370                    ]),
3371                    mapping: YubiHsm2UserMapping::AuditLog {
3372                        authentication_key_id: "3".parse()?,
3373                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3374                        system_user: "yubihsm2-metrics-user".parse()?,
3375                    },
3376                },
3377                UserBackendConnection::YubiHsm2 {
3378                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3379                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3380                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3381                    },
3382                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3383                    connections: BTreeSet::from_iter([
3384                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3385                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3386                    ]),
3387                    mapping: YubiHsm2UserMapping::Backup{
3388                        authentication_key_id: "2".parse()?,
3389                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3390                        system_user: "yubihsm2-backup-user".parse()?,
3391                        wrapping_key_id: "1".parse()?,
3392                    },
3393                },
3394                UserBackendConnection::YubiHsm2 {
3395                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3396                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3397                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3398                    },
3399                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3400                    connections: BTreeSet::from_iter([
3401                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3402                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3403                    ]),
3404                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
3405                        authentication_key_id: "4".parse()?,
3406                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
3407                    },
3408                },
3409                UserBackendConnection::YubiHsm2 {
3410                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3411                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3412                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3413                    },
3414                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3415                    connections: BTreeSet::from_iter([
3416                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3417                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3418                    ]),
3419                    mapping: YubiHsm2UserMapping::Signing {
3420                        authentication_key_id: "5".parse()?,
3421                        signing_key_id: "1".parse()?,
3422                        key_setup: SigningKeySetup::new(
3423                            KeyType::Curve25519,
3424                            vec![KeyMechanism::EdDsaSignature],
3425                            None,
3426                            SignatureType::EdDsa,
3427                            CryptographicKeyContext::OpenPgp {
3428                                user_ids: OpenPgpUserIdList::new(vec![
3429                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3430                                ])?,
3431                                version: "v4".parse()?,
3432                            },
3433                        )?,
3434                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3435                        system_user: "yubihsm2-signing-user".parse()?,
3436                        domain: Domain::One,
3437                    }
3438                },
3439            ],
3440        )]
3441        #[case::filter_admin(
3442            UserBackendConnectionFilter::Admin,
3443            vec![
3444                UserBackendConnection::NetHsm {
3445                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3446                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3447                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3448                    },
3449                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3450                    connections: BTreeSet::from_iter([
3451                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3452                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3453                    ]),
3454                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
3455                },
3456                UserBackendConnection::YubiHsm2 {
3457                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3458                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3459                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3460                    },
3461                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3462                    connections: BTreeSet::from_iter([
3463                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3464                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3465                    ]),
3466                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
3467                },
3468            ],
3469        )]
3470        #[case::filter_non_admin(
3471            UserBackendConnectionFilter::NonAdmin,
3472            vec![
3473                UserBackendConnection::NetHsm {
3474                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3475                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3476                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3477                    },
3478                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3479                    connections: BTreeSet::from_iter([
3480                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3481                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3482                    ]),
3483                    mapping: NetHsmUserMapping::Backup{
3484                        backend_user: "backup".parse()?,
3485                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
3486                        system_user: "nethsm-backup-user".parse()?,
3487                    }
3488                },
3489                UserBackendConnection::NetHsm {
3490                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3491                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3492                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3493                    },
3494                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3495                    connections: BTreeSet::from_iter([
3496                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3497                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3498                    ]),
3499                    mapping: NetHsmUserMapping::HermeticMetrics {
3500                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
3501                        system_user: "nethsm-hermetic-metrics-user".parse()?,
3502                    }
3503                },
3504                UserBackendConnection::NetHsm {
3505                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3506                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3507                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3508                    },
3509                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3510                    connections: BTreeSet::from_iter([
3511                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3512                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3513                    ]),
3514                    mapping: NetHsmUserMapping::Metrics {
3515                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
3516                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
3517                        system_user: "nethsm-metrics-user".parse()?,
3518                    }
3519                },
3520                UserBackendConnection::NetHsm {
3521                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3522                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3523                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3524                    },
3525                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3526                    connections: BTreeSet::from_iter([
3527                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3528                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3529                    ]),
3530                    mapping: NetHsmUserMapping::Signing {
3531                        backend_user: "signing".parse()?,
3532                        signing_key_id: "signing1".parse()?,
3533                        key_setup: SigningKeySetup::new(
3534                            KeyType::Curve25519,
3535                            vec![KeyMechanism::EdDsaSignature],
3536                            None,
3537                            SignatureType::EdDsa,
3538                            CryptographicKeyContext::OpenPgp {
3539                                user_ids: OpenPgpUserIdList::new(vec![
3540                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3541                                ])?,
3542                                version: "v4".parse()?,
3543                            },
3544                        )?,
3545                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3546                        system_user: "nethsm-signing-user".parse()?,
3547                        tag: "signing1".to_string(),
3548                    }
3549                },
3550                UserBackendConnection::YubiHsm2 {
3551                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3552                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3553                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3554                    },
3555                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3556                    connections: BTreeSet::from_iter([
3557                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3558                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3559                    ]),
3560                    mapping: YubiHsm2UserMapping::AuditLog {
3561                        authentication_key_id: "3".parse()?,
3562                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3563                        system_user: "yubihsm2-metrics-user".parse()?,
3564                    },
3565                },
3566                UserBackendConnection::YubiHsm2 {
3567                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3568                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3569                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3570                    },
3571                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3572                    connections: BTreeSet::from_iter([
3573                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3574                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3575                    ]),
3576                    mapping: YubiHsm2UserMapping::Backup{
3577                        authentication_key_id: "2".parse()?,
3578                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3579                        system_user: "yubihsm2-backup-user".parse()?,
3580                        wrapping_key_id: "1".parse()?,
3581                    },
3582                },
3583                UserBackendConnection::YubiHsm2 {
3584                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3585                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3586                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3587                    },
3588                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3589                    connections: BTreeSet::from_iter([
3590                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3591                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3592                    ]),
3593                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
3594                        authentication_key_id: "4".parse()?,
3595                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
3596                    },
3597                },
3598                UserBackendConnection::YubiHsm2 {
3599                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3600                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3601                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3602                    },
3603                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3604                    connections: BTreeSet::from_iter([
3605                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3606                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3607                    ]),
3608                    mapping: YubiHsm2UserMapping::Signing {
3609                        authentication_key_id: "5".parse()?,
3610                        signing_key_id: "1".parse()?,
3611                        key_setup: SigningKeySetup::new(
3612                            KeyType::Curve25519,
3613                            vec![KeyMechanism::EdDsaSignature],
3614                            None,
3615                            SignatureType::EdDsa,
3616                            CryptographicKeyContext::OpenPgp {
3617                                user_ids: OpenPgpUserIdList::new(vec![
3618                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3619                                ])?,
3620                                version: "v4".parse()?,
3621                            },
3622                        )?,
3623                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3624                        system_user: "yubihsm2-signing-user".parse()?,
3625                        domain: Domain::One,
3626                    }
3627                },
3628            ],
3629        )]
3630        fn config_user_backend_connections(
3631            default_config: TestResult<Config>,
3632            #[case] filter: UserBackendConnectionFilter,
3633            #[case] expected_connections: Vec<UserBackendConnection>,
3634        ) -> TestResult {
3635            let config = default_config?;
3636
3637            assert_eq!(
3638                expected_connections,
3639                config.user_backend_connections(filter)
3640            );
3641
3642            Ok(())
3643        }
3644
3645        /// Ensures, that a [`Config`] object leads to a specific YAML output.
3646        ///
3647        /// In this particular case, a [`SystemConfig`], a [`NetHsmConfig`] and a [`YubiHsm2Config`]
3648        /// object are present.
3649        #[rstest]
3650        fn config_to_yaml_string(
3651            default_system_config: TestResult<SystemConfig>,
3652            default_nethsm_config: TestResult<NetHsmConfig>,
3653            default_yubihsm2_config: TestResult<YubiHsm2Config>,
3654        ) -> TestResult {
3655            let config = ConfigBuilder::new(default_system_config?)
3656                .set_nethsm_config(default_nethsm_config?)
3657                .set_yubihsm2_config(default_yubihsm2_config?)
3658                .finish()?;
3659            let config_str = config.to_yaml_string()?;
3660
3661            with_settings!({
3662                description => "Configuration with system-wide, NetHSM and YubiHSM2 configuration",
3663                snapshot_path => SNAPSHOT_PATH,
3664                prepend_module_to_snapshot => false,
3665            }, {
3666                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
3667            });
3668
3669            Ok(())
3670        }
3671
3672        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
3673        /// correctly.
3674        #[rstest]
3675        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
3676            let config = default_config?;
3677            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
3678                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
3679                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3680                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
3681                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
3682                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
3683                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
3684                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3685                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3686                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3687                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3688            ]);
3689
3690            assert_eq!(
3691                config.authorized_key_entries(),
3692                expected.iter().collect::<HashSet<_>>()
3693            );
3694            Ok(())
3695        }
3696
3697        /// Ensures, that [`Config::system_user_data`] returns [`SystemUserData`] entries correctly.
3698        #[rstest]
3699        fn config_system_user_data(
3700            default_config: TestResult<Config>,
3701            raw_user_data: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
3702        ) -> TestResult {
3703            let config = default_config?;
3704            let raw_user_data = raw_user_data?;
3705            let expected: HashSet<SystemUserData> = HashSet::from_iter([
3706                SystemUserData::HostShareholder {
3707                    system_user: &raw_user_data[0].0,
3708                    ssh_authorized_key: raw_user_data[0]
3709                        .1
3710                        .as_ref()
3711                        .expect("to have SSH authorized key"),
3712                },
3713                SystemUserData::HostShareholder {
3714                    system_user: &raw_user_data[1].0,
3715                    ssh_authorized_key: raw_user_data[1]
3716                        .1
3717                        .as_ref()
3718                        .expect("to have SSH authorized key"),
3719                },
3720                SystemUserData::HostShareholder {
3721                    system_user: &raw_user_data[2].0,
3722                    ssh_authorized_key: raw_user_data[2]
3723                        .1
3724                        .as_ref()
3725                        .expect("to have SSH authorized key"),
3726                },
3727                SystemUserData::HostDownloadNetworkConfig {
3728                    system_user: &raw_user_data[3].0,
3729                    ssh_authorized_key: raw_user_data[3]
3730                        .1
3731                        .as_ref()
3732                        .expect("to have SSH authorized key"),
3733                },
3734                SystemUserData::BackendAdmin {
3735                    system_user: raw_user_data[4].0.clone(),
3736                },
3737                SystemUserData::BackendBackup {
3738                    system_user: &raw_user_data[5].0,
3739                    ssh_authorized_key: raw_user_data[5]
3740                        .1
3741                        .as_ref()
3742                        .expect("to have SSH authorized key"),
3743                },
3744                SystemUserData::BackendHermeticMetrics {
3745                    system_user: &raw_user_data[6].0,
3746                },
3747                SystemUserData::BackendMetrics {
3748                    system_user: &raw_user_data[7].0,
3749                    ssh_authorized_key: raw_user_data[7]
3750                        .1
3751                        .as_ref()
3752                        .expect("to have SSH authorized key"),
3753                },
3754                SystemUserData::BackendSign {
3755                    system_user: &raw_user_data[8].0,
3756                    ssh_authorized_key: raw_user_data[8]
3757                        .1
3758                        .as_ref()
3759                        .expect("to have SSH authorized key"),
3760                },
3761                SystemUserData::BackendMetrics {
3762                    system_user: &raw_user_data[10].0,
3763                    ssh_authorized_key: raw_user_data[10]
3764                        .1
3765                        .as_ref()
3766                        .expect("to have SSH authorized key"),
3767                },
3768                SystemUserData::BackendBackup {
3769                    system_user: &raw_user_data[11].0,
3770                    ssh_authorized_key: raw_user_data[11]
3771                        .1
3772                        .as_ref()
3773                        .expect("to have SSH authorized key"),
3774                },
3775                SystemUserData::BackendHermeticMetrics {
3776                    system_user: &raw_user_data[12].0,
3777                },
3778                SystemUserData::BackendSign {
3779                    system_user: &raw_user_data[13].0,
3780                    ssh_authorized_key: raw_user_data[13]
3781                        .1
3782                        .as_ref()
3783                        .expect("to have SSH authorized key"),
3784                },
3785            ]);
3786
3787            assert_eq!(config.system_user_data(), expected);
3788            Ok(())
3789        }
3790
3791        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
3792        #[rstest]
3793        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
3794            let config = default_config?;
3795            let expected: HashSet<SystemUserId> = HashSet::from_iter([
3796                "share-holder1".parse()?,
3797                "share-holder2".parse()?,
3798                "share-holder3".parse()?,
3799                "wireguard-downloader".parse()?,
3800                "nethsm-backup-user".parse()?,
3801                "nethsm-hermetic-metrics-user".parse()?,
3802                "nethsm-metrics-user".parse()?,
3803                "nethsm-signing-user".parse()?,
3804                "yubihsm2-metrics-user".parse()?,
3805                "yubihsm2-backup-user".parse()?,
3806                "yubihsm2-hermetic-metrics-user".parse()?,
3807                "yubihsm2-signing-user".parse()?,
3808            ]);
3809
3810            assert_eq!(
3811                config.system_user_ids(),
3812                expected.iter().collect::<HashSet<_>>()
3813            );
3814            Ok(())
3815        }
3816
3817        /// Create a [`Config`] using [`ConfigBuilder`].
3818        #[rstest]
3819        fn config_builder_new(
3820            default_system_config: TestResult<SystemConfig>,
3821            default_nethsm_config: TestResult<NetHsmConfig>,
3822            default_yubihsm2_config: TestResult<YubiHsm2Config>,
3823        ) -> TestResult {
3824            let _config = ConfigBuilder::new(default_system_config?)
3825                .set_nethsm_config(default_nethsm_config?)
3826                .set_yubihsm2_config(default_yubihsm2_config?)
3827                .finish()?;
3828
3829            Ok(())
3830        }
3831
3832        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
3833        /// the same YAML string.
3834        ///
3835        /// The configuration file describes a [`SystemConfig`], [`NetHsmConfig`] and a
3836        /// [`YubiHsm2Config`] object.
3837        #[rstest]
3838        fn roundtrip_yaml_config(
3839            #[files("../fixtures/config/all_backends/*.yaml")] path: PathBuf,
3840        ) -> TestResult {
3841            let config_string = read_to_string(&path)?;
3842            let config = Config::from_file_path(&path)?;
3843
3844            assert_eq!(config.to_yaml_string()?, config_string);
3845
3846            Ok(())
3847        }
3848
3849        /// Ensures, that [`AdministrativeSecretHandling`] and
3850        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
3851        /// [`UserBackendConnection`].
3852        #[rstest]
3853        fn user_backend_connection_secret_handling(
3854            default_config: TestResult<Config>,
3855        ) -> TestResult {
3856            let config = default_config?;
3857            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
3858                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3859                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3860            };
3861            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
3862
3863            for user in ["nethsm-signing-user", "yubihsm2-signing-user"] {
3864                let user_backend_connection = config
3865                    .user_backend_connection(&user.parse()?)
3866                    .expect("there to be a mapping of the requested name");
3867
3868                assert_eq!(
3869                    user_backend_connection.admin_secret_handling(),
3870                    admin_secret_handling
3871                );
3872                assert_eq!(
3873                    user_backend_connection.non_admin_secret_handling(),
3874                    non_admin_secret_handling
3875                );
3876            }
3877
3878            Ok(())
3879        }
3880    }
3881}