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 .map(ssh_key::authorized_keys::Entry::from)
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 .map(ssh_key::authorized_keys::Entry::from)
917 {
918 if !public_keys.insert(ssh_authorized_key.public_key().clone()) {
919 return Err(Error::DuplicateSshPublicKey {
920 ssh_public_key: ssh_authorized_key.public_key().to_string(),
921 }
922 .into());
923 }
924 }
925 }
926
927 // ensure that only one-to-one relationships between users in the Operator role and keys
928 // exist (system-wide and per-namespace)
929 {
930 // ensure that KeyIds are not reused system-wide
931 let mut set = HashSet::new();
932 for key_id in self
933 .users
934 .iter()
935 .flat_map(|mapping| mapping.get_nethsm_key_ids(None))
936 {
937 if !set.insert(key_id.clone()) {
938 return Err(Error::DuplicateKeyId {
939 key_id,
940 namespace: None,
941 }
942 .into());
943 }
944 }
945
946 // ensure that KeyIds are not reused per namespace
947 for namespace in self
948 .users
949 .iter()
950 .flat_map(|mapping| mapping.get_nethsm_namespaces())
951 {
952 let mut set = HashSet::new();
953 for key_id in self
954 .users
955 .iter()
956 .flat_map(|mapping| mapping.get_nethsm_key_ids(Some(&namespace)))
957 {
958 if !set.insert(key_id.clone()) {
959 return Err(Error::DuplicateKeyId {
960 key_id,
961 namespace: Some(namespace),
962 }
963 .into());
964 }
965 }
966 }
967 }
968
969 // ensure unique tags system-wide and per namespace
970 {
971 // ensure that tags are unique system-wide
972 let mut set = HashSet::new();
973 for tag in self
974 .users
975 .iter()
976 .flat_map(|mapping| mapping.get_nethsm_tags(None))
977 {
978 if !set.insert(tag) {
979 return Err(Error::DuplicateTag {
980 tag: tag.to_string(),
981 namespace: None,
982 }
983 .into());
984 }
985 }
986
987 // ensure that tags are unique in each namespace
988 for namespace in self
989 .users
990 .iter()
991 .flat_map(|mapping| mapping.get_nethsm_namespaces())
992 {
993 let mut set = HashSet::new();
994 for tag in self
995 .users
996 .iter()
997 .flat_map(|mapping| mapping.get_nethsm_tags(Some(&namespace)))
998 {
999 if !set.insert(tag) {
1000 return Err(Error::DuplicateTag {
1001 tag: tag.to_string(),
1002 namespace: Some(namespace),
1003 }
1004 .into());
1005 }
1006 }
1007 }
1008 }
1009
1010 Ok(())
1011 }
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use core::panic;
1017 use std::path::PathBuf;
1018
1019 use rstest::rstest;
1020 use testresult::TestResult;
1021
1022 use super::*;
1023
1024 #[rstest]
1025 fn signstar_config_new_from_file(
1026 #[files("signstar-config-*.toml")]
1027 #[base_dir = "tests/fixtures/working/"]
1028 config_file: PathBuf,
1029 ) -> TestResult {
1030 SignstarConfig::new_from_file(Some(&config_file))?;
1031
1032 Ok(())
1033 }
1034
1035 #[rstest]
1036 fn signstar_config_duplicate_system_user(
1037 #[files("signstar-config-*.toml")]
1038 #[base_dir = "tests/fixtures/duplicate-system-user/"]
1039 config_file: PathBuf,
1040 ) -> TestResult {
1041 println!("{config_file:?}");
1042 match SignstarConfig::new_from_file(Some(&config_file)) {
1043 Err(crate::Error::Config(Error::DuplicateSystemUserId { .. })) => Ok(()),
1044 Ok(_) => panic!("Did not trigger any Error!"),
1045 Err(error) => panic!("Did not trigger the correct Error: {:?}!", error),
1046 }
1047 }
1048
1049 #[rstest]
1050 fn signstar_config_duplicate_nethsm_user(
1051 #[files("signstar-config-*.toml")]
1052 #[base_dir = "tests/fixtures/duplicate-nethsm-user/"]
1053 config_file: PathBuf,
1054 ) -> TestResult {
1055 if let Err(crate::Error::Config(Error::DuplicateNetHsmUserId { .. })) =
1056 SignstarConfig::new_from_file(Some(&config_file))
1057 {
1058 Ok(())
1059 } else {
1060 panic!("Did not trigger the correct Error!")
1061 }
1062 }
1063
1064 #[rstest]
1065 fn signstar_config_missing_administrator(
1066 #[files("signstar-config-*.toml")]
1067 #[base_dir = "tests/fixtures/missing-administrator/"]
1068 config_file: PathBuf,
1069 ) -> TestResult {
1070 if let Err(crate::Error::Config(Error::MissingAdministrator { .. })) =
1071 SignstarConfig::new_from_file(Some(&config_file))
1072 {
1073 Ok(())
1074 } else {
1075 panic!("Did not trigger the correct Error!")
1076 }
1077 }
1078
1079 #[rstest]
1080 fn signstar_config_missing_namespace_administrators(
1081 #[files("signstar-config-*.toml")]
1082 #[base_dir = "tests/fixtures/missing-namespace-administrator/"]
1083 config_file: PathBuf,
1084 ) -> TestResult {
1085 if let Err(crate::Error::Config(Error::MissingAdministrator { .. })) =
1086 SignstarConfig::new_from_file(Some(&config_file))
1087 {
1088 Ok(())
1089 } else {
1090 panic!("Did not trigger the correct Error!")
1091 }
1092 }
1093
1094 #[rstest]
1095 fn signstar_config_duplicate_authorized_keys_share_uploader(
1096 #[files("signstar-config-*.toml")]
1097 #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-uploader/"]
1098 config_file: PathBuf,
1099 ) -> TestResult {
1100 println!("Using configuration {config_file:?}");
1101 let config_file_string = config_file
1102 .clone()
1103 .into_os_string()
1104 .into_string()
1105 .map_err(|e| format!("Can't convert {config_file:?}:\n{e:?}"))?;
1106 // when using plaintext or systemd-creds for administrative credentials, there are no share
1107 // uploaders
1108 if config_file_string.ends_with("ntext.toml")
1109 || config_file_string.ends_with("emd-creds.toml")
1110 {
1111 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1112 Ok(())
1113 } else if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1114 SignstarConfig::new_from_file(Some(&config_file))
1115 {
1116 Ok(())
1117 } else {
1118 panic!("Did not trigger the correct Error!")
1119 }
1120 }
1121
1122 #[rstest]
1123 fn signstar_config_duplicate_authorized_keys_share_downloader(
1124 #[files("signstar-config-*.toml")]
1125 #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-downloader/"]
1126 config_file: PathBuf,
1127 ) -> TestResult {
1128 println!("Using configuration {config_file:?}");
1129 let config_file_string = config_file
1130 .clone()
1131 .into_os_string()
1132 .into_string()
1133 .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1134 // when using plaintext or systemd-creds for administrative credentials, there are no share
1135 // downloaders
1136 if config_file_string.ends_with("ntext.toml")
1137 || config_file_string.ends_with("systemd-creds.toml")
1138 {
1139 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1140 Ok(())
1141 } else if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1142 SignstarConfig::new_from_file(Some(&config_file))
1143 {
1144 Ok(())
1145 } else {
1146 panic!("Did not trigger the correct Error!")
1147 }
1148 }
1149
1150 #[rstest]
1151 fn signstar_config_duplicate_authorized_keys_users(
1152 #[files("signstar-config-*.toml")]
1153 #[base_dir = "tests/fixtures/duplicate-authorized-keys-users/"]
1154 config_file: PathBuf,
1155 ) -> TestResult {
1156 if let Err(crate::Error::Config(Error::DuplicateSshPublicKey { .. })) =
1157 SignstarConfig::new_from_file(Some(&config_file))
1158 {
1159 Ok(())
1160 } else {
1161 panic!("Did not trigger the correct Error!")
1162 }
1163 }
1164
1165 #[rstest]
1166 fn signstar_config_missing_share_download_user(
1167 #[files("signstar-config-*.toml")]
1168 #[base_dir = "tests/fixtures/missing-share-download-user/"]
1169 config_file: PathBuf,
1170 ) -> TestResult {
1171 println!("Using configuration {config_file:?}");
1172 let config_file_string = config_file
1173 .clone()
1174 .into_os_string()
1175 .into_string()
1176 .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1177 // when using plaintext or systemd-creds for administrative credentials, there are no share
1178 // downloaders
1179 if config_file_string.ends_with("plaintext.toml")
1180 || config_file_string.ends_with("systemd-creds.toml")
1181 {
1182 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1183 Ok(())
1184 } else if let Err(crate::Error::Config(Error::MissingShareDownloadSystemUser)) =
1185 SignstarConfig::new_from_file(Some(&config_file))
1186 {
1187 Ok(())
1188 } else {
1189 panic!("Did not trigger the correct Error!")
1190 }
1191 }
1192
1193 #[rstest]
1194 fn signstar_config_missing_share_upload_user(
1195 #[files("signstar-config-*.toml")]
1196 #[base_dir = "tests/fixtures/missing-share-upload-user/"]
1197 config_file: PathBuf,
1198 ) -> TestResult {
1199 println!("Using configuration {config_file:?}");
1200 let config_file_string = config_file
1201 .clone()
1202 .into_os_string()
1203 .into_string()
1204 .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1205 // when using plaintext or systemd-creds for administrative credentials, there are no share
1206 // downloaders
1207 if config_file_string.ends_with("plaintext.toml")
1208 || config_file_string.ends_with("systemd-creds.toml")
1209 {
1210 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1211 Ok(())
1212 } else if let Err(crate::Error::Config(Error::MissingShareUploadSystemUser)) =
1213 SignstarConfig::new_from_file(Some(&config_file))
1214 {
1215 Ok(())
1216 } else {
1217 panic!("Did not trigger the correct Error!")
1218 }
1219 }
1220
1221 #[rstest]
1222 fn signstar_config_no_sss_but_shares(
1223 #[files("signstar-config-*.toml")]
1224 #[base_dir = "tests/fixtures/no-sss-but-shares/"]
1225 config_file: PathBuf,
1226 ) -> TestResult {
1227 println!("Using configuration {config_file:?}");
1228 let config_file_string = config_file
1229 .clone()
1230 .into_os_string()
1231 .into_string()
1232 .map_err(|_x| format!("Can't convert {config_file:?}"))?;
1233 // when using shamir's secret sharing for administrative credentials, there ought to be
1234 // share downloaders and uploaders
1235 if config_file_string.ends_with("irs-secret-sharing.toml") {
1236 let _config = SignstarConfig::new_from_file(Some(&config_file))?;
1237 Ok(())
1238 } else if let Err(crate::Error::Config(Error::NoSssButShareUsers { .. })) =
1239 SignstarConfig::new_from_file(Some(&config_file))
1240 {
1241 Ok(())
1242 } else {
1243 panic!("Did not trigger the correct Error!")
1244 }
1245 }
1246
1247 #[rstest]
1248 fn signstar_config_duplicate_key_id(
1249 #[files("signstar-config-*.toml")]
1250 #[base_dir = "tests/fixtures/duplicate-key-id/"]
1251 config_file: PathBuf,
1252 ) -> TestResult {
1253 if let Err(crate::Error::Config(Error::DuplicateKeyId { .. })) =
1254 SignstarConfig::new_from_file(Some(&config_file))
1255 {
1256 Ok(())
1257 } else {
1258 panic!("Did not trigger the correct Error!")
1259 }
1260 }
1261
1262 #[rstest]
1263 fn signstar_config_duplicate_key_id_in_namespace(
1264 #[files("signstar-config-*.toml")]
1265 #[base_dir = "tests/fixtures/duplicate-key-id-in-namespace/"]
1266 config_file: PathBuf,
1267 ) -> TestResult {
1268 if let Err(crate::Error::Config(Error::DuplicateKeyId { .. })) =
1269 SignstarConfig::new_from_file(Some(&config_file))
1270 {
1271 Ok(())
1272 } else {
1273 panic!("Did not trigger the correct Error!")
1274 }
1275 }
1276
1277 #[rstest]
1278 fn signstar_config_duplicate_tag(
1279 #[files("signstar-config-*.toml")]
1280 #[base_dir = "tests/fixtures/duplicate-tag/"]
1281 config_file: PathBuf,
1282 ) -> TestResult {
1283 if let Err(crate::Error::Config(Error::DuplicateTag { .. })) =
1284 SignstarConfig::new_from_file(Some(&config_file))
1285 {
1286 Ok(())
1287 } else {
1288 panic!("Did not trigger the correct Error!")
1289 }
1290 }
1291
1292 #[rstest]
1293 fn signstar_config_duplicate_tag_in_namespace(
1294 #[files("signstar-config-*.toml")]
1295 #[base_dir = "tests/fixtures/duplicate-tag-in-namespace/"]
1296 config_file: PathBuf,
1297 ) -> TestResult {
1298 if let Err(crate::Error::Config(Error::DuplicateTag { .. })) =
1299 SignstarConfig::new_from_file(Some(&config_file))
1300 {
1301 Ok(())
1302 } else {
1303 panic!("Did not trigger the correct Error!")
1304 }
1305 }
1306
1307 #[rstest]
1308 #[case("ssh-backup1")]
1309 #[case("ssh-metrics1")]
1310 #[case("ssh-operator1")]
1311 #[case("ssh-operator2")]
1312 #[case("ns1-ssh-operator1")]
1313 #[case("ns1-ssh-operator2")]
1314 #[case("local-metrics1")]
1315 #[case("ssh-wireguard-down")]
1316 fn signstar_config_get_extended_usermapping_succeeds(
1317 #[files("signstar-config-*.toml")]
1318 #[base_dir = "tests/fixtures/working/"]
1319 config_file: PathBuf,
1320 #[case] name: &str,
1321 ) -> TestResult {
1322 let config = SignstarConfig::new_from_file(Some(&config_file))?;
1323 if config.get_extended_mapping_for_user(name).is_none() {
1324 panic!("The user with name {name} is supposed to exist in the Signstar config");
1325 }
1326
1327 Ok(())
1328 }
1329
1330 #[rstest]
1331 fn signstar_config_get_extended_usermapping_fails(
1332 #[files("signstar-config-*.toml")]
1333 #[base_dir = "tests/fixtures/working/"]
1334 config_file: PathBuf,
1335 ) -> TestResult {
1336 let config = SignstarConfig::new_from_file(Some(&config_file))?;
1337 if config.get_extended_mapping_for_user("foo").is_some() {
1338 panic!("The user \"foo\" should not exist in the Signstar config");
1339 }
1340
1341 Ok(())
1342 }
1343}