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, Ord, PartialEq, PartialOrd, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum SystemUserMapping {
35 ShareHolder {
37 system_user: SystemUserId,
39
40 ssh_authorized_key: AuthorizedKeyEntry,
42 },
43
44 WireGuardDownload {
47 system_user: SystemUserId,
49
50 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
99fn validate_system_config_mappings(
119 admin_secret_handling: &AdministrativeSecretHandling,
120) -> impl FnOnce(&BTreeSet<SystemUserMapping>, &()) -> garde::Result + '_ {
121 move |mappings, _| {
122 let duplicate_system_user_ids = duplicate_system_user_ids(mappings);
124
125 let duplicate_authorized_keys = duplicate_authorized_keys(mappings);
127
128 let num_shares = mappings
130 .iter()
131 .filter(|mapping| matches!(mapping, SystemUserMapping::ShareHolder { .. }))
132 .count();
133
134 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#[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 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 pub fn iteration(&self) -> u32 {
250 self.iteration
251 }
252
253 pub fn admin_secret_handling(&self) -> &AdministrativeSecretHandling {
255 &self.admin_secret_handling
256 }
257
258 pub fn non_admin_secret_handling(&self) -> &NonAdministrativeSecretHandling {
260 &self.non_admin_secret_handling
261 }
262
263 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#[derive(Debug)]
289pub struct SystemUserDiff<'a, 'b> {
290 pub config: &'a SystemUserConfigState<'a>,
292
293 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 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 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 }
338
339 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}