1use std::{fmt::Display, path::PathBuf};
4
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 Error,
10 config::technology::openpgp::{OpenpgpSettings, VerificationMethod},
11 file::{ConfigOpenpgpSettings, ConfigTechnologySettings, ConfigVerificationMethod},
12};
13
14#[derive(Clone, Copy, Debug)]
28pub(crate) struct TechnologySettingsDefaults<'a> {
29 pub(crate) built_in_defaults: &'a TechnologySettings,
30 pub(crate) os_defaults: Option<&'a TechnologySettings>,
31}
32
33impl<'a> TechnologySettingsDefaults<'a> {
34 pub(crate) fn matching_openpgp_settings(
38 &self,
39 config: &ConfigOpenpgpSettings,
40 ) -> Option<(&OpenpgpSettings, &[ConfigOrigin])> {
41 match config.config_verification_method() {
42 ConfigVerificationMethod::Plain(_) => {
43 if matches!(
44 self.os_defaults,
45 Some(TechnologySettings {
46 openpgp: OpenpgpSettings {
47 verification_method: VerificationMethod::Plain(_),
48 ..
49 },
50 ..
51 })
52 ) {
53 self.os_defaults
54 .map(|settings| (settings.openpgp_settings(), settings.origins()))
55 } else if matches!(
56 self.built_in_defaults,
57 TechnologySettings {
58 openpgp: OpenpgpSettings {
59 verification_method: VerificationMethod::Plain(_),
60 ..
61 },
62 ..
63 }
64 ) {
65 Some((
66 self.built_in_defaults.openpgp_settings(),
67 self.built_in_defaults.origins(),
68 ))
69 } else {
70 None
71 }
72 }
73 ConfigVerificationMethod::TrustAnchor(_) => {
74 if matches!(
75 self.os_defaults,
76 Some(TechnologySettings {
77 openpgp: OpenpgpSettings {
78 verification_method: VerificationMethod::TrustAnchor(_),
79 ..
80 },
81 ..
82 })
83 ) {
84 self.os_defaults
85 .map(|settings| (settings.openpgp_settings(), settings.origins()))
86 } else if matches!(
87 self.built_in_defaults,
88 TechnologySettings {
89 openpgp: OpenpgpSettings {
90 verification_method: VerificationMethod::TrustAnchor(_),
91 ..
92 },
93 ..
94 }
95 ) {
96 Some((
97 self.built_in_defaults.openpgp_settings(),
98 self.built_in_defaults.origins(),
99 ))
100 } else {
101 None
102 }
103 }
104 ConfigVerificationMethod::WebOfTrust(_) => {
105 if matches!(
106 self.os_defaults,
107 Some(TechnologySettings {
108 openpgp: OpenpgpSettings {
109 verification_method: VerificationMethod::WebOfTrust(_),
110 ..
111 },
112 ..
113 })
114 ) {
115 self.os_defaults
116 .map(|settings| (settings.openpgp_settings(), settings.origins()))
117 } else if matches!(
118 self.built_in_defaults,
119 TechnologySettings {
120 openpgp: OpenpgpSettings {
121 verification_method: VerificationMethod::WebOfTrust(_),
122 ..
123 },
124 ..
125 }
126 ) {
127 Some((
128 self.built_in_defaults.openpgp_settings(),
129 self.built_in_defaults.origins(),
130 ))
131 } else {
132 None
133 }
134 }
135 }
136 }
137}
138
139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
141#[serde(rename_all = "snake_case")]
142pub enum ConfigOrigin {
143 ConfigFile(PathBuf),
145
146 DropInFile(PathBuf),
148
149 Default,
151}
152
153impl Display for ConfigOrigin {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 Self::ConfigFile(path) => write!(f, "Config file: {}", path.to_string_lossy()),
157 Self::DropInFile(path) => {
158 write!(f, "Drop-in config file: {}", path.to_string_lossy())
159 }
160 Self::Default => write!(f, "Built-in defaults"),
161 }
162 }
163}
164
165#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Validate)]
170pub struct TechnologySettings {
171 #[garde(skip)]
172 pub(crate) origins: Vec<ConfigOrigin>,
173
174 #[garde(dive)]
175 openpgp: OpenpgpSettings,
176}
177
178impl TechnologySettings {
179 pub fn new(origins: Vec<ConfigOrigin>, openpgp: OpenpgpSettings) -> Self {
181 Self { origins, openpgp }
182 }
183
184 pub fn origins(&self) -> &[ConfigOrigin] {
187 &self.origins
188 }
189
190 pub(crate) fn from_config_with_defaults(
198 config: &ConfigTechnologySettings,
199 defaults: TechnologySettingsDefaults,
200 ) -> Result<Self, Error> {
201 let (openpgp_settings, origins) =
202 if let Some(config_openpgp_settings) = config.config_openpgp_settings() {
203 let (openpgp_settings_defaults, origins) = if let Some(openpgp_settings) =
206 defaults.matching_openpgp_settings(config_openpgp_settings)
207 {
208 openpgp_settings
209 } else {
212 (
213 &OpenpgpSettings::default(),
214 [ConfigOrigin::Default].as_slice(),
215 )
216 };
217
218 (
219 OpenpgpSettings::from_config_with_defaults(
220 config_openpgp_settings,
221 openpgp_settings_defaults,
222 )?,
223 origins,
224 )
225 } else if let Some(os_level) = defaults.os_defaults {
228 (os_level.openpgp_settings().clone(), os_level.origins())
229 } else {
230 (
231 defaults.built_in_defaults.openpgp_settings().clone(),
232 defaults.built_in_defaults.origins(),
233 )
234 };
235 let origins = [config.origins(), origins].concat();
237
238 Ok(TechnologySettings::new(origins, openpgp_settings))
239 }
240
241 pub fn from_yaml_str(s: &str) -> Result<Self, Error> {
243 let settings: Self =
244 serde_saphyr::from_str(s).map_err(|source| Error::YamlDeserialize {
245 context: "deserializing a YAML-based system-level configuration string".to_string(),
246 source,
247 })?;
248
249 settings.validate().map_err(|source| Error::Validation {
250 context: "creating technology settings from a YAML string".to_string(),
251 source,
252 })?;
253
254 Ok(settings)
255 }
256
257 pub fn openpgp_settings(&self) -> &OpenpgpSettings {
259 &self.openpgp
260 }
261}
262
263impl Default for TechnologySettings {
264 fn default() -> Self {
265 Self {
266 origins: vec![ConfigOrigin::Default],
267 openpgp: OpenpgpSettings::default(),
268 }
269 }
270}
271
272impl Display for TechnologySettings {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 writeln!(f, "{}", self.openpgp_settings())?;
275
276 if !self.origins().is_empty() {
277 writeln!(
278 f,
279 "📝 The following sources have been considered for the creation of the settings:"
280 )?;
281 for origin in self.origins().iter() {
282 writeln!(f, "⤷ {origin}")?;
283 }
284 }
285
286 Ok(())
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use std::{collections::HashSet, num::NonZeroUsize, thread::current};
293
294 use insta::{assert_snapshot, with_settings};
295 use rstest::rstest;
296 use testresult::TestResult;
297
298 use super::*;
299 use crate::{
300 file::{ConfigTrustAnchorMode, ConfigWebOfTrustMode},
301 openpgp::{
302 NumCertifications,
303 NumDataSignatures,
304 PlainMode,
305 TrustAmountFlow,
306 TrustAmountPartial,
307 TrustAmountRoot,
308 TrustAnchorMode,
309 WebOfTrustMode,
310 WebOfTrustRoot,
311 },
312 };
313
314 const SNAPSHOT_PATH: &str = "fixtures/settings/";
315
316 #[rstest]
318 #[case::config_without_openpgp_settings_overriden_with_system_level(
319 ConfigTechnologySettings::new(
320 vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml"))],
321 None,
322 ),
323 TechnologySettingsDefaults{
324 built_in_defaults: &TechnologySettings::new(
325 vec![ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml"))],
326 OpenpgpSettings::new(
327 NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
328 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
329 )?
330 ),
331 os_defaults: None,
332 },
333 TechnologySettings::new(
334 vec![
335 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
336 ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml")),
337 ],
338 OpenpgpSettings::new(
339 NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
340 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
341 )?
342 )
343 )]
344 #[case::config_without_openpgp_settings_overriden_with_os_level(
345 ConfigTechnologySettings::new(
346 vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml"))],
347 None,
348 ),
349 TechnologySettingsDefaults{
350 built_in_defaults: &TechnologySettings::new(
351 vec![ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml"))],
352 OpenpgpSettings::new(
353 NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
354 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
355 )?
356 ),
357 os_defaults: Some(&TechnologySettings::new(
358 vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml"))],
359 OpenpgpSettings::new(
360 NumDataSignatures::new(NonZeroUsize::new(3).expect("2 is larger than 0")),
361 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
362 )?
363 )),
364 },
365 TechnologySettings::new(
366 vec![
367 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml")),
368 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
369 ],
370 OpenpgpSettings::new(
371 NumDataSignatures::new(NonZeroUsize::new(3).expect("2 is larger than 0")),
372 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
373 )?
374 )
375 )]
376 #[case::config_with_wot_openpgp_settings_overridden_by_generic_defaults(
377 ConfigTechnologySettings::new(
378 vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml"))],
379 Some(ConfigOpenpgpSettings::new(
380 Some(NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0"))),
381 ConfigVerificationMethod::WebOfTrust(ConfigWebOfTrustMode::new(None, None, Some(HashSet::new()), Some(HashSet::new())))
382 )?
383 )),
384 TechnologySettingsDefaults{
385 built_in_defaults: &TechnologySettings::new(
386 vec![ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml"))],
387 OpenpgpSettings::new(
388 NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
389 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
390 )?
391 ),
392 os_defaults: Some(&TechnologySettings::new(
393 vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml"))],
394 OpenpgpSettings::new(
395 NumDataSignatures::new(NonZeroUsize::new(3).expect("2 is larger than 0")),
396 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
397 )?
398 )),
399 },
400 TechnologySettings::new(
401 vec![
402 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml")),
403 ConfigOrigin::Default,
404 ],
405 OpenpgpSettings::new(
406 NumDataSignatures::default(),
407 VerificationMethod::WebOfTrust(
408 WebOfTrustMode::new(
409 TrustAmountFlow::default(),
410 TrustAmountPartial::default(),
411 HashSet::new(),
412 HashSet::new(),
413 )
414 )
415 )?
416 )
417 )]
418 #[case::config_with_trust_anchor_openpgp_settings_overridden_by_trust_anchor_defaults(
419 ConfigTechnologySettings::new(
420 vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml"))],
421 Some(ConfigOpenpgpSettings::new(
422 Some(NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0"))),
423 ConfigVerificationMethod::TrustAnchor(ConfigTrustAnchorMode::new(None, Some(HashSet::new()), Some(HashSet::new()))?)
424 )?
425 )),
426 TechnologySettingsDefaults{
427 built_in_defaults: &TechnologySettings::new(
428 vec![ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml"))],
429 OpenpgpSettings::new(
430 NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
431 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
432 )?
433 ),
434 os_defaults: Some(&TechnologySettings::new(
435 vec![ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml"))],
436 OpenpgpSettings::new(
437 NumDataSignatures::new(NonZeroUsize::new(3).expect("2 is larger than 0")),
438 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
439 )?
440 )),
441 },
442 TechnologySettings::new(
443 vec![
444 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/20-example.yaml")),
445 ConfigOrigin::Default,
446 ],
447 OpenpgpSettings::new(
448 NumDataSignatures::default(),
449 VerificationMethod::TrustAnchor(
450 TrustAnchorMode::new(NumCertifications::default(), HashSet::new(), HashSet::new())?
451 )
452 )?
453 )
454 )]
455 fn technology_settings_from_config_with_defaults_succeeds(
456 #[case] config: ConfigTechnologySettings,
457 #[case] defaults: TechnologySettingsDefaults,
458 #[case] expected: TechnologySettings,
459 ) -> TestResult {
460 let settings = TechnologySettings::from_config_with_defaults(&config, defaults)?;
461 assert_eq!(settings, expected);
462
463 Ok(())
464 }
465
466 #[test]
468 fn technology_settings_from_yaml_str_fails_on_invalid_yaml() {
469 let broken_yaml = "foo:\n bar:";
470
471 match TechnologySettings::from_yaml_str(broken_yaml) {
472 Err(Error::YamlDeserialize { .. }) => {}
473 Err(error) => {
474 panic!(
475 "Expected an Error::YamlDeserialize but failed with a different error: {error}"
476 )
477 }
478 Ok(settings) => {
479 panic!("Expected an Error::YamlDeserialize but succeeded: {settings:?}")
480 }
481 }
482 }
483
484 #[test]
485 fn technology_settings_from_yaml_str_fails_on_validation() {
486 let invalid_settings = r#"origins:
487 - config_file: /usr/share/voa/example.yaml
488openpgp:
489 num_data_signatures: 2
490 verification_method:
491 plain:
492 identity_domain_matches:
493 - example.org
494 fingerprint_matches:
495 - d3b0f7c0b825ecbb0f0d7398072947e7b1537b6f
496"#;
497
498 match TechnologySettings::from_yaml_str(invalid_settings) {
499 Err(Error::Validation { .. }) => {}
500 Err(error) => {
501 panic!("Expected an Error::Validation but failed with a different error: {error}")
502 }
503 Ok(settings) => {
504 panic!("Expected an Error::Validation but succeeded: {settings:?}")
505 }
506 }
507 }
508
509 #[rstest]
511 #[case::plain_mode_empty_lists(
512 TechnologySettings::new(
513 vec![
514 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
515 ConfigOrigin::Default,
516 ],
517 OpenpgpSettings::new(
518 NumDataSignatures::default(),
519 VerificationMethod::Plain(PlainMode::new(HashSet::new(), HashSet::new()))
520 )?
521 )
522 )]
523 #[case::plain_mode_custom_lists(
524 TechnologySettings::new(
525 vec![
526 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
527 ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml")),
528 ],
529 OpenpgpSettings::new(
530 NumDataSignatures::default(),
531 VerificationMethod::Plain(PlainMode::new(
532 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
533 HashSet::from_iter([
534 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
535 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
536 ]),
537 ))
538 )?
539 )
540 )]
541 #[case::trust_anchor_empty_lists(
542 TechnologySettings::new(
543 vec![
544 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
545 ConfigOrigin::Default,
546 ],
547 OpenpgpSettings::new(
548 NumDataSignatures::default(),
549 VerificationMethod::TrustAnchor(
550 TrustAnchorMode::new(
551 NumCertifications::default(),
552 HashSet::new(),
553 HashSet::new(),
554 )?
555 )
556 )?
557 )
558 )]
559 #[case::trust_anchor_custom_lists(
560 TechnologySettings::new(
561 vec![
562 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
563 ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml")),
564 ],
565 OpenpgpSettings::new(
566 NumDataSignatures::default(),
567 VerificationMethod::TrustAnchor(
568 TrustAnchorMode::new(
569 NumCertifications::default(),
570 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
571 HashSet::from_iter([
572 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
573 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
574 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
575 ]),
576 )?
577 )
578 )?
579 )
580 )]
581 #[case::wot_empty_lists(
582 TechnologySettings::new(
583 vec![
584 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
585 ConfigOrigin::Default,
586 ],
587 OpenpgpSettings::new(
588 NumDataSignatures::default(),
589 VerificationMethod::WebOfTrust(
590 WebOfTrustMode::new(
591 TrustAmountFlow::default(),
592 TrustAmountPartial::default(),
593 HashSet::new(),
594 HashSet::new(),
595 )
596 )
597 )?
598 )
599 )]
600 #[case::wot_custom_lists(
601 TechnologySettings::new(
602 vec![
603 ConfigOrigin::DropInFile(PathBuf::from("/usr/share/voa/example.yaml.d/10-example.yaml")),
604 ConfigOrigin::ConfigFile(PathBuf::from("/usr/share/voa/example.yaml")),
605 ],
606 OpenpgpSettings::new(
607 NumDataSignatures::default(),
608 VerificationMethod::WebOfTrust(
609 WebOfTrustMode::new(
610 TrustAmountFlow::default(),
611 TrustAmountPartial::default(),
612 HashSet::from_iter([
613 WebOfTrustRoot::new(
614 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
615 TrustAmountRoot::default(),
616 ),
617 WebOfTrustRoot::new(
618 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
619 TrustAmountRoot::default(),
620 ),
621 ]),
622 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
623 )
624 )
625 )?
626 )
627 )]
628 fn technology_settings_display(#[case] settings: TechnologySettings) -> TestResult {
629 let description =
630 "Technology settings with OpenPGP settings that use various custom settings.";
631 let settings_str = settings.to_string();
632
633 with_settings!({
634 description => description,
635 snapshot_path => SNAPSHOT_PATH,
636 prepend_module_to_snapshot => false,
637 }, {
638 assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), settings_str);
639 });
640
641 Ok(())
642 }
643
644 #[rstest]
647 #[case::plain_mode(
648 TechnologySettings::new(
649 vec![
650 ConfigOrigin::Default,
651 ],
652 OpenpgpSettings::new(
653 NumDataSignatures::default(),
654 VerificationMethod::Plain(PlainMode::default())
655 )?
656 )
657 )]
658 #[case::trust_anchor(
659 TechnologySettings::new(
660 vec![
661 ConfigOrigin::Default,
662 ],
663 OpenpgpSettings::new(
664 NumDataSignatures::default(),
665 VerificationMethod::TrustAnchor(TrustAnchorMode::default())
666 )?
667 )
668 )]
669 #[case::wot(
670 TechnologySettings::new(
671 vec![
672 ConfigOrigin::Default,
673 ],
674 OpenpgpSettings::new(
675 NumDataSignatures::default(),
676 VerificationMethod::WebOfTrust(WebOfTrustMode::default())
677 )?
678 )
679 )]
680 fn technology_settings_verification_method_defaults(
681 #[case] settings: TechnologySettings,
682 ) -> TestResult {
683 let description = "Technology settings with OpenPGP settings that use the default settings for each verification method.";
684 let settings_str = settings.to_string();
685
686 with_settings!({
687 description => description,
688 snapshot_path => SNAPSHOT_PATH,
689 prepend_module_to_snapshot => false,
690 }, {
691 assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), settings_str);
692 });
693
694 Ok(())
695 }
696}