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