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