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