signstar_config/config/
system.rs

1//! Configuration objects for system users and functionality.
2
3use std::{
4    collections::{BTreeSet, HashSet},
5    num::NonZeroUsize,
6};
7
8use garde::Validate;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    AuthorizedKeyEntry,
13    SystemUserId,
14    config::{
15        ConfigAuthorizedKeyEntries,
16        ConfigSystemUserIds,
17        MappingAuthorizedKeyEntry,
18        MappingSystemUserId,
19        duplicate_authorized_keys,
20        duplicate_system_user_ids,
21    },
22};
23
24/// The default number of shares for [Shamir's Secret Sharing] (SSS).
25///
26/// [Shamir's Secret Sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
27const SSS_DEFAULT_NUMBER_OF_SHARES: NonZeroUsize =
28    NonZeroUsize::new(6).expect("6 is larger than 0");
29/// The default number of shares required for decrypting secrets encrypted using [Shamir's Secret
30/// Sharing] (SSS).
31///
32/// [Shamir's Secret Sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
33const SSS_DEFAULT_THRESHOLD: NonZeroUsize = NonZeroUsize::new(3).expect("3 is larger than 0");
34
35/// The handling of administrative secrets.
36///
37/// Administrative secrets may be handled in different ways (e.g. persistent or non-persistent).
38#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
39#[serde(rename_all = "kebab-case")]
40pub enum AdministrativeSecretHandling {
41    /// The administrative secrets are handled in a plaintext file in a non-volatile directory.
42    ///
43    /// ## Warning
44    ///
45    /// This variant should only be used in non-production test setups, as it implies the
46    /// persistence of unencrypted administrative secrets on a file system.
47    Plaintext,
48
49    /// The administrative secrets are handled in a file encrypted using [systemd-creds] in a
50    /// non-volatile directory.
51    ///
52    /// ## Warning
53    ///
54    /// This variant should only be used in non-production test setups, as it implies the
55    /// persistence of (host-specific) encrypted administrative secrets on a file system, that
56    /// could be extracted if the host is compromised.
57    ///
58    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
59    SystemdCreds,
60
61    /// The administrative secrets are handled using [Shamir's Secret Sharing] (SSS).
62    ///
63    /// This variant is the default for production use, as the administrative secrets are only ever
64    /// exposed on a volatile filesystem for the time of their use.
65    /// The secrets are only made available to the system as shares of a shared secret, split using
66    /// SSS.
67    /// This way no holder of a share is aware of the administrative secrets and the system only
68    /// for as long as it needs to use the administrative secrets.
69    ///
70    /// [Shamir's Secret Sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
71    ShamirsSecretSharing {
72        /// The number of shares used to encrypt the shared secret.
73        number_of_shares: NonZeroUsize,
74
75        /// The number of shares (see `number_of_shares`) required to decrypt the shared secret.
76        threshold: NonZeroUsize,
77    },
78}
79
80impl Default for AdministrativeSecretHandling {
81    fn default() -> Self {
82        Self::ShamirsSecretSharing {
83            number_of_shares: SSS_DEFAULT_NUMBER_OF_SHARES,
84            threshold: SSS_DEFAULT_THRESHOLD,
85        }
86    }
87}
88
89/// The handling of non-administrative secrets.
90///
91/// Non-administrative secrets represent passphrases for (non-administrator) HSM users and may be
92/// handled in different ways (e.g. encrypted or not encrypted).
93#[derive(
94    Clone,
95    Copy,
96    Debug,
97    Default,
98    Deserialize,
99    strum::Display,
100    strum::EnumString,
101    Eq,
102    Ord,
103    PartialEq,
104    PartialOrd,
105    Serialize,
106)]
107#[serde(rename_all = "kebab-case")]
108#[strum(serialize_all = "kebab-case")]
109pub enum NonAdministrativeSecretHandling {
110    /// Each non-administrative secret is handled in a plaintext file in a non-volatile
111    /// directory.
112    ///
113    /// ## Warning
114    ///
115    /// This variant should only be used in non-production test setups, as it implies the
116    /// persistence of unencrypted non-administrative secrets on a file system.
117    Plaintext,
118
119    /// Each non-administrative secret is encrypted for a specific system user using
120    /// [systemd-creds] and the resulting files are stored in a non-volatile directory.
121    ///
122    /// ## Note
123    ///
124    /// Although secrets are stored as encrypted strings in dedicated files, they may be extracted
125    /// under certain circumstances:
126    ///
127    /// - the root account is compromised
128    ///   - decrypts and exfiltrates _all_ secrets
129    ///   - the secret is not encrypted using a [TPM] and the file
130    ///     `/var/lib/systemd/credential.secret` as well as _any_ encrypted secret is exfiltrated
131    /// - a specific user is compromised, decrypts and exfiltrates its own secret
132    ///
133    /// It is therefore crucial to follow common best-practices:
134    ///
135    /// - rely on a [TPM] for encrypting secrets, so that files become host-specific
136    /// - heavily guard access to all users, especially root
137    ///
138    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
139    /// [TPM]: https://en.wikipedia.org/wiki/Trusted_Platform_Module
140    #[default]
141    SystemdCreds,
142}
143
144/// Mappings for system users.
145///
146/// # Note
147///
148/// None of the variants are mapped to backend users.
149#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
150#[serde(rename_all = "snake_case")]
151pub enum SystemUserMapping {
152    /// A user for up and downloading shares of a shared secret.
153    ShareHolder {
154        /// The name of the system user.
155        system_user: SystemUserId,
156
157        /// The list of SSH public keys used for connecting to the `system_user`.
158        ssh_authorized_key: AuthorizedKeyEntry,
159    },
160
161    /// A system user, with SSH access, not mapped to any backend user, that is used for downloading
162    /// the WireGuard configuration of the host.
163    WireGuardDownload {
164        /// The name of the system user.
165        system_user: SystemUserId,
166
167        /// The list of SSH public keys used for connecting to the `system_user`.
168        ssh_authorized_key: AuthorizedKeyEntry,
169    },
170}
171
172impl MappingAuthorizedKeyEntry for SystemUserMapping {
173    fn authorized_key_entry(&self) -> Option<&AuthorizedKeyEntry> {
174        match self {
175            Self::ShareHolder {
176                ssh_authorized_key, ..
177            }
178            | Self::WireGuardDownload {
179                ssh_authorized_key, ..
180            } => Some(ssh_authorized_key),
181        }
182    }
183}
184
185impl MappingSystemUserId for SystemUserMapping {
186    fn system_user_id(&self) -> Option<&SystemUserId> {
187        match self {
188            Self::ShareHolder { system_user, .. } | Self::WireGuardDownload { system_user, .. } => {
189                Some(system_user)
190            }
191        }
192    }
193}
194
195/// Validates a set of [`SystemUserMapping`] objects against [`AdministrativeSecretHandling`].
196///
197/// Ensures that `value` is not empty.
198///
199/// Ensures that in `mappings` there are
200///
201/// - no duplicate system users
202/// - no duplicate SSH authorized keys (by comparing the actual SSH public keys)
203/// - enough shareholders for SSS, if SSS is configured in `admin_secret_handling`
204/// - no shareholders for SSS, if SSS is _not_ configured in `admin_secret_handling`
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/// - not enough shareholders for SSS, if SSS is configured in `admin_secret_handling`
213/// - shareholders for SSS, if SSS is _not_ configured in `admin_secret_handling`
214fn validate_system_config_mappings(
215    admin_secret_handling: &AdministrativeSecretHandling,
216) -> impl FnOnce(&BTreeSet<SystemUserMapping>, &()) -> garde::Result + '_ {
217    move |mappings, _| {
218        // Collect all duplicate system user IDs.
219        let duplicate_system_user_ids = duplicate_system_user_ids(mappings);
220
221        // Collect all duplicate SSH public keys used as authorized_keys.
222        let duplicate_authorized_keys = duplicate_authorized_keys(mappings);
223
224        // Get the number of user mappings that represent a shareholder for SSS.
225        let num_shares = mappings
226            .iter()
227            .filter(|mapping| matches!(mapping, SystemUserMapping::ShareHolder { .. }))
228            .count();
229
230        // Collect issues around the use of SSS shareholders.
231        let mismatching_sss_shares = match admin_secret_handling {
232            AdministrativeSecretHandling::ShamirsSecretSharing {
233                number_of_shares, ..
234            } => {
235                if number_of_shares.get() > num_shares {
236                    Some(format!(
237                        "only {num_shares} shareholders, but the SSS setup requires {}",
238                        number_of_shares.get()
239                    ))
240                } else {
241                    None
242                }
243            }
244            AdministrativeSecretHandling::Plaintext => {
245                if num_shares != 0 {
246                    Some(format!(
247                        "{num_shares} SSS shareholders, but the administrative secret handling is plaintext"
248                    ))
249                } else {
250                    None
251                }
252            }
253            AdministrativeSecretHandling::SystemdCreds => {
254                if num_shares != 0 {
255                    Some(format!(
256                        "{num_shares} SSS shareholders, but the administrative secret handling is systemd-creds"
257                    ))
258                } else {
259                    None
260                }
261            }
262        };
263
264        let messages = [
265            duplicate_system_user_ids,
266            duplicate_authorized_keys,
267            mismatching_sss_shares,
268        ];
269        let error_messages = {
270            let mut error_messages = Vec::new();
271
272            for message in messages.iter().flatten() {
273                error_messages.push(message.as_str());
274            }
275
276            error_messages
277        };
278
279        match error_messages.len() {
280            0 => Ok(()),
281            1 => Err(garde::Error::new(format!(
282                "contains {}",
283                error_messages.join("\n")
284            ))),
285            _ => Err(garde::Error::new(format!(
286                "contains multiple issues:\n⤷ {}",
287                error_messages.join("\n⤷ ")
288            ))),
289        }
290    }
291}
292
293/// System-wide configuration items.
294///
295/// This struct tracks various items:
296///
297/// - the `iteration` (version) of the configuration
298/// - the `admin_secret_handling` which describes how administrative secrets are stored/handled on
299///   the system
300/// - the `non_admin_secret_handling` which describes how non-administrative secrets are stored on
301///   the system
302/// - the `mappings` which describe user mappings for system users (e.g. SSS shareholders or users
303///   for downloading wireguard configurations)
304#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)]
305#[serde(rename_all = "snake_case")]
306pub struct SystemConfig {
307    #[garde(skip)]
308    iteration: u32,
309
310    #[garde(skip)]
311    admin_secret_handling: AdministrativeSecretHandling,
312
313    #[garde(skip)]
314    non_admin_secret_handling: NonAdministrativeSecretHandling,
315
316    #[garde(custom(validate_system_config_mappings(&self.admin_secret_handling)))]
317    mappings: BTreeSet<SystemUserMapping>,
318}
319
320impl SystemConfig {
321    /// Creates a new [`SystemConfig`].
322    pub fn new(
323        iteration: u32,
324        admin_secret_handling: AdministrativeSecretHandling,
325        non_admin_secret_handling: NonAdministrativeSecretHandling,
326        mappings: BTreeSet<SystemUserMapping>,
327    ) -> Result<Self, crate::Error> {
328        let config = Self {
329            iteration,
330            admin_secret_handling,
331            non_admin_secret_handling,
332            mappings,
333        };
334        config
335            .validate()
336            .map_err(|source| crate::Error::Validation {
337                context: "validating a system configuration object".to_string(),
338                source,
339            })?;
340
341        Ok(config)
342    }
343
344    /// Returns the iteration of the configuration.
345    pub fn iteration(&self) -> u32 {
346        self.iteration
347    }
348
349    /// Returns a reference to the [`AdministrativeSecretHandling`].
350    pub fn admin_secret_handling(&self) -> &AdministrativeSecretHandling {
351        &self.admin_secret_handling
352    }
353
354    /// Returns a reference to the [`NonAdministrativeSecretHandling`].
355    pub fn non_admin_secret_handling(&self) -> &NonAdministrativeSecretHandling {
356        &self.non_admin_secret_handling
357    }
358
359    /// Returns a reference to the set of [`SystemUserMapping`] objects.
360    pub fn mappings(&self) -> &BTreeSet<SystemUserMapping> {
361        &self.mappings
362    }
363}
364
365impl ConfigAuthorizedKeyEntries for SystemConfig {
366    fn authorized_key_entries(&self) -> HashSet<&AuthorizedKeyEntry> {
367        self.mappings
368            .iter()
369            .filter_map(|mapping| mapping.authorized_key_entry())
370            .collect()
371    }
372}
373
374impl ConfigSystemUserIds for SystemConfig {
375    fn system_user_ids(&self) -> HashSet<&SystemUserId> {
376        self.mappings
377            .iter()
378            .filter_map(|mapping| mapping.system_user_id())
379            .collect()
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use std::thread::current;
386
387    use insta::{assert_snapshot, with_settings};
388    use rstest::{fixture, rstest};
389    use testresult::TestResult;
390
391    use super::*;
392
393    const SNAPSHOT_PATH: &str = "fixtures/system_config/";
394
395    #[test]
396    fn administrative_secret_handling_default() {
397        assert_eq!(
398            AdministrativeSecretHandling::default(),
399            AdministrativeSecretHandling::ShamirsSecretSharing {
400                number_of_shares: SSS_DEFAULT_NUMBER_OF_SHARES,
401                threshold: SSS_DEFAULT_THRESHOLD,
402            },
403        )
404    }
405
406    #[rstest]
407    #[case::shamirs_secret_sharing_plaintext(
408        AdministrativeSecretHandling::ShamirsSecretSharing {
409            number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
410            threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
411        },
412        NonAdministrativeSecretHandling::Plaintext,
413        BTreeSet::from_iter([
414            SystemUserMapping::ShareHolder {
415                system_user: "share-holder1".parse()?,
416                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
417            },
418            SystemUserMapping::ShareHolder {
419                system_user: "share-holder2".parse()?,
420                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
421            },
422            SystemUserMapping::ShareHolder {
423                system_user: "share-holder3".parse()?,
424                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
425            },
426            SystemUserMapping::WireGuardDownload {
427                system_user: "wireguard-downloader".parse()?,
428                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
429            },
430        ]),
431    )]
432    #[case::shamirs_secret_sharing_systemd_creds(
433        AdministrativeSecretHandling::ShamirsSecretSharing {
434            number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
435            threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
436        },
437        NonAdministrativeSecretHandling::SystemdCreds,
438        BTreeSet::from_iter([
439            SystemUserMapping::ShareHolder {
440                system_user: "share-holder1".parse()?,
441                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
442            },
443            SystemUserMapping::ShareHolder {
444                system_user: "share-holder2".parse()?,
445                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
446            },
447            SystemUserMapping::ShareHolder {
448                system_user: "share-holder3".parse()?,
449                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
450            },
451            SystemUserMapping::WireGuardDownload {
452                system_user: "wireguard-downloader".parse()?,
453                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
454            },
455        ]),
456    )]
457    #[case::systemd_creds_plaintext(
458        AdministrativeSecretHandling::SystemdCreds,
459        NonAdministrativeSecretHandling::Plaintext,
460        BTreeSet::from_iter([
461            SystemUserMapping::WireGuardDownload {
462                system_user: "wireguard-downloader".parse()?,
463                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
464            },
465        ]),
466    )]
467    #[case::systemd_creds_systemd_creds(
468        AdministrativeSecretHandling::SystemdCreds,
469        NonAdministrativeSecretHandling::SystemdCreds,
470        BTreeSet::from_iter([
471            SystemUserMapping::WireGuardDownload {
472                system_user: "wireguard-downloader".parse()?,
473                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
474            },
475        ]),
476    )]
477    #[case::plaintext_plaintext(
478        AdministrativeSecretHandling::Plaintext,
479        NonAdministrativeSecretHandling::Plaintext,
480        BTreeSet::from_iter([
481            SystemUserMapping::WireGuardDownload {
482                system_user: "wireguard-downloader".parse()?,
483                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
484            },
485        ]),
486    )]
487    #[case::plaintext_systemd_creds(
488        AdministrativeSecretHandling::Plaintext,
489        NonAdministrativeSecretHandling::SystemdCreds,
490        BTreeSet::from_iter([
491            SystemUserMapping::WireGuardDownload {
492                system_user: "wireguard-downloader".parse()?,
493                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
494            },
495        ]),
496    )]
497    fn system_config_new_succeeds(
498        #[case] administrative_secret_handling: AdministrativeSecretHandling,
499        #[case] non_administrative_secret_handling: NonAdministrativeSecretHandling,
500        #[case] mappings: BTreeSet<SystemUserMapping>,
501    ) -> TestResult {
502        assert!(
503            SystemConfig::new(
504                1,
505                administrative_secret_handling,
506                non_administrative_secret_handling,
507                mappings,
508            )
509            .is_ok()
510        );
511
512        Ok(())
513    }
514
515    #[rstest]
516    #[case::duplicate_user_ids(
517        "Error message for SystemConfig::new with duplicate system user IDs",
518        AdministrativeSecretHandling::ShamirsSecretSharing {
519            number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
520            threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
521        },
522        BTreeSet::from_iter([
523            SystemUserMapping::ShareHolder {
524                system_user: "share-holder1".parse()?,
525                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
526            },
527            SystemUserMapping::ShareHolder {
528                system_user: "share-holder1".parse()?,
529                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
530            },
531            SystemUserMapping::ShareHolder {
532                system_user: "share-holder3".parse()?,
533                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
534            },
535            SystemUserMapping::WireGuardDownload {
536                system_user: "wireguard-downloader".parse()?,
537                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
538            },
539        ]),
540    )]
541    #[case::duplicate_ssh_public_keys(
542        "Error message for SystemConfig::new with duplicate SSH public keys as authorized_keys",
543        AdministrativeSecretHandling::ShamirsSecretSharing {
544            number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
545            threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
546        },
547        BTreeSet::from_iter([
548            SystemUserMapping::ShareHolder {
549                system_user: "share-holder1".parse()?,
550                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
551            },
552            SystemUserMapping::ShareHolder {
553                system_user: "share-holder2".parse()?,
554                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user2@host3".parse()?,
555            },
556            SystemUserMapping::ShareHolder {
557                system_user: "share-holder3".parse()?,
558                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
559            },
560            SystemUserMapping::WireGuardDownload {
561                system_user: "wireguard-downloader".parse()?,
562                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
563            },
564        ]),
565    )]
566    #[case::too_few_sss_shares(
567        "Error message for SystemConfig::new with too few SSS shareholders",
568        AdministrativeSecretHandling::ShamirsSecretSharing {
569            number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
570            threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
571        },
572        BTreeSet::from_iter([
573            SystemUserMapping::ShareHolder {
574                system_user: "share-holder1".parse()?,
575                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
576            },
577            SystemUserMapping::ShareHolder {
578                system_user: "share-holder2".parse()?,
579                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
580            },
581            SystemUserMapping::WireGuardDownload {
582                system_user: "wireguard-downloader".parse()?,
583                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
584            },
585        ]),
586    )]
587    #[case::plaintext_admin_creds_with_sss_shareholders(
588        "Error message for SystemConfig::new with SSS shareholders but plaintext based admin credentials handling",
589        AdministrativeSecretHandling::Plaintext,
590        BTreeSet::from_iter([
591            SystemUserMapping::ShareHolder {
592                system_user: "share-holder1".parse()?,
593                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
594            },
595            SystemUserMapping::ShareHolder {
596                system_user: "share-holder2".parse()?,
597                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
598            },
599            SystemUserMapping::ShareHolder {
600                system_user: "share-holder3".parse()?,
601                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
602            },
603            SystemUserMapping::WireGuardDownload {
604                system_user: "wireguard-downloader".parse()?,
605                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
606            },
607        ]),
608    )]
609    #[case::systemd_creds_admin_creds_with_sss_shareholders(
610        "Error message for SystemConfig::new with SSS shareholders but systemd-creds based admin credentials handling",
611        AdministrativeSecretHandling::SystemdCreds,
612        BTreeSet::from_iter([
613            SystemUserMapping::ShareHolder {
614                system_user: "share-holder1".parse()?,
615                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
616            },
617            SystemUserMapping::ShareHolder {
618                system_user: "share-holder2".parse()?,
619                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
620            },
621            SystemUserMapping::ShareHolder {
622                system_user: "share-holder3".parse()?,
623                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
624            },
625            SystemUserMapping::WireGuardDownload {
626                system_user: "wireguard-downloader".parse()?,
627                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
628            },
629        ]),
630    )]
631    #[case::multiple_issues(
632        "Error message for SystemConfig::new with SSS shareholders but plaintext based admin credentials handling, duplicate system user IDs and SSH public keys",
633        AdministrativeSecretHandling::SystemdCreds,
634        BTreeSet::from_iter([
635            SystemUserMapping::ShareHolder {
636                system_user: "share-holder1".parse()?,
637                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
638            },
639            SystemUserMapping::ShareHolder {
640                system_user: "share-holder1".parse()?,
641                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user1@host5".parse()?,
642            },
643            SystemUserMapping::ShareHolder {
644                system_user: "wireguard-downloader".parse()?,
645                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user2@host3".parse()?
646            },
647            SystemUserMapping::WireGuardDownload {
648                system_user: "wireguard-downloader".parse()?,
649                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
650            },
651        ]),
652    )]
653    fn system_config_new_fails_validation(
654        #[case] description: &str,
655        #[case] admin_secret_handling: AdministrativeSecretHandling,
656        #[case] mappings: BTreeSet<SystemUserMapping>,
657    ) -> TestResult {
658        let error_msg = match SystemConfig::new(
659            1,
660            admin_secret_handling,
661            NonAdministrativeSecretHandling::default(),
662            mappings,
663        ) {
664            Err(crate::Error::Validation { source, .. }) => source.to_string(),
665            Ok(config) => {
666                panic!(
667                    "Expected to fail with Error::Validation, but succeeded instead:
668    {config:?}"
669                )
670            }
671            Err(error) => panic!(
672                "Expected to fail with Error::Validation, but failed with a different error
673    instead: {error}"
674            ),
675        };
676
677        with_settings!({
678            description => description,
679            snapshot_path => SNAPSHOT_PATH,
680            prepend_module_to_snapshot => false,
681        }, {
682            assert_snapshot!(current().name().expect("current thread should have a
683    name").to_string().replace("::", "__"), error_msg);     });
684        Ok(())
685    }
686
687    #[fixture]
688    fn administrative_secret_handling() -> AdministrativeSecretHandling {
689        AdministrativeSecretHandling::default()
690    }
691
692    #[fixture]
693    fn non_administrative_secret_handling() -> NonAdministrativeSecretHandling {
694        NonAdministrativeSecretHandling::default()
695    }
696
697    #[fixture]
698    fn mappings() -> TestResult<BTreeSet<SystemUserMapping>> {
699        Ok(BTreeSet::from_iter([
700                    SystemUserMapping::ShareHolder {
701                        system_user: "share-holder1".parse()?,
702                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
703                    },
704                    SystemUserMapping::ShareHolder {
705                        system_user: "share-holder2".parse()?,
706                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
707                    },
708                    SystemUserMapping::ShareHolder {
709                        system_user: "share-holder3".parse()?,
710                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
711                    },
712                    SystemUserMapping::ShareHolder {
713                        system_user: "share-holder4".parse()?,
714                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINsej5PBntjmthtYKXUrPKwYKadruZMhvZE3EmVxbOwL user@host".parse()?
715                    },
716                    SystemUserMapping::ShareHolder {
717                        system_user: "share-holder5".parse()?,
718                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJMmh08ZQTPRQS9NDNJY6zRVdjwSBwcPcefiXnAEtsgE user@host".parse()?
719                    },
720                    SystemUserMapping::ShareHolder {
721                        system_user: "share-holder6".parse()?,
722                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJAW0YOVnJHm5qqiZBvIwPc0GH1D7ALDGwDRsBZHWbGU user@host".parse()?
723                    },
724                    SystemUserMapping::WireGuardDownload {
725                        system_user: "wireguard-downloader".parse()?,
726                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
727                    },
728                ]))
729    }
730
731    #[fixture]
732    fn system_config(
733        administrative_secret_handling: AdministrativeSecretHandling,
734        non_administrative_secret_handling: NonAdministrativeSecretHandling,
735        mappings: TestResult<BTreeSet<SystemUserMapping>>,
736    ) -> TestResult<SystemConfig> {
737        let mappings = mappings?;
738        Ok(SystemConfig::new(
739            1,
740            administrative_secret_handling,
741            non_administrative_secret_handling,
742            mappings,
743        )?)
744    }
745
746    #[rstest]
747    fn system_config_iteration(system_config: TestResult<SystemConfig>) -> TestResult {
748        let system_config = system_config?;
749        assert_eq!(system_config.iteration(), 1);
750
751        Ok(())
752    }
753
754    #[rstest]
755    fn system_config_admin_secret_handling(
756        system_config: TestResult<SystemConfig>,
757        administrative_secret_handling: AdministrativeSecretHandling,
758    ) -> TestResult {
759        let system_config = system_config?;
760        assert_eq!(
761            system_config.admin_secret_handling(),
762            &administrative_secret_handling
763        );
764
765        Ok(())
766    }
767
768    #[rstest]
769    fn system_config_non_admin_secret_handling(
770        system_config: TestResult<SystemConfig>,
771        non_administrative_secret_handling: NonAdministrativeSecretHandling,
772    ) -> TestResult {
773        let system_config = system_config?;
774        assert_eq!(
775            system_config.non_admin_secret_handling(),
776            &non_administrative_secret_handling
777        );
778
779        Ok(())
780    }
781
782    #[rstest]
783    fn system_config_mappings(
784        system_config: TestResult<SystemConfig>,
785        mappings: TestResult<BTreeSet<SystemUserMapping>>,
786    ) -> TestResult {
787        let system_config = system_config?;
788        let mappings = mappings?;
789        assert_eq!(system_config.mappings(), &mappings);
790
791        Ok(())
792    }
793
794    #[rstest]
795    fn system_config_authorized_key_entries(
796        system_config: TestResult<SystemConfig>,
797        mappings: TestResult<BTreeSet<SystemUserMapping>>,
798    ) -> TestResult {
799        let system_config = system_config?;
800        let mappings = mappings?;
801        let authorized_keys = mappings
802            .iter()
803            .filter_map(|mapping| mapping.authorized_key_entry())
804            .collect::<HashSet<_>>();
805        assert_eq!(system_config.authorized_key_entries(), authorized_keys);
806
807        Ok(())
808    }
809
810    #[rstest]
811    fn system_config_system_user_ids(
812        system_config: TestResult<SystemConfig>,
813        mappings: TestResult<BTreeSet<SystemUserMapping>>,
814    ) -> TestResult {
815        let system_config = system_config?;
816        let mappings = mappings?;
817        let system_user_ids = mappings
818            .iter()
819            .filter_map(|mapping| mapping.system_user_id())
820            .collect::<HashSet<_>>();
821        assert_eq!(system_config.system_user_ids(), system_user_ids);
822
823        Ok(())
824    }
825}