1use std::{
4 collections::{BTreeSet, HashSet},
5 num::NonZeroUsize,
6};
7
8use garde::Validate;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 AuthorizedKeyEntry,
13 SystemUserId,
14 config::{
15 ConfigAuthorizedKeyEntries,
16 ConfigSystemUserIds,
17 MappingAuthorizedKeyEntry,
18 MappingSystemUserId,
19 duplicate_authorized_keys,
20 duplicate_system_user_ids,
21 },
22};
23
24const SSS_DEFAULT_NUMBER_OF_SHARES: NonZeroUsize =
28 NonZeroUsize::new(6).expect("6 is larger than 0");
29const SSS_DEFAULT_THRESHOLD: NonZeroUsize = NonZeroUsize::new(3).expect("3 is larger than 0");
34
35#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
39#[serde(rename_all = "kebab-case")]
40pub enum AdministrativeSecretHandling {
41 Plaintext,
48
49 SystemdCreds,
60
61 ShamirsSecretSharing {
72 number_of_shares: NonZeroUsize,
74
75 threshold: NonZeroUsize,
77 },
78}
79
80impl Default for AdministrativeSecretHandling {
81 fn default() -> Self {
82 Self::ShamirsSecretSharing {
83 number_of_shares: SSS_DEFAULT_NUMBER_OF_SHARES,
84 threshold: SSS_DEFAULT_THRESHOLD,
85 }
86 }
87}
88
89#[derive(
94 Clone,
95 Copy,
96 Debug,
97 Default,
98 Deserialize,
99 strum::Display,
100 strum::EnumString,
101 Eq,
102 Ord,
103 PartialEq,
104 PartialOrd,
105 Serialize,
106)]
107#[serde(rename_all = "kebab-case")]
108#[strum(serialize_all = "kebab-case")]
109pub enum NonAdministrativeSecretHandling {
110 Plaintext,
118
119 #[default]
141 SystemdCreds,
142}
143
144#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
150#[serde(rename_all = "snake_case")]
151pub enum SystemUserMapping {
152 ShareHolder {
154 system_user: SystemUserId,
156
157 ssh_authorized_key: AuthorizedKeyEntry,
159 },
160
161 WireGuardDownload {
164 system_user: SystemUserId,
166
167 ssh_authorized_key: AuthorizedKeyEntry,
169 },
170}
171
172impl MappingAuthorizedKeyEntry for SystemUserMapping {
173 fn authorized_key_entry(&self) -> Option<&AuthorizedKeyEntry> {
174 match self {
175 Self::ShareHolder {
176 ssh_authorized_key, ..
177 }
178 | Self::WireGuardDownload {
179 ssh_authorized_key, ..
180 } => Some(ssh_authorized_key),
181 }
182 }
183}
184
185impl MappingSystemUserId for SystemUserMapping {
186 fn system_user_id(&self) -> Option<&SystemUserId> {
187 match self {
188 Self::ShareHolder { system_user, .. } | Self::WireGuardDownload { system_user, .. } => {
189 Some(system_user)
190 }
191 }
192 }
193}
194
195fn validate_system_config_mappings(
215 admin_secret_handling: &AdministrativeSecretHandling,
216) -> impl FnOnce(&BTreeSet<SystemUserMapping>, &()) -> garde::Result + '_ {
217 move |mappings, _| {
218 let duplicate_system_user_ids = duplicate_system_user_ids(mappings);
220
221 let duplicate_authorized_keys = duplicate_authorized_keys(mappings);
223
224 let num_shares = mappings
226 .iter()
227 .filter(|mapping| matches!(mapping, SystemUserMapping::ShareHolder { .. }))
228 .count();
229
230 let mismatching_sss_shares = match admin_secret_handling {
232 AdministrativeSecretHandling::ShamirsSecretSharing {
233 number_of_shares, ..
234 } => {
235 if number_of_shares.get() > num_shares {
236 Some(format!(
237 "only {num_shares} shareholders, but the SSS setup requires {}",
238 number_of_shares.get()
239 ))
240 } else {
241 None
242 }
243 }
244 AdministrativeSecretHandling::Plaintext => {
245 if num_shares != 0 {
246 Some(format!(
247 "{num_shares} SSS shareholders, but the administrative secret handling is plaintext"
248 ))
249 } else {
250 None
251 }
252 }
253 AdministrativeSecretHandling::SystemdCreds => {
254 if num_shares != 0 {
255 Some(format!(
256 "{num_shares} SSS shareholders, but the administrative secret handling is systemd-creds"
257 ))
258 } else {
259 None
260 }
261 }
262 };
263
264 let messages = [
265 duplicate_system_user_ids,
266 duplicate_authorized_keys,
267 mismatching_sss_shares,
268 ];
269 let error_messages = {
270 let mut error_messages = Vec::new();
271
272 for message in messages.iter().flatten() {
273 error_messages.push(message.as_str());
274 }
275
276 error_messages
277 };
278
279 match error_messages.len() {
280 0 => Ok(()),
281 1 => Err(garde::Error::new(format!(
282 "contains {}",
283 error_messages.join("\n")
284 ))),
285 _ => Err(garde::Error::new(format!(
286 "contains multiple issues:\n⤷ {}",
287 error_messages.join("\n⤷ ")
288 ))),
289 }
290 }
291}
292
293#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)]
305#[serde(rename_all = "snake_case")]
306pub struct SystemConfig {
307 #[garde(skip)]
308 iteration: u32,
309
310 #[garde(skip)]
311 admin_secret_handling: AdministrativeSecretHandling,
312
313 #[garde(skip)]
314 non_admin_secret_handling: NonAdministrativeSecretHandling,
315
316 #[garde(custom(validate_system_config_mappings(&self.admin_secret_handling)))]
317 mappings: BTreeSet<SystemUserMapping>,
318}
319
320impl SystemConfig {
321 pub fn new(
323 iteration: u32,
324 admin_secret_handling: AdministrativeSecretHandling,
325 non_admin_secret_handling: NonAdministrativeSecretHandling,
326 mappings: BTreeSet<SystemUserMapping>,
327 ) -> Result<Self, crate::Error> {
328 let config = Self {
329 iteration,
330 admin_secret_handling,
331 non_admin_secret_handling,
332 mappings,
333 };
334 config
335 .validate()
336 .map_err(|source| crate::Error::Validation {
337 context: "validating a system configuration object".to_string(),
338 source,
339 })?;
340
341 Ok(config)
342 }
343
344 pub fn iteration(&self) -> u32 {
346 self.iteration
347 }
348
349 pub fn admin_secret_handling(&self) -> &AdministrativeSecretHandling {
351 &self.admin_secret_handling
352 }
353
354 pub fn non_admin_secret_handling(&self) -> &NonAdministrativeSecretHandling {
356 &self.non_admin_secret_handling
357 }
358
359 pub fn mappings(&self) -> &BTreeSet<SystemUserMapping> {
361 &self.mappings
362 }
363}
364
365impl ConfigAuthorizedKeyEntries for SystemConfig {
366 fn authorized_key_entries(&self) -> HashSet<&AuthorizedKeyEntry> {
367 self.mappings
368 .iter()
369 .filter_map(|mapping| mapping.authorized_key_entry())
370 .collect()
371 }
372}
373
374impl ConfigSystemUserIds for SystemConfig {
375 fn system_user_ids(&self) -> HashSet<&SystemUserId> {
376 self.mappings
377 .iter()
378 .filter_map(|mapping| mapping.system_user_id())
379 .collect()
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use std::thread::current;
386
387 use insta::{assert_snapshot, with_settings};
388 use rstest::{fixture, rstest};
389 use testresult::TestResult;
390
391 use super::*;
392
393 const SNAPSHOT_PATH: &str = "fixtures/system_config/";
394
395 #[test]
396 fn administrative_secret_handling_default() {
397 assert_eq!(
398 AdministrativeSecretHandling::default(),
399 AdministrativeSecretHandling::ShamirsSecretSharing {
400 number_of_shares: SSS_DEFAULT_NUMBER_OF_SHARES,
401 threshold: SSS_DEFAULT_THRESHOLD,
402 },
403 )
404 }
405
406 #[rstest]
407 #[case::shamirs_secret_sharing_plaintext(
408 AdministrativeSecretHandling::ShamirsSecretSharing {
409 number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
410 threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
411 },
412 NonAdministrativeSecretHandling::Plaintext,
413 BTreeSet::from_iter([
414 SystemUserMapping::ShareHolder {
415 system_user: "share-holder1".parse()?,
416 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
417 },
418 SystemUserMapping::ShareHolder {
419 system_user: "share-holder2".parse()?,
420 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
421 },
422 SystemUserMapping::ShareHolder {
423 system_user: "share-holder3".parse()?,
424 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
425 },
426 SystemUserMapping::WireGuardDownload {
427 system_user: "wireguard-downloader".parse()?,
428 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
429 },
430 ]),
431 )]
432 #[case::shamirs_secret_sharing_systemd_creds(
433 AdministrativeSecretHandling::ShamirsSecretSharing {
434 number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
435 threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
436 },
437 NonAdministrativeSecretHandling::SystemdCreds,
438 BTreeSet::from_iter([
439 SystemUserMapping::ShareHolder {
440 system_user: "share-holder1".parse()?,
441 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
442 },
443 SystemUserMapping::ShareHolder {
444 system_user: "share-holder2".parse()?,
445 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
446 },
447 SystemUserMapping::ShareHolder {
448 system_user: "share-holder3".parse()?,
449 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
450 },
451 SystemUserMapping::WireGuardDownload {
452 system_user: "wireguard-downloader".parse()?,
453 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
454 },
455 ]),
456 )]
457 #[case::systemd_creds_plaintext(
458 AdministrativeSecretHandling::SystemdCreds,
459 NonAdministrativeSecretHandling::Plaintext,
460 BTreeSet::from_iter([
461 SystemUserMapping::WireGuardDownload {
462 system_user: "wireguard-downloader".parse()?,
463 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
464 },
465 ]),
466 )]
467 #[case::systemd_creds_systemd_creds(
468 AdministrativeSecretHandling::SystemdCreds,
469 NonAdministrativeSecretHandling::SystemdCreds,
470 BTreeSet::from_iter([
471 SystemUserMapping::WireGuardDownload {
472 system_user: "wireguard-downloader".parse()?,
473 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
474 },
475 ]),
476 )]
477 #[case::plaintext_plaintext(
478 AdministrativeSecretHandling::Plaintext,
479 NonAdministrativeSecretHandling::Plaintext,
480 BTreeSet::from_iter([
481 SystemUserMapping::WireGuardDownload {
482 system_user: "wireguard-downloader".parse()?,
483 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
484 },
485 ]),
486 )]
487 #[case::plaintext_systemd_creds(
488 AdministrativeSecretHandling::Plaintext,
489 NonAdministrativeSecretHandling::SystemdCreds,
490 BTreeSet::from_iter([
491 SystemUserMapping::WireGuardDownload {
492 system_user: "wireguard-downloader".parse()?,
493 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
494 },
495 ]),
496 )]
497 fn system_config_new_succeeds(
498 #[case] administrative_secret_handling: AdministrativeSecretHandling,
499 #[case] non_administrative_secret_handling: NonAdministrativeSecretHandling,
500 #[case] mappings: BTreeSet<SystemUserMapping>,
501 ) -> TestResult {
502 assert!(
503 SystemConfig::new(
504 1,
505 administrative_secret_handling,
506 non_administrative_secret_handling,
507 mappings,
508 )
509 .is_ok()
510 );
511
512 Ok(())
513 }
514
515 #[rstest]
516 #[case::duplicate_user_ids(
517 "Error message for SystemConfig::new with duplicate system user IDs",
518 AdministrativeSecretHandling::ShamirsSecretSharing {
519 number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
520 threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
521 },
522 BTreeSet::from_iter([
523 SystemUserMapping::ShareHolder {
524 system_user: "share-holder1".parse()?,
525 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
526 },
527 SystemUserMapping::ShareHolder {
528 system_user: "share-holder1".parse()?,
529 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
530 },
531 SystemUserMapping::ShareHolder {
532 system_user: "share-holder3".parse()?,
533 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
534 },
535 SystemUserMapping::WireGuardDownload {
536 system_user: "wireguard-downloader".parse()?,
537 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
538 },
539 ]),
540 )]
541 #[case::duplicate_ssh_public_keys(
542 "Error message for SystemConfig::new with duplicate SSH public keys as authorized_keys",
543 AdministrativeSecretHandling::ShamirsSecretSharing {
544 number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
545 threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
546 },
547 BTreeSet::from_iter([
548 SystemUserMapping::ShareHolder {
549 system_user: "share-holder1".parse()?,
550 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
551 },
552 SystemUserMapping::ShareHolder {
553 system_user: "share-holder2".parse()?,
554 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user2@host3".parse()?,
555 },
556 SystemUserMapping::ShareHolder {
557 system_user: "share-holder3".parse()?,
558 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
559 },
560 SystemUserMapping::WireGuardDownload {
561 system_user: "wireguard-downloader".parse()?,
562 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
563 },
564 ]),
565 )]
566 #[case::too_few_sss_shares(
567 "Error message for SystemConfig::new with too few SSS shareholders",
568 AdministrativeSecretHandling::ShamirsSecretSharing {
569 number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
570 threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
571 },
572 BTreeSet::from_iter([
573 SystemUserMapping::ShareHolder {
574 system_user: "share-holder1".parse()?,
575 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
576 },
577 SystemUserMapping::ShareHolder {
578 system_user: "share-holder2".parse()?,
579 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
580 },
581 SystemUserMapping::WireGuardDownload {
582 system_user: "wireguard-downloader".parse()?,
583 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
584 },
585 ]),
586 )]
587 #[case::plaintext_admin_creds_with_sss_shareholders(
588 "Error message for SystemConfig::new with SSS shareholders but plaintext based admin credentials handling",
589 AdministrativeSecretHandling::Plaintext,
590 BTreeSet::from_iter([
591 SystemUserMapping::ShareHolder {
592 system_user: "share-holder1".parse()?,
593 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
594 },
595 SystemUserMapping::ShareHolder {
596 system_user: "share-holder2".parse()?,
597 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
598 },
599 SystemUserMapping::ShareHolder {
600 system_user: "share-holder3".parse()?,
601 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
602 },
603 SystemUserMapping::WireGuardDownload {
604 system_user: "wireguard-downloader".parse()?,
605 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
606 },
607 ]),
608 )]
609 #[case::systemd_creds_admin_creds_with_sss_shareholders(
610 "Error message for SystemConfig::new with SSS shareholders but systemd-creds based admin credentials handling",
611 AdministrativeSecretHandling::SystemdCreds,
612 BTreeSet::from_iter([
613 SystemUserMapping::ShareHolder {
614 system_user: "share-holder1".parse()?,
615 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
616 },
617 SystemUserMapping::ShareHolder {
618 system_user: "share-holder2".parse()?,
619 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
620 },
621 SystemUserMapping::ShareHolder {
622 system_user: "share-holder3".parse()?,
623 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
624 },
625 SystemUserMapping::WireGuardDownload {
626 system_user: "wireguard-downloader".parse()?,
627 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
628 },
629 ]),
630 )]
631 #[case::multiple_issues(
632 "Error message for SystemConfig::new with SSS shareholders but plaintext based admin credentials handling, duplicate system user IDs and SSH public keys",
633 AdministrativeSecretHandling::SystemdCreds,
634 BTreeSet::from_iter([
635 SystemUserMapping::ShareHolder {
636 system_user: "share-holder1".parse()?,
637 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
638 },
639 SystemUserMapping::ShareHolder {
640 system_user: "share-holder1".parse()?,
641 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user1@host5".parse()?,
642 },
643 SystemUserMapping::ShareHolder {
644 system_user: "wireguard-downloader".parse()?,
645 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user2@host3".parse()?
646 },
647 SystemUserMapping::WireGuardDownload {
648 system_user: "wireguard-downloader".parse()?,
649 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
650 },
651 ]),
652 )]
653 fn system_config_new_fails_validation(
654 #[case] description: &str,
655 #[case] admin_secret_handling: AdministrativeSecretHandling,
656 #[case] mappings: BTreeSet<SystemUserMapping>,
657 ) -> TestResult {
658 let error_msg = match SystemConfig::new(
659 1,
660 admin_secret_handling,
661 NonAdministrativeSecretHandling::default(),
662 mappings,
663 ) {
664 Err(crate::Error::Validation { source, .. }) => source.to_string(),
665 Ok(config) => {
666 panic!(
667 "Expected to fail with Error::Validation, but succeeded instead:
668 {config:?}"
669 )
670 }
671 Err(error) => panic!(
672 "Expected to fail with Error::Validation, but failed with a different error
673 instead: {error}"
674 ),
675 };
676
677 with_settings!({
678 description => description,
679 snapshot_path => SNAPSHOT_PATH,
680 prepend_module_to_snapshot => false,
681 }, {
682 assert_snapshot!(current().name().expect("current thread should have a
683 name").to_string().replace("::", "__"), error_msg); });
684 Ok(())
685 }
686
687 #[fixture]
688 fn administrative_secret_handling() -> AdministrativeSecretHandling {
689 AdministrativeSecretHandling::default()
690 }
691
692 #[fixture]
693 fn non_administrative_secret_handling() -> NonAdministrativeSecretHandling {
694 NonAdministrativeSecretHandling::default()
695 }
696
697 #[fixture]
698 fn mappings() -> TestResult<BTreeSet<SystemUserMapping>> {
699 Ok(BTreeSet::from_iter([
700 SystemUserMapping::ShareHolder {
701 system_user: "share-holder1".parse()?,
702 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?
703 },
704 SystemUserMapping::ShareHolder {
705 system_user: "share-holder2".parse()?,
706 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
707 },
708 SystemUserMapping::ShareHolder {
709 system_user: "share-holder3".parse()?,
710 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
711 },
712 SystemUserMapping::ShareHolder {
713 system_user: "share-holder4".parse()?,
714 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINsej5PBntjmthtYKXUrPKwYKadruZMhvZE3EmVxbOwL user@host".parse()?
715 },
716 SystemUserMapping::ShareHolder {
717 system_user: "share-holder5".parse()?,
718 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJMmh08ZQTPRQS9NDNJY6zRVdjwSBwcPcefiXnAEtsgE user@host".parse()?
719 },
720 SystemUserMapping::ShareHolder {
721 system_user: "share-holder6".parse()?,
722 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJAW0YOVnJHm5qqiZBvIwPc0GH1D7ALDGwDRsBZHWbGU user@host".parse()?
723 },
724 SystemUserMapping::WireGuardDownload {
725 system_user: "wireguard-downloader".parse()?,
726 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
727 },
728 ]))
729 }
730
731 #[fixture]
732 fn system_config(
733 administrative_secret_handling: AdministrativeSecretHandling,
734 non_administrative_secret_handling: NonAdministrativeSecretHandling,
735 mappings: TestResult<BTreeSet<SystemUserMapping>>,
736 ) -> TestResult<SystemConfig> {
737 let mappings = mappings?;
738 Ok(SystemConfig::new(
739 1,
740 administrative_secret_handling,
741 non_administrative_secret_handling,
742 mappings,
743 )?)
744 }
745
746 #[rstest]
747 fn system_config_iteration(system_config: TestResult<SystemConfig>) -> TestResult {
748 let system_config = system_config?;
749 assert_eq!(system_config.iteration(), 1);
750
751 Ok(())
752 }
753
754 #[rstest]
755 fn system_config_admin_secret_handling(
756 system_config: TestResult<SystemConfig>,
757 administrative_secret_handling: AdministrativeSecretHandling,
758 ) -> TestResult {
759 let system_config = system_config?;
760 assert_eq!(
761 system_config.admin_secret_handling(),
762 &administrative_secret_handling
763 );
764
765 Ok(())
766 }
767
768 #[rstest]
769 fn system_config_non_admin_secret_handling(
770 system_config: TestResult<SystemConfig>,
771 non_administrative_secret_handling: NonAdministrativeSecretHandling,
772 ) -> TestResult {
773 let system_config = system_config?;
774 assert_eq!(
775 system_config.non_admin_secret_handling(),
776 &non_administrative_secret_handling
777 );
778
779 Ok(())
780 }
781
782 #[rstest]
783 fn system_config_mappings(
784 system_config: TestResult<SystemConfig>,
785 mappings: TestResult<BTreeSet<SystemUserMapping>>,
786 ) -> TestResult {
787 let system_config = system_config?;
788 let mappings = mappings?;
789 assert_eq!(system_config.mappings(), &mappings);
790
791 Ok(())
792 }
793
794 #[rstest]
795 fn system_config_authorized_key_entries(
796 system_config: TestResult<SystemConfig>,
797 mappings: TestResult<BTreeSet<SystemUserMapping>>,
798 ) -> TestResult {
799 let system_config = system_config?;
800 let mappings = mappings?;
801 let authorized_keys = mappings
802 .iter()
803 .filter_map(|mapping| mapping.authorized_key_entry())
804 .collect::<HashSet<_>>();
805 assert_eq!(system_config.authorized_key_entries(), authorized_keys);
806
807 Ok(())
808 }
809
810 #[rstest]
811 fn system_config_system_user_ids(
812 system_config: TestResult<SystemConfig>,
813 mappings: TestResult<BTreeSet<SystemUserMapping>>,
814 ) -> TestResult {
815 let system_config = system_config?;
816 let mappings = mappings?;
817 let system_user_ids = mappings
818 .iter()
819 .filter_map(|mapping| mapping.system_user_id())
820 .collect::<HashSet<_>>();
821 assert_eq!(system_config.system_user_ids(), system_user_ids);
822
823 Ok(())
824 }
825}