Skip to main content

signstar_config/config/
system.rs

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