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