1use 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#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum SystemUserMapping {
35 ShareHolder {
37 ssh_authorized_key: AuthorizedKeyEntry,
39
40 system_user: SystemUserId,
42 },
43
44 WireguardDownload {
47 ssh_authorized_key: AuthorizedKeyEntry,
49
50 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
140fn validate_system_config_mappings(
160 admin_secret_handling: &AdministrativeSecretHandling,
161) -> impl FnOnce(&BTreeSet<SystemUserMapping>, &()) -> garde::Result + '_ {
162 move |mappings, _| {
163 let duplicate_system_user_ids = duplicate_system_user_ids(mappings);
165
166 let duplicate_authorized_keys = duplicate_authorized_keys(mappings);
168
169 let num_shares = mappings
171 .iter()
172 .filter(|mapping| matches!(mapping, SystemUserMapping::ShareHolder { .. }))
173 .count();
174
175 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#[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 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 pub fn iteration(&self) -> u32 {
291 self.iteration
292 }
293
294 pub fn admin_secret_handling(&self) -> &AdministrativeSecretHandling {
296 &self.admin_secret_handling
297 }
298
299 pub fn non_admin_secret_handling(&self) -> &NonAdministrativeSecretHandling {
301 &self.non_admin_secret_handling
302 }
303
304 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#[derive(Debug)]
330pub struct SystemUserDiff<'a, 'b> {
331 pub config: &'a SystemUserConfigState<'a>,
333
334 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 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 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 }
379
380 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}