signstar_config/config/base.rs
1//! [`SignstarConfig`] for _Signstar hosts_.
2
3use std::{
4 collections::HashSet,
5 fs::{File, create_dir_all, read_to_string},
6 io::Write,
7 path::Path,
8};
9
10use nethsm::{Connection, NamespaceId};
11use serde::{Deserialize, Serialize};
12use signstar_common::config::{get_config_file, get_run_override_config_file_path};
13
14use crate::{
15 ConfigError as Error,
16 SystemUserId,
17 config::mapping::{ExtendedUserMapping, UserMapping},
18};
19
20/// The handling of administrative secrets.
21///
22/// Administrative secrets may be handled in different ways (e.g. persistent or non-persistent).
23#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(rename_all = "kebab-case")]
25pub enum AdministrativeSecretHandling {
26 /// The administrative secrets are handled in a plaintext file in a non-volatile directory.
27 ///
28 /// ## Warning
29 ///
30 /// This variant should only be used in non-production test setups, as it implies the
31 /// persistence of unencrypted administrative secrets on a file system.
32 Plaintext,
33
34 /// The administrative secrets are handled in a file encrypted using [systemd-creds] in a
35 /// non-volatile directory.
36 ///
37 /// ## Warning
38 ///
39 /// This variant should only be used in non-production test setups, as it implies the
40 /// persistence of (host-specific) encrypted administrative secrets on a file system, that
41 /// could be extracted if the host is compromised.
42 ///
43 /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
44 SystemdCreds,
45
46 /// The administrative secrets are handled using [Shamir's Secret Sharing] (SSS).
47 ///
48 /// This variant is the default for production use, as the administrative secrets are only ever
49 /// exposed on a volatile filesystem for the time of their use.
50 /// The secrets are only made available to the system as shares of a shared secret, split using
51 /// SSS.
52 /// This way no holder of a share is aware of the administrative secrets and the system only
53 /// for as long as it needs to use the administrative secrets.
54 ///
55 /// [Shamir's Secret Sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
56 #[default]
57 ShamirsSecretSharing,
58}
59
60/// The handling of non-administrative secrets.
61///
62/// Non-administrative secrets represent passphrases for (non-Administrator) NetHSM users and may be
63/// handled in different ways (e.g. encrypted or not encrypted).
64#[derive(
65 Clone,
66 Copy,
67 Debug,
68 Default,
69 Deserialize,
70 strum::Display,
71 strum::EnumString,
72 Eq,
73 PartialEq,
74 Serialize,
75)]
76#[serde(rename_all = "kebab-case")]
77#[strum(serialize_all = "kebab-case")]
78pub enum NonAdministrativeSecretHandling {
79 /// Each non-administrative secret is handled in a plaintext file in a non-volatile
80 /// directory.
81 ///
82 /// ## Warning
83 ///
84 /// This variant should only be used in non-production test setups, as it implies the
85 /// persistence of unencrypted non-administrative secrets on a file system.
86 Plaintext,
87
88 /// Each non-administrative secret is encrypted for a specific system user using
89 /// [systemd-creds] and the resulting files are stored in a non-volatile directory.
90 ///
91 /// ## Note
92 ///
93 /// Although secrets are stored as encrypted strings in dedicated files, they may be extracted
94 /// under certain circumstances:
95 ///
96 /// - the root account is compromised
97 /// - decrypts and exfiltrates _all_ secrets
98 /// - the secret is not encrypted using a [TPM] and the file
99 /// `/var/lib/systemd/credential.secret` as well as _any_ encrypted secret is exfiltrated
100 /// - a specific user is compromised, decrypts and exfiltrates its own secret
101 ///
102 /// It is therefore crucial to follow common best-practices:
103 ///
104 /// - rely on a [TPM] for encrypting secrets, so that files become host-specific
105 /// - heavily guard access to all users, especially root
106 ///
107 /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
108 /// [TPM]: https://en.wikipedia.org/wiki/Trusted_Platform_Module
109 #[default]
110 SystemdCreds,
111}
112
113/// A configuration for parallel use of connections with a set of system and NetHSM users.
114///
115/// This configuration type is meant to be used in a read-only fashion and does not support tracking
116/// the passphrases for users.
117/// As such, it is useful for tools, that create system users, as well as NetHSM users and keys
118/// according to it.
119///
120/// Various mappings of system and NetHSM users exist, that are defined by the variants of
121/// [`UserMapping`].
122///
123/// Some system users require providing SSH authorized key(s), while others do not allow that at
124/// all.
125/// NetHSM users can be added in namespaces, or system-wide, depending on their use-case.
126/// System and NetHSM users must be unique.
127///
128/// Key IDs must be unique per namespace or system-wide (depending on where they are used).
129/// Tags, used to provide access to keys for NetHSM users must be unique per namespace or
130/// system-wide (depending on in which scope the user and key are used)
131///
132/// # Examples
133///
134/// The below example provides a fully functional TOML configuration, outlining all available
135/// functionalities.
136///
137/// ```
138/// # use std::io::Write;
139/// #
140/// # use signstar_config::{SignstarConfig};
141/// #
142/// # fn main() -> testresult::TestResult {
143/// # let config_file = testdir::testdir!().join("signstar_config_example.conf");
144/// # {
145/// let config_string = r#"
146/// ## A non-negative integer, that describes the iteration of the configuration.
147/// ## The iteration should only ever be increased between changes to the config and only under the circumstance,
148/// ## that user mappings are removed and should also be removed from the state of the system making use of this
149/// ## configuration.
150/// ## Applications reading the configuration are thereby enabled to compare existing state on the system with the
151/// ## current iteration and remove user mappings and accompanying data accordingly.
152/// iteration = 1
153///
154/// ## The handling of administrative secrets on the system.
155/// ## One of:
156/// ## - "shamirs-secret-sharing": Administrative secrets are never persisted on the system and only provided as shares of a shared secret.
157/// ## - "systemd-creds": Administrative secrets are persisted on the system as host-specific files, encrypted using systemd-creds (only for testing).
158/// ## - "plaintext": Administrative secrets are persisted on the system in unencrypted plaintext files (only for testing).
159/// admin_secret_handling = "shamirs-secret-sharing"
160///
161/// ## The handling of non-administrative secrets on the system.
162/// ## One of:
163/// ## - "systemd-creds": Non-administrative secrets are persisted on the system as host-specific files, encrypted using systemd-creds (the default).
164/// ## - "plaintext": Non-administrative secrets are persisted on the system in unencrypted plaintext files (only for testing).
165/// non_admin_secret_handling = "systemd-creds"
166///
167/// [[connections]]
168/// url = "https://localhost:8443/api/v1/"
169/// tls_security = "Unsafe"
170///
171/// ## The NetHSM user "admin" is a system-wide Administrator
172/// [[users]]
173/// nethsm_only_admin = "admin"
174///
175/// ## The SSH-accessible system user "ssh-backup1" is used in conjunction with
176/// ## the NetHSM user "backup1" (system-wide Backup)
177/// [[users]]
178///
179/// [users.system_nethsm_backup]
180/// nethsm_user = "backup1"
181/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
182/// system_user = "ssh-backup1"
183///
184/// ## The SSH-accessible system user "ssh-metrics1" is used with several NetHSM users:
185/// ## - "metrics1" (system-wide Metrics)
186/// ## - "keymetrics1" (system-wide Operator)
187/// ## - "ns1~keymetrics1" (namespace Operator)
188/// [[users]]
189///
190/// [users.system_nethsm_metrics]
191/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
192/// system_user = "ssh-metrics1"
193///
194/// [users.system_nethsm_metrics.nethsm_users]
195/// metrics_user = "metrics1"
196/// operator_users = ["keymetrics1", "ns1~keymetrics1"]
197///
198/// ## The SSH-accessible system user "ssh-operator1" is used in conjunction with
199/// ## the NetHSM user "operator1" (system-wide Operator).
200/// ## User "operator1" shares tag "tag1" with key "key1" and can therefore use it
201/// ## (for OpenPGP signing).
202/// [[users]]
203///
204/// [users.system_nethsm_operator_signing]
205/// nethsm_user = "operator1"
206/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
207/// system_user = "ssh-operator1"
208/// tag = "tag1"
209///
210/// [users.system_nethsm_operator_signing.nethsm_key_setup]
211/// key_id = "key1"
212/// key_type = "Curve25519"
213/// key_mechanisms = ["EdDsaSignature"]
214/// signature_type = "EdDsa"
215///
216/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
217/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
218/// version = "4"
219///
220/// ## The SSH-accessible system user "ssh-operator2" is used in conjunction with
221/// ## the NetHSM user "operator2" (system-wide Operator).
222/// ## User "operator2" shares tag "tag2" with key "key2" and can therefore use it
223/// ## (for OpenPGP signing).
224/// [[users]]
225///
226/// [users.system_nethsm_operator_signing]
227/// nethsm_user = "operator2"
228/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
229/// system_user = "ssh-operator2"
230/// tag = "tag2"
231///
232/// [users.system_nethsm_operator_signing.nethsm_key_setup]
233/// key_id = "key2"
234/// key_type = "Curve25519"
235/// key_mechanisms = ["EdDsaSignature"]
236/// signature_type = "EdDsa"
237///
238/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
239/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
240/// version = "4"
241///
242/// ## The NetHSM user "ns1~admin" is a namespace Administrator
243/// [[users]]
244/// nethsm_only_admin = "ns1~admin"
245///
246/// ## The SSH-accessible system user "ns1-ssh-operator1" is used in conjunction with
247/// ## the NetHSM user "ns1~operator1" (namespace Operator).
248/// ## User "ns1~operator1" shares tag "tag1" with key "key1" and can therefore use it
249/// ## in its namespace (for OpenPGP signing).
250/// [[users]]
251///
252/// [users.system_nethsm_operator_signing]
253/// nethsm_user = "ns1~operator1"
254/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host"
255/// system_user = "ns1-ssh-operator1"
256/// tag = "tag1"
257///
258/// [users.system_nethsm_operator_signing.nethsm_key_setup]
259/// key_id = "key1"
260/// key_type = "Curve25519"
261/// key_mechanisms = ["EdDsaSignature"]
262/// signature_type = "EdDsa"
263///
264/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
265/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
266/// version = "4"
267///
268/// ## The SSH-accessible system user "ns1-ssh-operator2" is used in conjunction with
269/// ## the NetHSM user "ns2~operator1" (namespace Operator).
270/// ## User "ns1~operator2" shares tag "tag2" with key "key1" and can therefore use it
271/// ## in its namespace (for OpenPGP signing).
272/// [[users]]
273///
274/// [users.system_nethsm_operator_signing]
275/// nethsm_user = "ns1~operator2"
276/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIYA+bfMBThUP5lKbMFEHiytmcCPhpkGrB/85n0mAN user@host"
277/// system_user = "ns1-ssh-operator2"
278/// tag = "tag2"
279///
280/// [users.system_nethsm_operator_signing.nethsm_key_setup]
281/// key_id = "key2"
282/// key_type = "Curve25519"
283/// key_mechanisms = ["EdDsaSignature"]
284/// signature_type = "EdDsa"
285///
286/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
287/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
288/// version = "4"
289///
290/// ## The hermetic system user "local-metrics1" is used with several NetHSM users:
291/// ## - "metrics2" (system-wide Metrics)
292/// ## - "keymetrics2" (system-wide Operator)
293/// ## - "ns1~keymetrics2" (namespace Operator)
294/// [[users]]
295///
296/// [users.hermetic_system_nethsm_metrics]
297/// system_user = "local-metrics1"
298///
299/// [users.hermetic_system_nethsm_metrics.nethsm_users]
300/// metrics_user = "metrics2"
301/// operator_users = ["keymetrics2", "ns1~keymetrics2"]
302///
303/// ## The SSH-accessible system user "ssh-share-down" is used for the
304/// ## download of shares of a shared secret (divided by Shamir's Secret Sharing).
305/// [[users]]
306///
307/// [users.system_only_share_download]
308/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"
309/// system_user = "ssh-share-down"
310///
311/// ## The SSH-accessible system user "ssh-share-up" is used for the
312/// ## upload of shares of a shared secret (divided by Shamir's Secret Sharing).
313/// [[users]]
314///
315/// [users.system_only_share_upload]
316/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"
317/// system_user = "ssh-share-up"
318///
319/// ## The SSH-accessible system user "ssh-wireguard-down" is used for the
320/// ## download of WireGuard configuration, used on the host.
321/// [[users]]
322///
323/// [users.system_only_wireguard_download]
324/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"
325/// system_user = "ssh-wireguard-down"
326/// "#;
327/// #
328/// # let mut buffer = std::fs::File::create(&config_file)?;
329/// # buffer.write_all(config_string.as_bytes())?;
330/// # }
331/// # SignstarConfig::new_from_file(
332/// # Some(&config_file),
333/// # )?;
334/// # Ok(())
335/// # }
336/// ```
337#[derive(Clone, Debug, Default, Deserialize, Serialize)]
338pub struct SignstarConfig {
339 iteration: u32,
340 admin_secret_handling: AdministrativeSecretHandling,
341 non_admin_secret_handling: NonAdministrativeSecretHandling,
342 connections: HashSet<Connection>,
343 users: HashSet<UserMapping>,
344}
345
346impl SignstarConfig {
347 /// Creates a new [`SignstarConfig`] from an optional configuration file path.
348 ///
349 /// If no configuration file path is provided, attempts to return the first configuration file
350 /// location found using [`get_config_file`].
351 ///
352 /// # Errors
353 ///
354 /// Returns an error if
355 ///
356 /// - no configuration file path is provided and [`get_config_file`] is unable to find any,
357 /// - reading the contents of the configuration file to string fails,
358 /// - deserializing the contents of the configuration file as a [`SignstarConfig`],
359 /// - or the [`SignstarConfig`] fails to validate.
360 ///
361 /// # Examples
362 ///
363 /// ```
364 /// # use std::io::Write;
365 ///
366 /// use signstar_config::SignstarConfig;
367 ///
368 /// # fn main() -> testresult::TestResult {
369 /// let config_file = testdir::testdir!().join("signstar_config_new.conf");
370 /// {
371 /// #[rustfmt::skip]
372 /// let config_string = r#"
373 /// iteration = 1
374 /// admin_secret_handling = "shamirs-secret-sharing"
375 /// non_admin_secret_handling = "systemd-creds"
376 /// [[connections]]
377 /// url = "https://localhost:8443/api/v1/"
378 /// tls_security = "Unsafe"
379 ///
380 /// [[users]]
381 /// nethsm_only_admin = "admin"
382 ///
383 /// [[users]]
384 /// [users.system_nethsm_backup]
385 /// nethsm_user = "backup1"
386 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
387 /// system_user = "ssh-backup1"
388 ///
389 /// [[users]]
390 ///
391 /// [users.system_nethsm_metrics]
392 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
393 /// system_user = "ssh-metrics1"
394 ///
395 /// [users.system_nethsm_metrics.nethsm_users]
396 /// metrics_user = "metrics1"
397 /// operator_users = ["operator1metrics1"]
398 ///
399 /// [[users]]
400 /// [users.system_nethsm_operator_signing]
401 /// nethsm_user = "operator1"
402 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
403 /// system_user = "ssh-operator1"
404 /// tag = "tag1"
405 ///
406 /// [users.system_nethsm_operator_signing.nethsm_key_setup]
407 /// key_id = "key1"
408 /// key_type = "Curve25519"
409 /// key_mechanisms = ["EdDsaSignature"]
410 /// signature_type = "EdDsa"
411 ///
412 /// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
413 /// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
414 /// version = "4"
415 ///
416 /// [[users]]
417 /// [users.system_nethsm_operator_signing]
418 /// nethsm_user = "operator2"
419 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
420 /// system_user = "ssh-operator2"
421 /// tag = "tag2"
422 ///
423 /// [users.system_nethsm_operator_signing.nethsm_key_setup]
424 /// key_id = "key2"
425 /// key_type = "Curve25519"
426 /// key_mechanisms = ["EdDsaSignature"]
427 /// signature_type = "EdDsa"
428 ///
429 /// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
430 /// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
431 /// version = "4"
432 ///
433 /// [[users]]
434 ///
435 /// [users.hermetic_system_nethsm_metrics]
436 /// system_user = "local-metrics1"
437 ///
438 /// [users.hermetic_system_nethsm_metrics.nethsm_users]
439 /// metrics_user = "metrics2"
440 /// operator_users = ["operator2metrics1"]
441 ///
442 /// [[users]]
443 /// [users.system_only_share_download]
444 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"
445 /// system_user = "ssh-share-down"
446 ///
447 /// [[users]]
448 /// [users.system_only_share_upload]
449 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"
450 /// system_user = "ssh-share-up"
451 ///
452 /// [[users]]
453 /// [users.system_only_wireguard_download]
454 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"
455 /// system_user = "ssh-wireguard-down"
456 /// "#;
457 /// let mut buffer = std::fs::File::create(&config_file)?;
458 /// buffer.write_all(config_string.as_bytes())?;
459 /// }
460 /// SignstarConfig::new_from_file(Some(&config_file))?;
461 /// # Ok(())
462 /// # }
463 /// ```
464 pub fn new_from_file(path: Option<&Path>) -> Result<Self, crate::Error> {
465 let path = if let Some(path) = path {
466 path.to_path_buf()
467 } else {
468 let Some(path) = get_config_file() else {
469 return Err(Error::ConfigIsMissing.into());
470 };
471 path
472 };
473
474 let config: Self =
475 toml::from_str(
476 &read_to_string(&path).map_err(|source| crate::Error::IoPath {
477 path: path.clone(),
478 context: "reading it to string",
479 source,
480 })?,
481 )
482 .map_err(|source| crate::Error::TomlRead {
483 path,
484 context: "reading it as a Signstar config",
485 source: Box::new(source),
486 })?;
487 config.validate()?;
488
489 Ok(config)
490 }
491
492 /// Creates a new [`SignstarConfig`].
493 ///
494 /// # Errors
495 ///
496 /// Returns an error if the configuration file can not be loaded.
497 ///
498 /// # Examples
499 ///
500 /// ```
501 /// use std::collections::HashSet;
502 ///
503 /// use nethsm::{Connection, UserRole};
504 /// use signstar_config::{
505 /// AdministrativeSecretHandling,
506 /// SignstarConfig,
507 /// NonAdministrativeSecretHandling,
508 /// UserMapping,
509 /// };
510 ///
511 /// # fn main() -> testresult::TestResult {
512 /// SignstarConfig::new(
513 /// 1,
514 /// AdministrativeSecretHandling::ShamirsSecretSharing,
515 /// NonAdministrativeSecretHandling::SystemdCreds,
516 /// HashSet::from([Connection::new(
517 /// "https://localhost:8443/api/v1/".parse()?,
518 /// "Unsafe".parse()?,
519 /// )]),
520 /// HashSet::from([
521 /// UserMapping::NetHsmOnlyAdmin("admin".parse()?),
522 /// UserMapping::SystemOnlyShareDownload {
523 /// system_user: "ssh-share-down".parse()?,
524 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
525 /// },
526 /// UserMapping::SystemOnlyShareUpload {
527 /// system_user: "ssh-share-up".parse()?,
528 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
529 /// }]),
530 /// )?;
531 /// # Ok(())
532 /// # }
533 /// ```
534 pub fn new(
535 iteration: u32,
536 admin_secret_handling: AdministrativeSecretHandling,
537 non_admin_secret_handling: NonAdministrativeSecretHandling,
538 connections: HashSet<Connection>,
539 users: HashSet<UserMapping>,
540 ) -> Result<Self, crate::Error> {
541 let config = Self {
542 iteration,
543 admin_secret_handling,
544 non_admin_secret_handling,
545 connections,
546 users,
547 };
548 config.validate()?;
549 Ok(config)
550 }
551
552 /// Writes a [`SignstarConfig`] to file.
553 ///
554 /// # Errors
555 ///
556 /// Returns an error if
557 ///
558 /// - the parent directory for the configuration file cannot be created,
559 /// - the configuration file cannot be created,
560 /// - `self` cannot be serialized into a TOML string,
561 /// - or the TOML string cannot be written to the configuration file.
562 ///
563 /// # Examples
564 ///
565 /// ```
566 /// use std::collections::HashSet;
567 ///
568 /// use nethsm::{Connection,CryptographicKeyContext, OpenPgpUserIdList, SigningKeySetup, UserRole};
569 /// use signstar_config::{
570 /// AdministrativeSecretHandling,
571 /// SignstarConfig,
572 /// NetHsmMetricsUsers,
573 /// NonAdministrativeSecretHandling,
574 /// UserMapping,
575 /// };
576 ///
577 /// # fn main() -> testresult::TestResult {
578 /// let config = SignstarConfig::new(
579 /// 1,
580 /// AdministrativeSecretHandling::ShamirsSecretSharing,
581 /// NonAdministrativeSecretHandling::SystemdCreds,
582 /// HashSet::from([Connection::new(
583 /// "https://localhost:8443/api/v1/".parse()?,
584 /// "Unsafe".parse()?,
585 /// )]),
586 /// HashSet::from([UserMapping::NetHsmOnlyAdmin("admin".parse()?),
587 /// UserMapping::SystemNetHsmBackup {
588 /// nethsm_user: "backup1".parse()?,
589 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
590 /// system_user: "ssh-backup1".parse()?,
591 /// },
592 /// UserMapping::SystemNetHsmMetrics {
593 /// nethsm_users: NetHsmMetricsUsers::new("metrics1".parse()?, vec!["operator2metrics1".parse()?])?,
594 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIioJ9uvAxUPunFh89T+ENo7OQerqHE8SQ+2v4VWbfUZ user@host".parse()?,
595 /// system_user: "ssh-metrics1".parse()?,
596 /// },
597 /// UserMapping::SystemNetHsmOperatorSigning {
598 /// nethsm_user: "operator1".parse()?,
599 /// nethsm_key_setup: SigningKeySetup::new(
600 /// "key1".parse()?,
601 /// "Curve25519".parse()?,
602 /// vec!["EdDsaSignature".parse()?],
603 /// None,
604 /// "EdDsa".parse()?,
605 /// CryptographicKeyContext::OpenPgp{
606 /// user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
607 /// version: "4".parse()?,
608 /// })?,
609 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
610 /// system_user: "ssh-operator1".parse()?,
611 /// tag: "tag1".to_string(),
612 /// },
613 /// UserMapping::HermeticSystemNetHsmMetrics {
614 /// nethsm_users: NetHsmMetricsUsers::new("metrics2".parse()?, vec!["operator1metrics1".parse()?])?,
615 /// system_user: "local-metrics1".parse()?,
616 /// },
617 /// UserMapping::SystemOnlyShareDownload {
618 /// system_user: "ssh-share-down".parse()?,
619 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
620 /// },
621 /// UserMapping::SystemOnlyShareUpload {
622 /// system_user: "ssh-share-up".parse()?,
623 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
624 /// },
625 /// UserMapping::SystemOnlyWireGuardDownload {
626 /// system_user: "ssh-wireguard-down".parse()?,
627 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
628 /// },
629 /// ]),
630 /// )?;
631 ///
632 /// let config_file = testdir::testdir!().join("signstar_config_store.conf");
633 /// config.store(Some(&config_file))?;
634 /// # println!("{}", std::fs::read_to_string(&config_file)?);
635 /// # Ok(())
636 /// # }
637 /// ```
638 pub fn store(&self, path: Option<&Path>) -> Result<(), crate::Error> {
639 let path = if let Some(path) = path {
640 path.to_path_buf()
641 } else {
642 get_run_override_config_file_path()
643 };
644
645 if let Some(parent) = path.parent() {
646 create_dir_all(parent).map_err(|source| crate::Error::IoPath {
647 path: parent.to_path_buf(),
648 context: "creating the parent directory for a Signstar configuration",
649 source,
650 })?;
651 }
652 let mut output = File::create(&path).map_err(|source| crate::Error::IoPath {
653 path: path.clone(),
654 context: "creating a Signstar configuration file",
655 source,
656 })?;
657
658 write!(
659 output,
660 "{}",
661 toml::to_string_pretty(self).map_err(|source| crate::Error::TomlWrite {
662 path: path.clone(),
663 context: "creating a Signstar configuration",
664 source,
665 })?
666 )
667 .map_err(|source| crate::Error::IoPath {
668 path: path.clone(),
669 context: "writing to the Signstar configuration file",
670 source,
671 })
672 }
673
674 /// Returns an Iterator over the available [`Connection`]s.
675 pub fn iter_connections(&self) -> impl Iterator<Item = &Connection> {
676 self.connections.iter()
677 }
678
679 /// Returns an Iterator over the available [`UserMapping`]s.
680 pub fn iter_user_mappings(&self) -> impl Iterator<Item = &UserMapping> {
681 self.users.iter()
682 }
683
684 /// Returns the iteration.
685 pub fn get_iteration(&self) -> u32 {
686 self.iteration
687 }
688
689 /// Returns the [`AdministrativeSecretHandling`].
690 pub fn get_administrative_secret_handling(&self) -> AdministrativeSecretHandling {
691 self.admin_secret_handling
692 }
693
694 /// Returns the [`NonAdministrativeSecretHandling`].
695 pub fn get_non_administrative_secret_handling(&self) -> NonAdministrativeSecretHandling {
696 self.non_admin_secret_handling
697 }
698
699 /// Returns an [`ExtendedUserMapping`] for a system user of `name` if it exists.
700 ///
701 /// Returns [`None`] if no user of `name` can is found.
702 pub fn get_extended_mapping_for_user(&self, name: &str) -> Option<ExtendedUserMapping> {
703 for user_mapping in self.users.iter() {
704 if user_mapping
705 .get_system_user()
706 .is_some_and(|system_user| system_user.as_ref() == name)
707 {
708 return Some(ExtendedUserMapping::new(
709 self.admin_secret_handling,
710 self.non_admin_secret_handling,
711 self.connections.clone(),
712 user_mapping.clone(),
713 ));
714 }
715 }
716 None
717 }
718
719 /// Validates the components of the [`SignstarConfig`].
720 fn validate(&self) -> Result<(), crate::Error> {
721 // ensure there are no duplicate system users
722 {
723 let mut system_users = HashSet::new();
724 for system_user_id in self
725 .users
726 .iter()
727 .filter_map(|mapping| mapping.get_system_user())
728 {
729 if !system_users.insert(system_user_id.clone()) {
730 return Err(Error::DuplicateSystemUserId {
731 system_user_id: system_user_id.clone(),
732 }
733 .into());
734 }
735 }
736 }
737
738 // ensure there are no duplicate NetHsm users
739 {
740 let mut nethsm_users = HashSet::new();
741 for nethsm_user_id in self
742 .users
743 .iter()
744 .flat_map(|mapping| mapping.get_nethsm_users())
745 {
746 if !nethsm_users.insert(nethsm_user_id.clone()) {
747 return Err(Error::DuplicateNetHsmUserId {
748 nethsm_user_id: nethsm_user_id.clone(),
749 }
750 .into());
751 }
752 }
753 }
754
755 // ensure that there is at least one system-wide administrator
756 if self
757 .users
758 .iter()
759 .filter_map(|mapping| {
760 if let UserMapping::NetHsmOnlyAdmin(user_id) = mapping {
761 if !user_id.is_namespaced() {
762 Some(user_id)
763 } else {
764 None
765 }
766 } else {
767 None
768 }
769 })
770 .next()
771 .is_none()
772 {
773 return Err(Error::MissingAdministrator { namespaces: None }.into());
774 }
775
776 // ensure that there is an Administrator in each used namespace
777 {
778 // namespaces for all users, that are not in the Administrator role
779 let namespaces_users = self
780 .users
781 .iter()
782 .filter(|mapping| !matches!(mapping, UserMapping::NetHsmOnlyAdmin(_)))
783 .flat_map(|mapping| mapping.get_namespaces())
784 .collect::<HashSet<NamespaceId>>();
785 // namespaces for all users, that are in the Administrator role
786 let namespaces_admins = self
787 .users
788 .iter()
789 .filter(|mapping| matches!(mapping, UserMapping::NetHsmOnlyAdmin(_)))
790 .flat_map(|mapping| mapping.get_namespaces())
791 .collect::<HashSet<NamespaceId>>();
792
793 let namespaces = namespaces_users
794 .difference(&namespaces_admins)
795 .cloned()
796 .collect::<Vec<NamespaceId>>();
797 if !namespaces.is_empty() {
798 return Err(Error::MissingAdministrator {
799 namespaces: Some(namespaces),
800 }
801 .into());
802 }
803 }
804
805 if self.admin_secret_handling == AdministrativeSecretHandling::ShamirsSecretSharing {
806 // ensure there is at least one system user for downloading shares of a shared
807 // secret
808 if !self
809 .users
810 .iter()
811 .any(|mapping| matches!(mapping, UserMapping::SystemOnlyShareDownload { .. }))
812 {
813 return Err(Error::MissingShareDownloadSystemUser.into());
814 }
815
816 // ensure there is at least one system user for uploading shares of a shared secret
817 if !self
818 .users
819 .iter()
820 .any(|mapping| matches!(mapping, UserMapping::SystemOnlyShareUpload { .. }))
821 {
822 return Err(Error::MissingShareUploadSystemUser.into());
823 }
824 } else {
825 // ensure there is no system user setup for uploading or downloading of shares of a
826 // shared secret
827 let share_users: Vec<SystemUserId> = self
828 .users
829 .iter()
830 .filter_map(|mapping| match mapping {
831 UserMapping::SystemOnlyShareUpload {
832 system_user,
833 ssh_authorized_key: _,
834 }
835 | UserMapping::SystemOnlyShareDownload {
836 system_user,
837 ssh_authorized_key: _,
838 } => Some(system_user.clone()),
839 _ => None,
840 })
841 .collect();
842 if !share_users.is_empty() {
843 return Err(Error::NoSssButShareUsers { share_users }.into());
844 }
845 }
846
847 // ensure there are no duplicate authorized SSH keys in the set of uploading shareholders
848 // and the rest (minus downloading shareholders)
849 {
850 let mut public_keys = HashSet::new();
851 for ssh_authorized_key in self
852 .users
853 .iter()
854 .filter(|mapping| {
855 !matches!(
856 mapping,
857 UserMapping::SystemOnlyShareDownload {
858 system_user: _,
859 ssh_authorized_key: _,
860 }
861 )
862 })
863 .flat_map(|mapping| mapping.get_ssh_authorized_key())
864 // we know a valid Entry can be created from AuthorizedKeyEntry, because its
865 // constructor ensures it, hence we discard Errors
866 .filter_map(|authorized_key| {
867 ssh_key::authorized_keys::Entry::try_from(authorized_key).ok()
868 })
869 {
870 if !public_keys.insert(ssh_authorized_key.public_key().clone()) {
871 return Err(Error::DuplicateSshPublicKey {
872 ssh_public_key: ssh_authorized_key.public_key().to_string(),
873 }
874 .into());
875 }
876 }
877 }
878
879 // ensure there are no duplicate authorized SSH keys in the set of downloading shareholders
880 // and the rest (minus uploading shareholders)
881 {
882 let mut public_keys = HashSet::new();
883 for ssh_authorized_key in self
884 .users
885 .iter()
886 .filter(|mapping| {
887 !matches!(
888 mapping,
889 UserMapping::SystemOnlyShareUpload {
890 system_user: _,
891 ssh_authorized_key: _,
892 }
893 )
894 })
895 .flat_map(|mapping| mapping.get_ssh_authorized_key())
896 // we know a valid Entry can be created from AuthorizedKeyEntry, because its
897 // constructor ensures it, hence we discard Errors
898 .filter_map(|authorized_key| {
899 ssh_key::authorized_keys::Entry::try_from(authorized_key).ok()
900 })
901 {
902 if !public_keys.insert(ssh_authorized_key.public_key().clone()) {
903 return Err(Error::DuplicateSshPublicKey {
904 ssh_public_key: ssh_authorized_key.public_key().to_string(),
905 }
906 .into());
907 }
908 }
909 }
910
911 // ensure that only one-to-one relationships between users in the Operator role and keys
912 // exist (system-wide and per-namespace)
913 {
914 // ensure that KeyIds are not reused system-wide
915 let mut set = HashSet::new();
916 for key_id in self
917 .users
918 .iter()
919 .flat_map(|mapping| mapping.get_key_ids(None))
920 {
921 if !set.insert(key_id.clone()) {
922 return Err(Error::DuplicateKeyId {
923 key_id,
924 namespace: None,
925 }
926 .into());
927 }
928 }
929
930 // ensure that KeyIds are not reused per namespace
931 for namespace in self
932 .users
933 .iter()
934 .flat_map(|mapping| mapping.get_namespaces())
935 {
936 let mut set = HashSet::new();
937 for key_id in self
938 .users
939 .iter()
940 .flat_map(|mapping| mapping.get_key_ids(Some(&namespace)))
941 {
942 if !set.insert(key_id.clone()) {
943 return Err(Error::DuplicateKeyId {
944 key_id,
945 namespace: Some(namespace),
946 }
947 .into());
948 }
949 }
950 }
951 }
952
953 // ensure unique tags system-wide and per namespace
954 {
955 // ensure that tags are unique system-wide
956 let mut set = HashSet::new();
957 for tag in self.users.iter().flat_map(|mapping| mapping.get_tags(None)) {
958 if !set.insert(tag) {
959 return Err(Error::DuplicateTag {
960 tag: tag.to_string(),
961 namespace: None,
962 }
963 .into());
964 }
965 }
966
967 // ensure that tags are unique in each namespace
968 for namespace in self
969 .users
970 .iter()
971 .flat_map(|mapping| mapping.get_namespaces())
972 {
973 let mut set = HashSet::new();
974 for tag in self
975 .users
976 .iter()
977 .flat_map(|mapping| mapping.get_tags(Some(&namespace)))
978 {
979 if !set.insert(tag) {
980 return Err(Error::DuplicateTag {
981 tag: tag.to_string(),
982 namespace: Some(namespace),
983 }
984 .into());
985 }
986 }
987 }
988 }
989
990 Ok(())
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use core::panic;
997 use std::path::PathBuf;
998
999 use rstest::rstest;
1000 use testresult::TestResult;
1001
1002 use super::*;
1003
1004 #[rstest]
1005 fn signstar_config_new_from_file(
1006 #[files("signstar-config-*.toml")]
1007 #[base_dir = "tests/fixtures/working/"]
1008 config_file: PathBuf,
1009 ) -> TestResult {
1010 SignstarConfig::new_from_file(Some(&config_file))?;
1011
1012 Ok(())
1013 }
1014
1015 #[rstest]
1016 fn signstar_config_duplicate_system_user(
1017 #[files("signstar-config-*.toml")]
1018 #[base_dir = "tests/fixtures/duplicate-system-user/"]
1019 config_file: PathBuf,
1020 ) -> TestResult {
1021 println!("{config_file:?}");
1022 match SignstarConfig::new_from_file(Some(&config_file)) {
1023 Err(crate::Error::Config(Error::DuplicateSystemUserId { .. })) => Ok(()),
1024 Ok(_) => panic!("Did not trigger any Error!"),
1025 Err(error) => panic!("Did not trigger the correct Error: {:?}!", error),
1026 }
1027 }
1028
1029 #[rstest]
1030 fn signstar_config_duplicate_nethsm_user(
1031 #[files("signstar-config-*.toml")]
1032 #[base_dir = "tests/fixtures/duplicate-nethsm-user/"]
1033 config_file: PathBuf,
1034 ) -> TestResult {
1035 if let Err(crate::Error::Config(Error::DuplicateNetHsmUserId { .. })) =
1036 SignstarConfig::new_from_file(Some(&config_file))
1037 {
1038 Ok(())
1039 } else {
1040 panic!("Did not trigger the correct Error!")
1041 }
1042 }
1043
1044 #[rstest]
1045 fn signstar_config_missing_administrator(
1046 #[files("signstar-config-*.toml")]
1047 #[base_dir = "tests/fixtures/missing-administrator/"]
1048 config_file: PathBuf,
1049 ) -> TestResult {
1050 if let Err(crate::Error::Config(Error::MissingAdministrator { .. })) =
1051 SignstarConfig::new_from_file(Some(&config_file))
1052 {
1053 Ok(())
1054 } else {
1055 panic!("Did not trigger the correct Error!")
1056 }
1057 }
1058
1059 #[rstest]
1060 fn signstar_config_missing_namespace_administrators(
1061 #[files("signstar-config-*.toml")]
1062 #[base_dir = "tests/fixtures/missing-namespace-administrator/"]
1063 config_file: PathBuf,
1064 ) -> TestResult {
1065 if let Err(crate::Error::Config(Error::MissingAdministrator { .. })) =
1066 SignstarConfig::new_from_file(Some(&config_file))
1067 {
1068 Ok(())
1069 } else {
1070 panic!("Did not trigger the correct Error!")
1071 }
1072 }
1073
1074 #[rstest]
1075 fn signstar_config_duplicate_authorized_keys_share_uploader(
1076 #[files("signstar-config-*.toml")]
1077 #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-uploader/"]
1078 config_file: PathBuf,
1079 ) -> TestResult {
1080 println!("Using configuration {config_file:?}");
1081 let config_file_string = config_file
1082 .clone()
1083 .into_os_string()
1084 .into_string()
1085 .map_err(|e| format!("Can't convert {config_file:?}:\n{e:?}"))?;
1086 // when using plaintext or systemd-creds for administrative credentials, there are no share
1087 // uploaders
1088 if config_file_string.ends_with("ntext.toml")
1089 || config_file_string.ends_with("emd-creds.toml")
1090 {
1091 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1092 Ok(())
1093 } else if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1094 SignstarConfig::new_from_file(Some(&config_file))
1095 {
1096 Ok(())
1097 } else {
1098 panic!("Did not trigger the correct Error!")
1099 }
1100 }
1101
1102 #[rstest]
1103 fn signstar_config_duplicate_authorized_keys_share_downloader(
1104 #[files("signstar-config-*.toml")]
1105 #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-downloader/"]
1106 config_file: PathBuf,
1107 ) -> TestResult {
1108 println!("Using configuration {config_file:?}");
1109 let config_file_string = config_file
1110 .clone()
1111 .into_os_string()
1112 .into_string()
1113 .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1114 // when using plaintext or systemd-creds for administrative credentials, there are no share
1115 // downloaders
1116 if config_file_string.ends_with("ntext.toml")
1117 || config_file_string.ends_with("systemd-creds.toml")
1118 {
1119 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1120 Ok(())
1121 } else if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1122 SignstarConfig::new_from_file(Some(&config_file))
1123 {
1124 Ok(())
1125 } else {
1126 panic!("Did not trigger the correct Error!")
1127 }
1128 }
1129
1130 #[rstest]
1131 fn signstar_config_duplicate_authorized_keys_users(
1132 #[files("signstar-config-*.toml")]
1133 #[base_dir = "tests/fixtures/duplicate-authorized-keys-users/"]
1134 config_file: PathBuf,
1135 ) -> TestResult {
1136 if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1137 SignstarConfig::new_from_file(Some(&config_file))
1138 {
1139 Ok(())
1140 } else {
1141 panic!("Did not trigger the correct Error!")
1142 }
1143 }
1144
1145 #[rstest]
1146 fn signstar_config_missing_share_download_user(
1147 #[files("signstar-config-*.toml")]
1148 #[base_dir = "tests/fixtures/missing-share-download-user/"]
1149 config_file: PathBuf,
1150 ) -> TestResult {
1151 println!("Using configuration {config_file:?}");
1152 let config_file_string = config_file
1153 .clone()
1154 .into_os_string()
1155 .into_string()
1156 .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1157 // when using plaintext or systemd-creds for administrative credentials, there are no share
1158 // downloaders
1159 if config_file_string.ends_with("plaintext.toml")
1160 || config_file_string.ends_with("systemd-creds.toml")
1161 {
1162 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1163 Ok(())
1164 } else if let Err(crate::Error::Config(Error::MissingShareDownloadSystemUser)) =
1165 SignstarConfig::new_from_file(Some(&config_file))
1166 {
1167 Ok(())
1168 } else {
1169 panic!("Did not trigger the correct Error!")
1170 }
1171 }
1172
1173 #[rstest]
1174 fn signstar_config_missing_share_upload_user(
1175 #[files("signstar-config-*.toml")]
1176 #[base_dir = "tests/fixtures/missing-share-upload-user/"]
1177 config_file: PathBuf,
1178 ) -> TestResult {
1179 println!("Using configuration {config_file:?}");
1180 let config_file_string = config_file
1181 .clone()
1182 .into_os_string()
1183 .into_string()
1184 .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1185 // when using plaintext or systemd-creds for administrative credentials, there are no share
1186 // downloaders
1187 if config_file_string.ends_with("plaintext.toml")
1188 || config_file_string.ends_with("systemd-creds.toml")
1189 {
1190 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1191 Ok(())
1192 } else if let Err(crate::Error::Config(Error::MissingShareUploadSystemUser)) =
1193 SignstarConfig::new_from_file(Some(&config_file))
1194 {
1195 Ok(())
1196 } else {
1197 panic!("Did not trigger the correct Error!")
1198 }
1199 }
1200
1201 #[rstest]
1202 fn signstar_config_no_sss_but_shares(
1203 #[files("signstar-config-*.toml")]
1204 #[base_dir = "tests/fixtures/no-sss-but-shares/"]
1205 config_file: PathBuf,
1206 ) -> TestResult {
1207 println!("Using configuration {config_file:?}");
1208 let config_file_string = config_file
1209 .clone()
1210 .into_os_string()
1211 .into_string()
1212 .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1213 // when using shamir's secret sharing for administrative credentials, there ought to be
1214 // share downloaders and uploaders
1215 if config_file_string.ends_with("irs-secret-sharing.toml") {
1216 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1217 Ok(())
1218 } else if let Err(crate::Error::Config(Error::NoSssButShareUsers { .. })) =
1219 SignstarConfig::new_from_file(Some(&config_file))
1220 {
1221 Ok(())
1222 } else {
1223 panic!("Did not trigger the correct Error!")
1224 }
1225 }
1226
1227 #[rstest]
1228 fn signstar_config_duplicate_key_id(
1229 #[files("signstar-config-*.toml")]
1230 #[base_dir = "tests/fixtures/duplicate-key-id/"]
1231 config_file: PathBuf,
1232 ) -> TestResult {
1233 if let Err(crate::Error::Config(Error::DuplicateKeyId { .. })) =
1234 SignstarConfig::new_from_file(Some(&config_file))
1235 {
1236 Ok(())
1237 } else {
1238 panic!("Did not trigger the correct Error!")
1239 }
1240 }
1241
1242 #[rstest]
1243 fn signstar_config_duplicate_key_id_in_namespace(
1244 #[files("signstar-config-*.toml")]
1245 #[base_dir = "tests/fixtures/duplicate-key-id-in-namespace/"]
1246 config_file: PathBuf,
1247 ) -> TestResult {
1248 if let Err(crate::Error::Config(Error::DuplicateKeyId { .. })) =
1249 SignstarConfig::new_from_file(Some(&config_file))
1250 {
1251 Ok(())
1252 } else {
1253 panic!("Did not trigger the correct Error!")
1254 }
1255 }
1256
1257 #[rstest]
1258 fn signstar_config_duplicate_tag(
1259 #[files("signstar-config-*.toml")]
1260 #[base_dir = "tests/fixtures/duplicate-tag/"]
1261 config_file: PathBuf,
1262 ) -> TestResult {
1263 if let Err(crate::Error::Config(Error::DuplicateTag { .. })) =
1264 SignstarConfig::new_from_file(Some(&config_file))
1265 {
1266 Ok(())
1267 } else {
1268 panic!("Did not trigger the correct Error!")
1269 }
1270 }
1271
1272 #[rstest]
1273 fn signstar_config_duplicate_tag_in_namespace(
1274 #[files("signstar-config-*.toml")]
1275 #[base_dir = "tests/fixtures/duplicate-tag-in-namespace/"]
1276 config_file: PathBuf,
1277 ) -> TestResult {
1278 if let Err(crate::Error::Config(Error::DuplicateTag { .. })) =
1279 SignstarConfig::new_from_file(Some(&config_file))
1280 {
1281 Ok(())
1282 } else {
1283 panic!("Did not trigger the correct Error!")
1284 }
1285 }
1286
1287 #[rstest]
1288 #[case("ssh-backup1")]
1289 #[case("ssh-metrics1")]
1290 #[case("ssh-operator1")]
1291 #[case("ssh-operator2")]
1292 #[case("ns1-ssh-operator1")]
1293 #[case("ns1-ssh-operator2")]
1294 #[case("local-metrics1")]
1295 #[case("ssh-wireguard-down")]
1296 fn signstar_config_get_extended_usermapping_succeeds(
1297 #[files("signstar-config-*.toml")]
1298 #[base_dir = "tests/fixtures/working/"]
1299 config_file: PathBuf,
1300 #[case] name: &str,
1301 ) -> TestResult {
1302 let config = SignstarConfig::new_from_file(Some(&config_file))?;
1303 if config.get_extended_mapping_for_user(name).is_none() {
1304 panic!("The user with name {name} is supposed to exist in the Signstar config");
1305 }
1306
1307 Ok(())
1308 }
1309
1310 #[rstest]
1311 fn signstar_config_get_extended_usermapping_fails(
1312 #[files("signstar-config-*.toml")]
1313 #[base_dir = "tests/fixtures/working/"]
1314 config_file: PathBuf,
1315 ) -> TestResult {
1316 let config = SignstarConfig::new_from_file(Some(&config_file))?;
1317 if config.get_extended_mapping_for_user("foo").is_some() {
1318 panic!("The user \"foo\" should not exist in the Signstar config");
1319 }
1320
1321 Ok(())
1322 }
1323}