nethsm_config/
config.rs

1use std::{
2    cell::RefCell,
3    collections::{HashMap, HashSet, hash_map::Entry},
4    error::Error as StdError,
5    fmt::Display,
6    path::{Path, PathBuf},
7    str::FromStr,
8};
9
10use nethsm::{
11    Connection,
12    ConnectionSecurity,
13    Credentials,
14    KeyId,
15    NetHsm,
16    Passphrase,
17    Url,
18    UserId,
19    UserRole,
20};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24    ConfigCredentials,
25    ExtendedUserMapping,
26    PassphrasePrompt,
27    SystemUserId,
28    UserMapping,
29    UserPrompt,
30};
31
32/// Errors related to configuration
33#[derive(Debug, thiserror::Error)]
34pub enum Error {
35    /// Issue getting the config file location
36    #[error("Config file issue: {0}")]
37    ConfigFileLocation(#[source] confy::ConfyError),
38
39    /// A config loading error
40    ///
41    /// The variant tracks a [`ConfyError`][`confy::ConfyError`] and an optional
42    /// description of an inner Error type.
43    /// The description is tracked separately, as otherwise we do not get to useful error messages
44    /// of wrapped Error types (e.g. those for loading TOML files).
45    #[error("Config loading issue: {source}\n{description}")]
46    Load {
47        source: confy::ConfyError,
48        description: String,
49    },
50
51    /// A config storing error
52    #[error("Config storing issue: {0}")]
53    Store(#[source] confy::ConfyError),
54
55    /// Credentials exist already
56    #[error("Credentials exist already: {0}")]
57    CredentialsExist(UserId),
58
59    /// Credentials do not exist
60    #[error("Credentials do not exist: {0}")]
61    CredentialsMissing(UserId),
62
63    /// None of the provided users map to one of the provided roles
64    #[error("None of the provided users ({names:?}) map to one of the provided roles ({roles:?})")]
65    MatchingCredentialsMissing {
66        names: Vec<UserId>,
67        roles: Vec<UserRole>,
68    },
69
70    /// Credentials do not exist
71    #[error("No user matching one of the requested roles ({0:?}) exists")]
72    NoMatchingCredentials(Vec<UserRole>),
73
74    /// There is no mapping for a provided system user name.
75    #[error("No mapping found where a system user matches the name {name}")]
76    NoMatchingMappingForSystemUser { name: String },
77
78    /// Shamir's Secret Sharing (SSS) is not used for administrative secret handling, but users for
79    /// handling of secret shares are defined
80    #[error(
81        "Shamir's Secret Sharing not used for administrative secret handling, but the following users are setup to handle shares: {share_users:?}"
82    )]
83    NoSssButShareUsers { share_users: Vec<SystemUserId> },
84
85    /// Device exists already
86    #[error("Device exist already: {0}")]
87    DeviceExists(String),
88
89    /// Device does not exist
90    #[error("Device does not exist: {0}")]
91    DeviceMissing(String),
92
93    /// Duplicate NetHsm user names
94    #[error("The NetHsm user ID {nethsm_user_id} is used more than once!")]
95    DuplicateNetHsmUserId { nethsm_user_id: UserId },
96
97    /// Duplicate system user names
98    #[error("The authorized SSH key {ssh_authorized_key} is used more than once!")]
99    DuplicateSshAuthorizedKey { ssh_authorized_key: String },
100
101    /// Duplicate key ID
102    #[error("The key ID {key_id} is used more than once!")]
103    DuplicateKeyId { key_id: KeyId },
104
105    /// Duplicate key ID in a namespace
106    #[error("The key ID {key_id} is used more than once in namespace {namespace}!")]
107    DuplicateKeyIdInNamespace { key_id: KeyId, namespace: String },
108
109    /// Duplicate system user names
110    #[error("The system user ID {system_user_id} is used more than once!")]
111    DuplicateSystemUserId { system_user_id: SystemUserId },
112
113    /// Duplicate tag
114    #[error("The tag {tag} is used more than once!")]
115    DuplicateTag { tag: String },
116
117    /// Duplicate tag
118    #[error("The tag {tag} is used more than once in namespace {namespace}!")]
119    DuplicateTagInNamespace { tag: String, namespace: String },
120
121    /// Missing system-wide user in the Administrator role (R-Administrator)
122    #[error("No system-wide user in the Administrator role exists.")]
123    MissingAdministrator,
124
125    /// Missing user in the Administrator role for a namespace (N-Administrator)
126    #[error("No user in the Administrator role exist for the namespaces {namespaces:?}")]
127    MissingNamespaceAdministrators { namespaces: Vec<String> },
128
129    /// Missing system user for downloading shares of a shared secret
130    #[error("No system user for downloading shares of a shared secret exists.")]
131    MissingShareDownloadUser,
132
133    /// Missing system user for uploading shares of a shared secret
134    #[error("No system user for uploading shares of a shared secret exists.")]
135    MissingShareUploadUser,
136
137    /// There is more than one device (but none has been specified)
138    #[error("There is more than one device")]
139    MoreThanOneDevice,
140
141    /// There is no device
142    #[error("There is no device")]
143    NoDevice,
144
145    /// The configuration can not be used interactively
146    #[error("The configuration can not be used interactively")]
147    NonInteractive,
148
149    /// NetHsm connection initialization error
150    #[error("NetHsm connection can not be created: {0}")]
151    NetHsm(#[from] nethsm::Error),
152
153    /// A prompt requesting user data failed
154    #[error("A prompt issue")]
155    Prompt(#[from] crate::prompt::Error),
156
157    /// User data is invalid
158    #[error("User data invalid: {0}")]
159    User(#[from] nethsm::UserError),
160}
161
162/// The interactivity of a configuration
163///
164/// This enum is used by [`Config`] and [`DeviceConfig`] to define whether missing items are
165/// prompted for interactively ([`ConfigInteractivity::Interactive`]) or not
166/// ([`ConfigInteractivity::NonInteractive`]).
167#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
168pub enum ConfigInteractivity {
169    /// The configuration may spawn interactive prompts to request more data (e.g. usernames or
170    /// passphrases)
171    Interactive,
172    /// The configuration will return an [`Error`] if interactive prompts need to be spawned to
173    /// request more data (e.g. usernames or passphrases)
174    #[default]
175    NonInteractive,
176}
177
178/// The name of a configuration
179///
180/// The name defines the file name (without file suffix) used by a [`Config`] object.
181/// It defaults to `"config"`, but may be set specifically when initializing a [`Config`].
182#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
183pub struct ConfigName(String);
184
185impl Default for ConfigName {
186    fn default() -> Self {
187        Self("config".to_string())
188    }
189}
190
191impl Display for ConfigName {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        self.0.fmt(f)
194    }
195}
196
197impl FromStr for ConfigName {
198    type Err = Error;
199
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        Ok(Self(s.to_string()))
202    }
203}
204
205/// The settings for a [`Config`]
206///
207/// Settings contain the [`ConfigName`] by which the configuration file is loaded and stored, the
208/// application name which uses the configuration (and also influences the file path of the
209/// configuration) and the interactivity setting, which defines whether missing items are prompted
210/// for interactively or not.
211#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
212pub struct ConfigSettings {
213    /// The configuration name (file name without suffix)
214    config_name: ConfigName,
215    /// The name of the application using a [`Config`]
216    app_name: String,
217    /// The interactivity setting for the [`Config`] (and any [`DeviceConfig`] used by it)
218    interactivity: ConfigInteractivity,
219}
220
221impl ConfigSettings {
222    /// Creates a new [`ConfigSettings`]
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// use nethsm_config::{ConfigInteractivity, ConfigSettings};
228    ///
229    /// # fn main() -> testresult::TestResult {
230    /// // settings for an application called "my_app", that uses a custom configuration file named "my_app-config" interactively
231    /// let config_settings = ConfigSettings::new(
232    ///     "my_app".to_string(),
233    ///     ConfigInteractivity::Interactive,
234    ///     Some("my_app-config".parse()?),
235    /// );
236    ///
237    /// // settings for an application called "my_app", that uses a default config file non-interactively
238    /// let config_settings = ConfigSettings::new(
239    ///     "my_app".to_string(),
240    ///     ConfigInteractivity::NonInteractive,
241    ///     None,
242    /// );
243    /// # Ok(())
244    /// # }
245    /// ```
246    pub fn new(
247        app_name: String,
248        interactivity: ConfigInteractivity,
249        config_name: Option<ConfigName>,
250    ) -> Self {
251        Self {
252            app_name,
253            interactivity,
254            config_name: config_name.unwrap_or_default(),
255        }
256    }
257
258    /// Returns the configuration name
259    pub fn config_name(&self) -> ConfigName {
260        self.config_name.to_owned()
261    }
262
263    /// Returns the application name
264    pub fn app_name(&self) -> String {
265        self.app_name.clone()
266    }
267
268    /// Returns the interactivity setting
269    pub fn interactivity(&self) -> ConfigInteractivity {
270        self.interactivity
271    }
272}
273
274/// The configuration for a [`NetHsm`]
275///
276/// Tracks the [`Connection`] for a [`NetHsm`] as well as a set of [`ConfigCredentials`].
277#[derive(Clone, Debug, Deserialize, Serialize)]
278pub struct DeviceConfig {
279    connection: RefCell<Connection>,
280    credentials: RefCell<HashSet<ConfigCredentials>>,
281    #[serde(skip)]
282    interactivity: ConfigInteractivity,
283}
284
285impl DeviceConfig {
286    /// Creates a new [`DeviceConfig`]
287    ///
288    /// Creates a new [`DeviceConfig`] by providing a `connection`, an optional set of `credentials`
289    /// and the `interactivity` setting.
290    ///
291    /// # Errors
292    ///
293    /// Returns an [`Error::CredentialsExist`] if `credentials` contains duplicates.
294    ///
295    /// # Examples
296    ///
297    /// ```
298    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
299    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
300    ///
301    /// # fn main() -> testresult::TestResult {
302    /// let connection = Connection::new(
303    ///     "https://example.org/api/v1".parse()?,
304    ///     ConnectionSecurity::Unsafe,
305    /// );
306    ///
307    /// DeviceConfig::new(
308    ///     connection.clone(),
309    ///     vec![],
310    ///     ConfigInteractivity::NonInteractive,
311    /// )?;
312    ///
313    /// DeviceConfig::new(
314    ///     connection.clone(),
315    ///     vec![ConfigCredentials::new(
316    ///         UserRole::Operator,
317    ///         "user1".parse()?,
318    ///         Some("my-passphrase".to_string()),
319    ///     )],
320    ///     ConfigInteractivity::NonInteractive,
321    /// )?;
322    ///
323    /// // this fails because the provided credentials contain duplicates
324    /// assert!(
325    ///     DeviceConfig::new(
326    ///         connection.clone(),
327    ///         vec![
328    ///             ConfigCredentials::new(
329    ///                 UserRole::Operator,
330    ///                 "user1".parse()?,
331    ///                 Some("my-passphrase".to_string()),
332    ///             ),
333    ///             ConfigCredentials::new(
334    ///                 UserRole::Operator,
335    ///                 "user1".parse()?,
336    ///                 Some("my-passphrase".to_string()),
337    ///             ),
338    ///         ],
339    ///         ConfigInteractivity::NonInteractive,
340    ///     )
341    ///     .is_err()
342    /// );
343    /// # Ok(())
344    /// # }
345    /// ```
346    pub fn new(
347        connection: Connection,
348        credentials: Vec<ConfigCredentials>,
349        interactivity: ConfigInteractivity,
350    ) -> Result<DeviceConfig, Error> {
351        let device_config = DeviceConfig {
352            connection: RefCell::new(connection),
353            credentials: RefCell::new(HashSet::new()),
354            interactivity,
355        };
356
357        if !credentials.is_empty() {
358            for creds in credentials.into_iter() {
359                device_config.add_credentials(creds)?
360            }
361        }
362
363        Ok(device_config)
364    }
365
366    /// Sets the interactivity setting
367    ///
368    /// **NOTE**: This method is not necessarily useful by itself, as one usually wants to use the
369    /// same [`ConfigInteractivity`] as that of a [`Config`], which holds the [`DeviceConfig`].
370    pub fn set_config_interactivity(&mut self, config_type: ConfigInteractivity) {
371        self.interactivity = config_type;
372    }
373
374    /// Adds credentials to the device
375    ///
376    /// Adds new [`ConfigCredentials`] to the [`DeviceConfig`].
377    ///
378    /// # Errors
379    ///
380    /// Returns an [`Error::CredentialsExist`] if the `credentials` exist already.
381    ///
382    /// # Examples
383    ///
384    /// ```
385    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
386    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
387    ///
388    /// # fn main() -> testresult::TestResult {
389    /// let connection = Connection::new(
390    ///     "https://example.org/api/v1".parse()?,
391    ///     ConnectionSecurity::Unsafe,
392    /// );
393    ///
394    /// let device_config = DeviceConfig::new(
395    ///     connection.clone(),
396    ///     vec![],
397    ///     ConfigInteractivity::NonInteractive,
398    /// )?;
399    ///
400    /// device_config.add_credentials(ConfigCredentials::new(
401    ///     UserRole::Operator,
402    ///     "user1".parse()?,
403    ///     Some("my-passphrase".to_string()),
404    /// ))?;
405    ///
406    /// // this fails because the credentials exist already
407    /// assert!(
408    ///     device_config
409    ///         .add_credentials(ConfigCredentials::new(
410    ///             UserRole::Operator,
411    ///             "user1".parse()?,
412    ///             Some("my-passphrase".to_string()),
413    ///         ))
414    ///         .is_err()
415    /// );
416    /// # Ok(())
417    /// # }
418    /// ```
419    pub fn add_credentials(&self, credentials: ConfigCredentials) -> Result<(), Error> {
420        if !self
421            .credentials
422            .borrow()
423            .iter()
424            .any(|creds| creds.get_name() == credentials.get_name())
425        {
426            self.credentials.borrow_mut().insert(credentials);
427            Ok(())
428        } else {
429            Err(Error::CredentialsExist(credentials.get_name()))
430        }
431    }
432
433    /// Returns credentials by name
434    ///
435    /// Returns existing [`ConfigCredentials`] from the [`DeviceConfig`].
436    ///
437    /// # Errors
438    ///
439    /// Returns an [`Error::CredentialsMissing`] if no [`ConfigCredentials`] match the provided
440    /// `name`.
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
446    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
447    /// # fn main() -> testresult::TestResult {
448    /// let connection = Connection::new(
449    ///     "https://example.org/api/v1".parse()?,
450    ///     ConnectionSecurity::Unsafe,
451    /// );
452    ///
453    /// let device_config = DeviceConfig::new(
454    ///     connection.clone(),
455    ///     vec![],
456    ///     ConfigInteractivity::NonInteractive,
457    /// )?;
458    ///
459    /// // this fails because the credentials do not exist
460    /// assert!(device_config.get_credentials(&"user1".parse()?).is_err());
461    ///
462    /// device_config.add_credentials(ConfigCredentials::new(
463    ///     UserRole::Operator,
464    ///     "user1".parse()?,
465    ///     Some("my-passphrase".to_string()),
466    /// ))?;
467    ///
468    /// device_config.get_credentials(&"user1".parse()?)?;
469    /// # Ok(())
470    /// # }
471    /// ```
472    pub fn get_credentials(&self, name: &UserId) -> Result<ConfigCredentials, Error> {
473        if let Some(creds) = self
474            .credentials
475            .borrow()
476            .iter()
477            .find(|creds| &creds.get_name() == name)
478        {
479            Ok(creds.clone())
480        } else {
481            Err(Error::CredentialsMissing(name.to_owned()))
482        }
483    }
484
485    /// Deletes credentials by name
486    ///
487    /// Deletes [`ConfigCredentials`] identified by `name`.
488    ///
489    /// # Errors
490    ///
491    /// Returns an [`Error::CredentialsMissing`] if no [`ConfigCredentials`] match the provided
492    /// name.
493    ///
494    /// # Examples
495    ///
496    /// ```
497    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
498    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
499    ///
500    /// # fn main() -> testresult::TestResult {
501    /// let device_config = DeviceConfig::new(
502    ///     Connection::new(
503    ///         "https://example.org/api/v1".parse()?,
504    ///         ConnectionSecurity::Unsafe,
505    ///     ),
506    ///     vec![],
507    ///     ConfigInteractivity::NonInteractive,
508    /// )?;
509    /// device_config.add_credentials(ConfigCredentials::new(
510    ///     UserRole::Operator,
511    ///     "user1".parse()?,
512    ///     Some("my-passphrase".to_string()),
513    /// ))?;
514    ///
515    /// device_config.delete_credentials(&"user1".parse()?)?;
516    ///
517    /// // this fails because the credentials do not exist
518    /// assert!(device_config.delete_credentials(&"user1".parse()?).is_err());
519    /// # Ok(())
520    /// # }
521    /// ```
522    pub fn delete_credentials(&self, name: &UserId) -> Result<(), Error> {
523        let before = self.credentials.borrow().len();
524        self.credentials
525            .borrow_mut()
526            .retain(|creds| &creds.get_name() != name);
527        let after = self.credentials.borrow().len();
528        if before == after {
529            Err(Error::CredentialsMissing(name.to_owned()))
530        } else {
531            Ok(())
532        }
533    }
534
535    /// Returns credentials machting one or several roles and a optionally a name
536    ///
537    /// Returns [`ConfigCredentials`] matching a list of [`UserRole`]s and/or a list of [`UserId`]s.
538    ///
539    /// If `names` is empty, the [`ConfigCredentials`] first found matching one of the [`UserRole`]s
540    /// provided using `roles` are returned.
541    /// If `names` contains at least one entry, the first [`ConfigCredentials`] with a matching
542    /// [`UserId`] that have at least one matching [`UserRole`] are returned.
543    ///
544    /// # Errors
545    ///
546    /// Returns an [`Error::NoMatchingCredentials`] if `names` is empty and no existing credentials
547    /// match any of the provided `roles`.
548    /// Returns an [`Error::CredentialsMissing`] if a [`UserId`] in `names` does not exist and no
549    /// [`ConfigCredentials`] have been returned yet.
550    /// Returns an [`Error::MatchingCredentialsMissing`] if no [`ConfigCredentials`] matching either
551    /// the provided `names` or `roles` can be found.
552    ///
553    /// # Examples
554    ///
555    /// ```
556    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
557    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
558    ///
559    /// # fn main() -> testresult::TestResult {
560    /// let device_config = DeviceConfig::new(
561    ///     Connection::new(
562    ///         "https://example.org/api/v1".parse()?,
563    ///         ConnectionSecurity::Unsafe,
564    ///     ),
565    ///     vec![ConfigCredentials::new(
566    ///         UserRole::Administrator,
567    ///         "admin1".parse()?,
568    ///         Some("my-passphrase".to_string()),
569    ///     )],
570    ///     ConfigInteractivity::NonInteractive,
571    /// )?;
572    /// device_config.add_credentials(ConfigCredentials::new(
573    ///     UserRole::Operator,
574    ///     "user1".parse()?,
575    ///     Some("my-passphrase".to_string()),
576    /// ))?;
577    ///
578    /// device_config.get_matching_credentials(&[UserRole::Operator], &["user1".parse()?])?;
579    /// device_config.get_matching_credentials(&[UserRole::Administrator], &["admin1".parse()?])?;
580    /// assert_eq!(
581    ///     device_config
582    ///         .get_matching_credentials(&[UserRole::Operator], &[])?
583    ///         .get_name(),
584    ///     "user1".parse()?
585    /// );
586    /// assert_eq!(
587    ///     device_config
588    ///         .get_matching_credentials(&[UserRole::Administrator], &[])?
589    ///         .get_name(),
590    ///     "admin1".parse()?
591    /// );
592    ///
593    /// // this fails because we must provide a role to match against
594    /// assert!(
595    ///     device_config
596    ///         .get_matching_credentials(&[], &["user1".parse()?])
597    ///         .is_err()
598    /// );
599    ///
600    /// // this fails because no user in the requested role exists
601    /// assert!(
602    ///     device_config
603    ///         .get_matching_credentials(&[UserRole::Metrics], &[])
604    ///         .is_err()
605    /// );
606    ///
607    /// // this fails because no user with the name first provided exists
608    /// assert!(
609    ///     device_config
610    ///         .get_matching_credentials(&[UserRole::Operator], &["user2".parse()?, "user1".parse()?])
611    ///         .is_err()
612    /// );
613    ///
614    /// // this fails because no user in the requested role with any of the provided names exists
615    /// assert!(
616    ///     device_config
617    ///         .get_matching_credentials(&[UserRole::Metrics], &["admin1".parse()?, "user1".parse()?])
618    ///         .is_err()
619    /// );
620    /// # Ok(())
621    /// # }
622    /// ```
623    pub fn get_matching_credentials(
624        &self,
625        roles: &[UserRole],
626        names: &[UserId],
627    ) -> Result<ConfigCredentials, Error> {
628        if names.is_empty() {
629            let creds = self
630                .credentials
631                .borrow()
632                .iter()
633                .filter_map(|creds| {
634                    if roles.contains(&creds.get_role()) {
635                        Some(creds.clone())
636                    } else {
637                        None
638                    }
639                })
640                .collect::<Vec<ConfigCredentials>>();
641            return creds
642                .first()
643                .ok_or_else(|| Error::NoMatchingCredentials(roles.to_vec()))
644                .cloned();
645        }
646
647        for name in names {
648            if let Ok(creds) = &self.get_credentials(name) {
649                if roles.contains(&creds.get_role()) {
650                    return Ok(creds.clone());
651                }
652            } else {
653                return Err(Error::CredentialsMissing(name.to_owned()));
654            }
655        }
656
657        Err(Error::MatchingCredentialsMissing {
658            names: names.to_vec(),
659            roles: roles.to_vec(),
660        })
661    }
662
663    /// Returns a [`NetHsm`] based on the [`DeviceConfig`] (optionally with one set of credentials)
664    ///
665    /// Creates a [`NetHsm`] based on the [`DeviceConfig`].
666    /// Only if `roles` is not empty, one set of [`ConfigCredentials`] based on `roles`,
667    /// `names` and `passphrases` is added to the [`NetHsm`].
668    ///
669    /// **WARNING**: Depending on the [`ConfigInteractivity`] chosen when initializing the
670    /// [`DeviceConfig`] this method behaves differently with regards to adding credentials!
671    ///
672    /// # [`NonInteractive`][`ConfigInteractivity::NonInteractive`]
673    ///
674    /// If `roles` is not empty, optionally adds one set of [`ConfigCredentials`] found by
675    /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] to the returned
676    /// [`NetHsm`], based on `roles` and `names`.
677    /// If the found [`ConfigCredentials`] do not contain a passphrase, a [`Passphrase`] in
678    /// `pasphrases` with the same index as that of the [`UserId`] in `names` is used.
679    ///
680    /// # [`Interactive`][`ConfigInteractivity::Interactive`]
681    ///
682    /// If `roles` is not empty, optionally attempts to add one set of [`ConfigCredentials`] with
683    /// the help of [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] to the
684    /// returned [`NetHsm`], based on `roles` and `names`.
685    /// If no [`ConfigCredentials`] are found by
686    /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`], users are
687    /// interactively prompted for providing a user name.
688    /// If the found or prompted for [`UserId`] [`ConfigCredentials`] do not contain a passphrase, a
689    /// [`Passphrase`] in `pasphrases` with the same index as that of the [`UserId`] in `names`
690    /// is used. If [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`], or
691    /// those the user has been prompted for provides [`ConfigCredentials`] without a
692    /// passphrase, a [`Passphrase`] in `pasphrases` with the same index as that of the
693    /// [`UserId`] in `names` is used. If none is provided (at the right location) in `passphrases`,
694    /// the user is prompted for a passphrase interactively.
695    ///
696    /// # Errors
697    ///
698    /// Returns an [`Error::NoMatchingCredentials`], [`Error::CredentialsMissing`], or
699    /// [`Error::MatchingCredentialsMissing`] if the [`DeviceConfig`] is initialized with
700    /// [`Interactive`][`ConfigInteractivity::Interactive`] and
701    /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] is unable to return
702    /// [`ConfigCredentials`] based on `roles` and `names`.
703    ///
704    /// Returns an [`Error::NonInteractive`] if the [`DeviceConfig`] is initialized with
705    /// [`NonInteractive`][`ConfigInteractivity::NonInteractive`], but additional data would be
706    /// requested interactively.
707    ///
708    /// Returns an [`Error::Prompt`] if requesting additional data interactively leads to error.
709    ///
710    /// # Examples
711    ///
712    /// ```
713    /// use nethsm::{Connection, ConnectionSecurity, Passphrase, UserRole};
714    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
715    ///
716    /// # fn main() -> testresult::TestResult {
717    /// let device_config = DeviceConfig::new(
718    ///     Connection::new(
719    ///         "https://example.org/api/v1".parse()?,
720    ///         ConnectionSecurity::Unsafe,
721    ///     ),
722    ///     vec![ConfigCredentials::new(
723    ///         UserRole::Administrator,
724    ///         "admin1".parse()?,
725    ///         Some("my-passphrase".to_string()),
726    ///     )],
727    ///     ConfigInteractivity::NonInteractive,
728    /// )?;
729    /// device_config.add_credentials(ConfigCredentials::new(
730    ///     UserRole::Operator,
731    ///     "user1".parse()?,
732    ///     None,
733    /// ))?;
734    ///
735    /// // NetHsm with Operator credentials
736    /// // this works non-interactively, although the credentials in the config provide no passphrase, because we provide the passphrase manually
737    /// device_config.nethsm_with_matching_creds(
738    ///     &[UserRole::Operator],
739    ///     &["user1".parse()?],
740    ///     &[Passphrase::new("my-passphrase".to_string())],
741    /// )?;
742    ///
743    /// // NetHsm with Administrator credentials
744    /// // this automatically selects "admin1" as it is the only user in the Administrator role
745    /// // this works non-interactively, because the credentials in the config provide a passphrase!
746    /// device_config.nethsm_with_matching_creds(
747    ///     &[UserRole::Administrator],
748    ///     &[],
749    ///     &[],
750    /// )?;
751    ///
752    /// // a NetHsm without any credentials
753    /// device_config.nethsm_with_matching_creds(
754    ///     &[],
755    ///     &[],
756    ///     &[],
757    /// )?;
758    ///
759    /// // this fails because the config is non-interactive, the targeted credentials do not offer a passphrase and we also provide none
760    /// assert!(device_config.nethsm_with_matching_creds(
761    ///     &[UserRole::Operator],
762    ///     &["user1".parse()?],
763    ///     &[],
764    /// ).is_err());
765    ///
766    /// // this fails because the config is non-interactive and the targeted credentials do not exist
767    /// assert!(device_config.nethsm_with_matching_creds(
768    ///     &[UserRole::Operator],
769    ///     &["user2".parse()?],
770    ///     &[],
771    /// ).is_err());
772    ///
773    /// // this fails because the config is non-interactive and no user in the targeted role exists
774    /// assert!(device_config.nethsm_with_matching_creds(
775    ///     &[UserRole::Metrics],
776    ///     &[],
777    ///     &[],
778    /// ).is_err());
779    /// # Ok(())
780    /// # }
781    /// ```
782    pub fn nethsm_with_matching_creds(
783        &self,
784        roles: &[UserRole],
785        names: &[UserId],
786        passphrases: &[Passphrase],
787    ) -> Result<NetHsm, Error> {
788        let nethsm: NetHsm = self.try_into()?;
789
790        // do not add any users if no user roles are requested
791        if !roles.is_empty() {
792            // try to find a user name with a role in the requested set of credentials
793            let creds = if let Ok(creds) = self.get_matching_credentials(roles, names) {
794                creds
795            // or request a user name in the first requested role
796            } else {
797                // if running non-interactively, return Error
798                if self.interactivity == ConfigInteractivity::NonInteractive {
799                    return Err(Error::NonInteractive);
800                }
801
802                let role = roles.first().expect("We have at least one user role");
803                ConfigCredentials::new(
804                    role.to_owned(),
805                    UserPrompt::new(role.to_owned()).prompt()?,
806                    None,
807                )
808            };
809
810            // if no passphrase is set for the credentials, attempt to set it
811            let credentials = if !creds.has_passphrase() {
812                // get index of the found credentials name in the input names
813                let name_index = names.iter().position(|name| name == &creds.get_name());
814                if let Some(name_index) = name_index {
815                    // if a passphrase index in passphrases matches the index of the user, use it
816                    if let Some(passphrase) = passphrases.get(name_index) {
817                        Credentials::new(creds.get_name(), Some(passphrase.clone()))
818                        // else try to set the passphrase interactively
819                    } else {
820                        // if running non-interactively, return Error
821                        if self.interactivity == ConfigInteractivity::NonInteractive {
822                            return Err(Error::NonInteractive);
823                        }
824                        Credentials::new(
825                            creds.get_name(),
826                            Some(
827                                PassphrasePrompt::User {
828                                    user_id: Some(creds.get_name()),
829                                    real_name: None,
830                                }
831                                .prompt()?,
832                            ),
833                        )
834                    }
835                    // else try to set the passphrase interactively
836                } else {
837                    // if running non-interactively, return Error
838                    if self.interactivity == ConfigInteractivity::NonInteractive {
839                        return Err(Error::NonInteractive);
840                    }
841                    Credentials::new(
842                        creds.get_name(),
843                        Some(
844                            PassphrasePrompt::User {
845                                user_id: Some(creds.get_name()),
846                                real_name: None,
847                            }
848                            .prompt()?,
849                        ),
850                    )
851                }
852            } else {
853                creds.into()
854            };
855
856            let user_id = credentials.user_id.clone();
857            // add the found credentials
858            nethsm.add_credentials(credentials);
859            // use the found credentials by default
860            nethsm.use_credentials(&user_id)?;
861        }
862
863        Ok(nethsm)
864    }
865}
866
867impl TryFrom<DeviceConfig> for NetHsm {
868    type Error = Error;
869    fn try_from(value: DeviceConfig) -> Result<Self, Error> {
870        let nethsm = NetHsm::new(value.connection.borrow().clone(), None, None, None)?;
871        for creds in value.credentials.borrow().clone().into_iter() {
872            nethsm.add_credentials(creds.into())
873        }
874        Ok(nethsm)
875    }
876}
877
878impl TryFrom<&DeviceConfig> for NetHsm {
879    type Error = Error;
880    fn try_from(value: &DeviceConfig) -> Result<Self, Error> {
881        let nethsm = NetHsm::new(value.connection.borrow().clone(), None, None, None)?;
882        for creds in value.credentials.borrow().clone().into_iter() {
883            nethsm.add_credentials(creds.into())
884        }
885        Ok(nethsm)
886    }
887}
888
889/// A configuration for NetHSM devices
890///
891/// Tracks a set of [`DeviceConfig`]s hashed by label.
892#[derive(Clone, Debug, Default, Deserialize, Serialize)]
893pub struct Config {
894    devices: RefCell<HashMap<String, DeviceConfig>>,
895    #[serde(skip)]
896    config_settings: ConfigSettings,
897}
898
899impl Config {
900    /// Loads the configuration
901    ///
902    /// If `path` is `Some`, the configuration is loaded from a specific file.
903    /// If `path` is `None`, a default location is assumed. The default location depends on the
904    /// chosen [`app_name`][`ConfigSettings::app_name`] and the OS platform. Assuming
905    /// [`app_name`][`ConfigSettings::app_name`] is `"my_app"` on Linux the default location is
906    /// `~/.config/my_app/config.toml`.
907    ///
908    /// If the targeted configuration file does not yet exist, an empty default [`Config`] is
909    /// assumed.
910    ///
911    /// # Errors
912    ///
913    /// Returns an [`Error::Load`] if loading the configuration file fails.
914    ///
915    /// # Examples
916    ///
917    /// ```
918    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
919    ///
920    /// # fn main() -> testresult::TestResult {
921    /// let config_settings = ConfigSettings::new(
922    ///     "my_app".to_string(),
923    ///     ConfigInteractivity::NonInteractive,
924    ///     None,
925    /// );
926    /// let config_from_default = Config::new(config_settings.clone(), None)?;
927    ///
928    /// let tmpfile = testdir::testdir!().join("my_app_new.conf");
929    /// let config_from_file = Config::new(config_settings, Some(&tmpfile))?;
930    /// # Ok(())
931    /// # }
932    /// ```
933    pub fn new(config_settings: ConfigSettings, path: Option<&Path>) -> Result<Self, Error> {
934        let mut config: Config = if let Some(path) = path {
935            confy::load_path(path).map_err(|error| Error::Load {
936                description: if let Some(error) = error.source() {
937                    error.to_string()
938                } else {
939                    "".to_string()
940                },
941                source: error,
942            })?
943        } else {
944            confy::load(
945                &config_settings.app_name,
946                Some(config_settings.config_name.0.as_str()),
947            )
948            .map_err(|error| Error::Load {
949                description: if let Some(error) = error.source() {
950                    error.to_string()
951                } else {
952                    "".to_string()
953                },
954                source: error,
955            })?
956        };
957        for (_label, device) in config.devices.borrow_mut().iter_mut() {
958            device.set_config_interactivity(config_settings.interactivity);
959        }
960        config.set_config_settings(config_settings);
961
962        Ok(config)
963    }
964
965    fn set_config_settings(&mut self, config_settings: ConfigSettings) {
966        self.config_settings = config_settings
967    }
968
969    /// Adds a [`DeviceConfig`]
970    ///
971    /// A device is defined by its `label`, the `url` to connect to and the chosen `tls_security`
972    /// for the connection.
973    ///
974    /// # Errors
975    ///
976    /// Returns an [`Error::DeviceExists`] if a [`DeviceConfig`] with the same `label` exists
977    /// already.
978    ///
979    /// # Examples
980    ///
981    /// ```
982    /// use nethsm::ConnectionSecurity;
983    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
984    /// # fn main() -> testresult::TestResult {
985    /// # let config = Config::new(
986    /// #    ConfigSettings::new(
987    /// #        "my_app".to_string(),
988    /// #        ConfigInteractivity::NonInteractive,
989    /// #        None,
990    /// #    ),
991    /// #    Some(&testdir::testdir!().join("my_app_add_device.conf")),
992    /// # )?;
993    ///
994    /// config.add_device(
995    ///     "device1".to_string(),
996    ///     "https://example.org/api/v1".parse()?,
997    ///     ConnectionSecurity::Unsafe,
998    /// )?;
999    ///
1000    /// // adding the same device again leads to error
1001    /// assert!(
1002    ///     config
1003    ///         .add_device(
1004    ///             "device1".to_string(),
1005    ///             "https://example.org/api/v1".parse()?,
1006    ///             ConnectionSecurity::Unsafe,
1007    ///         )
1008    ///         .is_err()
1009    /// );
1010    /// # Ok(())
1011    /// # }
1012    /// ```
1013    pub fn add_device(
1014        &self,
1015        label: String,
1016        url: Url,
1017        tls_security: ConnectionSecurity,
1018    ) -> Result<(), Error> {
1019        if let Entry::Vacant(entry) = self.devices.borrow_mut().entry(label.clone()) {
1020            entry.insert(DeviceConfig::new(
1021                Connection::new(url, tls_security),
1022                vec![],
1023                self.config_settings.interactivity,
1024            )?);
1025            Ok(())
1026        } else {
1027            Err(Error::DeviceExists(label))
1028        }
1029    }
1030
1031    /// Deletes a [`DeviceConfig`] identified by `label`
1032    ///
1033    /// # Errors
1034    ///
1035    /// Returns an [`Error::DeviceMissing`] if no [`DeviceConfig`] with a matching `label` exists.
1036    ///
1037    /// # Examples
1038    ///
1039    /// ```
1040    /// use nethsm::ConnectionSecurity;
1041    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1042    /// # fn main() -> testresult::TestResult {
1043    /// # let config = Config::new(
1044    /// #    ConfigSettings::new(
1045    /// #        "my_app".to_string(),
1046    /// #        ConfigInteractivity::NonInteractive,
1047    /// #        None,
1048    /// #    ),
1049    /// #    Some(&testdir::testdir!().join("my_app_delete_device.conf")),
1050    /// # )?;
1051    ///
1052    /// config.add_device(
1053    ///     "device1".to_string(),
1054    ///     "https://example.org/api/v1".parse()?,
1055    ///     ConnectionSecurity::Unsafe,
1056    /// )?;
1057    ///
1058    /// config.delete_device("device1")?;
1059    ///
1060    /// // deleting a non-existent device leads to error
1061    /// assert!(config.delete_device("device1",).is_err());
1062    /// # Ok(())
1063    /// # }
1064    /// ```
1065    pub fn delete_device(&self, label: &str) -> Result<(), Error> {
1066        if self.devices.borrow_mut().remove(label).is_some() {
1067            Ok(())
1068        } else {
1069            Err(Error::DeviceMissing(label.to_string()))
1070        }
1071    }
1072
1073    /// Returns a single [`DeviceConfig`] from the [`Config`] based on an optional `label`
1074    ///
1075    /// If `label` is [`Some`], a specific [`DeviceConfig`] is retrieved.
1076    /// If `label` is [`None`] and only one device is defined in the config, then the
1077    /// [`DeviceConfig`] for that device is returned.
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns an [`Error::DeviceMissing`] if `label` is [`Some`] but it can not be found in the
1082    /// [`Config`].
1083    /// Returns an [`Error::NoDevice`], if `label` is [`None`] but the [`Config`] has no
1084    /// devices.
1085    /// Returns an [`Error::NoDevice`], if `label` is [`None`] and the [`Config`] has more than one
1086    /// device.
1087    ///
1088    /// # Examples
1089    ///
1090    /// ```
1091    /// use nethsm::ConnectionSecurity;
1092    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1093    /// # fn main() -> testresult::TestResult {
1094    /// # let config = Config::new(
1095    /// #    ConfigSettings::new(
1096    /// #        "my_app".to_string(),
1097    /// #        ConfigInteractivity::NonInteractive,
1098    /// #        None,
1099    /// #    ),
1100    /// #    Some(&testdir::testdir!().join("my_app_get_device.conf")),
1101    /// # )?;
1102    ///
1103    /// config.add_device(
1104    ///     "device1".to_string(),
1105    ///     "https://example.org/api/v1".parse()?,
1106    ///     ConnectionSecurity::Unsafe,
1107    /// )?;
1108    ///
1109    /// config.get_device(Some("device1"))?;
1110    ///
1111    /// // this fails because the device does not exist
1112    /// assert!(config.get_device(Some("device2")).is_err());
1113    ///
1114    /// config.add_device(
1115    ///     "device2".to_string(),
1116    ///     "https://example.org/other/api/v1".parse()?,
1117    ///     ConnectionSecurity::Unsafe,
1118    /// )?;
1119    /// // this fails because there is more than one device
1120    /// assert!(config.get_device(None).is_err());
1121    ///
1122    /// config.delete_device("device1")?;
1123    /// config.delete_device("device2")?;
1124    /// // this fails because there is no device
1125    /// assert!(config.get_device(None).is_err());
1126    /// # Ok(())
1127    /// # }
1128    /// ```
1129    pub fn get_device(&self, label: Option<&str>) -> Result<DeviceConfig, Error> {
1130        if let Some(label) = label {
1131            if let Some(device_config) = self.devices.borrow().get(label) {
1132                Ok(device_config.clone())
1133            } else {
1134                Err(Error::DeviceMissing(label.to_string()))
1135            }
1136        } else {
1137            match self.devices.borrow().len() {
1138                0 => Err(Error::NoDevice),
1139                1 => Ok(self
1140                    .devices
1141                    .borrow()
1142                    .values()
1143                    .next()
1144                    .expect("there should be one")
1145                    .to_owned()),
1146                _ => Err(Error::MoreThanOneDevice),
1147            }
1148        }
1149    }
1150
1151    /// Returns a single [`DeviceConfig`] label from the [`Config`]
1152    ///
1153    /// # Errors
1154    ///
1155    /// Returns an error if not exactly one [`DeviceConfig`] is present.
1156    ///
1157    /// # Examples
1158    ///
1159    /// ```
1160    /// use nethsm::ConnectionSecurity;
1161    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1162    /// # fn main() -> testresult::TestResult {
1163    /// # let config = Config::new(
1164    /// #    ConfigSettings::new(
1165    /// #        "my_app".to_string(),
1166    /// #        ConfigInteractivity::NonInteractive,
1167    /// #        None,
1168    /// #    ),
1169    /// #    Some(&testdir::testdir!().join("my_app_get_single_device_label.conf")),
1170    /// # )?;
1171    ///
1172    /// config.add_device(
1173    ///     "device1".to_string(),
1174    ///     "https://example.org/api/v1".parse()?,
1175    ///     ConnectionSecurity::Unsafe,
1176    /// )?;
1177    ///
1178    /// assert_eq!(config.get_single_device_label()?, "device1".to_string());
1179    ///
1180    /// config.add_device(
1181    ///     "device2".to_string(),
1182    ///     "https://example.org/other/api/v1".parse()?,
1183    ///     ConnectionSecurity::Unsafe,
1184    /// )?;
1185    /// // this fails because there is more than one device
1186    /// assert!(config.get_single_device_label().is_err());
1187    ///
1188    /// config.delete_device("device1")?;
1189    /// config.delete_device("device2")?;
1190    /// // this fails because there is no device
1191    /// assert!(config.get_single_device_label().is_err());
1192    /// # Ok(())
1193    /// # }
1194    /// ```
1195    pub fn get_single_device_label(&self) -> Result<String, Error> {
1196        if self.devices.borrow().keys().len() == 1 {
1197            self.devices
1198                .borrow()
1199                .keys()
1200                .next()
1201                .map(|label| label.to_string())
1202                .ok_or(Error::NoDevice)
1203        } else {
1204            Err(Error::MoreThanOneDevice)
1205        }
1206    }
1207
1208    /// Adds new credentials for a [`DeviceConfig`]
1209    ///
1210    /// Using a `label` that identifies a [`DeviceConfig`], new credentials tracking a [`UserRole`],
1211    /// a name and optionally a passphrase are added to it.
1212    ///
1213    /// # Errors
1214    ///
1215    /// Returns an [`Error::DeviceMissing`] if the targeted [`DeviceConfig`] does not exist.
1216    /// Returns an [`Error::CredentialsExist`] if the [`ConfigCredentials`] identified by `name`
1217    /// exist already.
1218    ///
1219    /// # Examples
1220    ///
1221    /// ```
1222    /// use nethsm::{ConnectionSecurity, UserRole};
1223    /// use nethsm_config::{Config, ConfigCredentials, ConfigInteractivity, ConfigSettings};
1224    /// # fn main() -> testresult::TestResult {
1225    /// # let config = Config::new(
1226    /// #    ConfigSettings::new(
1227    /// #        "my_app".to_string(),
1228    /// #        ConfigInteractivity::NonInteractive,
1229    /// #        None,
1230    /// #    ),
1231    /// #    Some(&testdir::testdir!().join("my_app_add_credentials.conf")),
1232    /// # )?;
1233    ///
1234    /// // this fails because the targeted device does not yet exist
1235    /// assert!(
1236    ///     config
1237    ///         .add_credentials(
1238    ///             "device1".to_string(),
1239    ///             ConfigCredentials::new(
1240    ///                 UserRole::Operator,
1241    ///                 "user1".parse()?,
1242    ///                 Some("my-passphrase".to_string()),
1243    ///             ),
1244    ///         )
1245    ///         .is_err()
1246    /// );
1247    ///
1248    /// config.add_device(
1249    ///     "device1".to_string(),
1250    ///     "https://example.org/api/v1".parse()?,
1251    ///     ConnectionSecurity::Unsafe,
1252    /// )?;
1253    ///
1254    /// config.add_credentials(
1255    ///     "device1".to_string(),
1256    ///     ConfigCredentials::new(
1257    ///         UserRole::Operator,
1258    ///         "user1".parse()?,
1259    ///         Some("my-passphrase".to_string()),
1260    ///     ),
1261    /// )?;
1262    ///
1263    /// // this fails because the credentials exist already
1264    /// assert!(
1265    ///     config
1266    ///         .add_credentials(
1267    ///             "device1".to_string(),
1268    ///             ConfigCredentials::new(
1269    ///                 UserRole::Operator,
1270    ///                 "user1".parse()?,
1271    ///                 Some("my-passphrase".to_string()),
1272    ///             ),
1273    ///         )
1274    ///         .is_err()
1275    /// );
1276    /// # Ok(())
1277    /// # }
1278    /// ```
1279    pub fn add_credentials(
1280        &self,
1281        label: String,
1282        credentials: ConfigCredentials,
1283    ) -> Result<(), Error> {
1284        if let Some(device) = self.devices.borrow_mut().get_mut(&label) {
1285            device.add_credentials(credentials)?
1286        } else {
1287            return Err(Error::DeviceMissing(label));
1288        }
1289
1290        Ok(())
1291    }
1292
1293    /// Deletes credentials from a [`DeviceConfig`]
1294    ///
1295    /// The `label` identifies the [`DeviceConfig`] and the `name` the name of the credentials.
1296    ///
1297    /// # Errors
1298    ///
1299    /// Returns an [`Error::DeviceMissing`] if the targeted [`DeviceConfig`] does not exist.
1300    /// Returns an [`Error::CredentialsMissing`] if the targeted [`ConfigCredentials`] do not exist.
1301    ///
1302    /// # Examples
1303    ///
1304    /// ```
1305    /// use nethsm::{ConnectionSecurity, UserRole};
1306    /// use nethsm_config::{Config, ConfigCredentials, ConfigInteractivity, ConfigSettings};
1307    /// # fn main() -> testresult::TestResult {
1308    /// # let config = Config::new(
1309    /// #    ConfigSettings::new(
1310    /// #        "my_app".to_string(),
1311    /// #        ConfigInteractivity::NonInteractive,
1312    /// #        None,
1313    /// #    ),
1314    /// #    Some(&testdir::testdir!().join("my_app_delete_credentials.conf")),
1315    /// # )?;
1316    ///
1317    /// // this fails because the targeted device does not yet exist
1318    /// assert!(
1319    ///     config
1320    ///         .delete_credentials("device1", &"user1".parse()?)
1321    ///         .is_err()
1322    /// );
1323    ///
1324    /// config.add_device(
1325    ///     "device1".to_string(),
1326    ///     "https://example.org/api/v1".parse()?,
1327    ///     ConnectionSecurity::Unsafe,
1328    /// )?;
1329    ///
1330    /// // this fails because the targeted credentials does not yet exist
1331    /// assert!(
1332    ///     config
1333    ///         .delete_credentials("device1", &"user1".parse()?)
1334    ///         .is_err()
1335    /// );
1336    ///
1337    /// config.add_credentials(
1338    ///     "device1".to_string(),
1339    ///     ConfigCredentials::new(
1340    ///         UserRole::Operator,
1341    ///         "user1".parse()?,
1342    ///         Some("my-passphrase".to_string()),
1343    ///     ),
1344    /// )?;
1345    ///
1346    /// config.delete_credentials("device1", &"user1".parse()?)?;
1347    /// # Ok(())
1348    /// # }
1349    /// ```
1350    pub fn delete_credentials(&self, label: &str, name: &UserId) -> Result<(), Error> {
1351        if let Some(device) = self.devices.borrow_mut().get_mut(label) {
1352            device.delete_credentials(name)?
1353        } else {
1354            return Err(Error::DeviceMissing(label.to_string()));
1355        }
1356
1357        Ok(())
1358    }
1359
1360    /// Returns the [`ConfigSettings`] of the [`Config`]
1361    ///
1362    /// # Examples
1363    ///
1364    /// ```
1365    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1366    /// # fn main() -> testresult::TestResult {
1367    /// let config_settings = ConfigSettings::new(
1368    ///     "my_app".to_string(),
1369    ///     ConfigInteractivity::NonInteractive,
1370    ///     None,
1371    /// );
1372    /// let config = Config::new(
1373    ///     config_settings.clone(),
1374    ///     Some(&testdir::testdir!().join("my_app_get_config_settings.conf")),
1375    /// )?;
1376    ///
1377    /// println!("{:?}", config.get_config_settings());
1378    /// # assert_eq!(config.get_config_settings(), config_settings);
1379    /// # Ok(())
1380    /// # }
1381    /// ```
1382    pub fn get_config_settings(&self) -> ConfigSettings {
1383        self.config_settings.clone()
1384    }
1385
1386    /// Returns the default config file location
1387    ///
1388    /// # Errors
1389    ///
1390    /// Returns an [`Error::ConfigFileLocation`] if the config file location can not be retrieved.
1391    ///
1392    /// # Examples
1393    ///
1394    /// ```
1395    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1396    /// # fn main() -> testresult::TestResult {
1397    /// let config = Config::new(
1398    ///     ConfigSettings::new(
1399    ///         "my_app".to_string(),
1400    ///         ConfigInteractivity::NonInteractive,
1401    ///         None,
1402    ///     ),
1403    ///     Some(&testdir::testdir!().join("my_app_get_default_config_file_path.conf")),
1404    /// )?;
1405    ///
1406    /// println!("{:?}", config.get_default_config_file_path()?);
1407    /// # assert_eq!(
1408    /// #     config.get_default_config_file_path()?,
1409    /// #     dirs::config_dir()
1410    /// #         .ok_or("Platform does not support config dir")?
1411    /// #         .join(config.get_config_settings().app_name())
1412    /// #         .join(format!(
1413    /// #             "{}.toml",
1414    /// #             config.get_config_settings().config_name()
1415    /// #         ))
1416    /// # );
1417    /// # Ok(())
1418    /// # }
1419    /// ```
1420    pub fn get_default_config_file_path(&self) -> Result<PathBuf, Error> {
1421        confy::get_configuration_file_path(
1422            &self.config_settings.app_name,
1423            Some(self.config_settings.config_name().0.as_str()),
1424        )
1425        .map_err(Error::ConfigFileLocation)
1426    }
1427
1428    /// Writes the configuration to file
1429    ///
1430    /// # Errors
1431    ///
1432    /// Returns an [`Error::Store`] if the configuration can not be written to file.
1433    ///
1434    /// # Examples
1435    ///
1436    /// ```
1437    /// use nethsm::ConnectionSecurity;
1438    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1439    ///
1440    /// # fn main() -> testresult::TestResult {
1441    /// let config_file = testdir::testdir!().join("my_app_store.conf");
1442    /// # let config = Config::new(
1443    /// #    ConfigSettings::new(
1444    /// #        "my_app".to_string(),
1445    /// #        ConfigInteractivity::NonInteractive,
1446    /// #        None,
1447    /// #    ),
1448    /// #    Some(&config_file),
1449    /// # )?;
1450    /// # config.add_device(
1451    /// #     "device1".to_string(),
1452    /// #     "https://example.org/api/v1".parse()?,
1453    /// #     ConnectionSecurity::Unsafe,
1454    /// # )?;
1455    /// config.store(Some(&config_file))?;
1456    ///
1457    /// // this fails because we can not write the configuration to a directory
1458    /// assert!(config.store(Some(&testdir::testdir!())).is_err());
1459    /// # // remove the config file again as we otherwise influence other tests
1460    /// # std::fs::remove_file(&config_file);
1461    /// # Ok(())
1462    /// # }
1463    /// ```
1464    pub fn store(&self, path: Option<&Path>) -> Result<(), Error> {
1465        if let Some(path) = path {
1466            confy::store_path(path, self).map_err(Error::Store)
1467        } else {
1468            confy::store(&self.config_settings.app_name, "config", self).map_err(Error::Store)
1469        }
1470    }
1471}
1472
1473/// The handling of administrative secrets.
1474///
1475/// Administrative secrets may be handled in different ways (e.g. persistent or non-persistent).
1476#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1477#[serde(rename_all = "kebab-case")]
1478pub enum AdministrativeSecretHandling {
1479    /// The administrative secrets are handled in a plaintext file in a non-volatile directory.
1480    ///
1481    /// ## Warning
1482    ///
1483    /// This variant should only be used in non-production test setups, as it implies the
1484    /// persistence of unencrypted administrative secrets on a file system.
1485    Plaintext,
1486
1487    /// The administrative secrets are handled in a file encrypted using [systemd-creds] in a
1488    /// non-volatile directory.
1489    ///
1490    /// ## Warning
1491    ///
1492    /// This variant should only be used in non-production test setups, as it implies the
1493    /// persistence of (host-specific) encrypted administrative secrets on a file system, that
1494    /// could be extracted if the host is compromised.
1495    ///
1496    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
1497    SystemdCreds,
1498
1499    /// The administrative secrets are handled using [Shamir's Secret Sharing] (SSS).
1500    ///
1501    /// This variant is the default for production use, as the administrative secrets are only ever
1502    /// exposed on a volatile filesystem for the time of their use.
1503    /// The secrets are only made available to the system as shares of a shared secret, split using
1504    /// SSS.
1505    /// This way no holder of a share is aware of the administrative secrets and the system only
1506    /// for as long as it needs to use the administrative secrets.
1507    ///
1508    /// [Shamir's Secret Sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
1509    #[default]
1510    ShamirsSecretSharing,
1511}
1512
1513/// The handling of non-administrative secrets.
1514///
1515/// Non-administrative secrets represent passphrases for (non-Administrator) NetHSM users and may be
1516/// handled in different ways (e.g. encrypted or not encrypted).
1517#[derive(
1518    Clone,
1519    Copy,
1520    Debug,
1521    Default,
1522    Deserialize,
1523    strum::Display,
1524    strum::EnumString,
1525    Eq,
1526    PartialEq,
1527    Serialize,
1528)]
1529#[serde(rename_all = "kebab-case")]
1530#[strum(serialize_all = "kebab-case")]
1531pub enum NonAdministrativeSecretHandling {
1532    /// Each non-administrative secret is handled in a plaintext file in a non-volatile
1533    /// directory.
1534    ///
1535    /// ## Warning
1536    ///
1537    /// This variant should only be used in non-production test setups, as it implies the
1538    /// persistence of unencrypted non-administrative secrets on a file system.
1539    Plaintext,
1540
1541    /// Each non-administrative secret is encrypted for a specific system user using
1542    /// [systemd-creds] and the resulting files are stored in a non-volatile directory.
1543    ///
1544    /// ## Note
1545    ///
1546    /// Although secrets are stored as encrypted strings in dedicated files, they may be extracted
1547    /// under certain circumstances:
1548    ///
1549    /// - the root account is compromised
1550    ///   - decrypts and exfiltrates _all_ secrets
1551    ///   - the secret is not encrypted using a [TPM] and the file
1552    ///     `/var/lib/systemd/credential.secret` as well as _any_ encrypted secret is exfiltrated
1553    /// - a specific user is compromised, decrypts and exfiltrates its own ssecret
1554    ///
1555    /// It is therefore crucial to follow common best-practices:
1556    ///
1557    /// - rely on a [TPM] for encrypting secrets, so that files become host-specific
1558    /// - heavily guard access to all users, especially root
1559    ///
1560    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
1561    /// [TPM]: https://en.wikipedia.org/wiki/Trusted_Platform_Module
1562    #[default]
1563    SystemdCreds,
1564}
1565
1566/// A configuration for parallel use of connections with a set of system and NetHSM users.
1567///
1568/// This configuration type is meant to be used in a read-only fashion and does not support tracking
1569/// the passphrases for users.
1570/// As such, it is useful for tools, that create system users, as well as NetHSM users and keys
1571/// according to it.
1572///
1573/// Various mappings of system and [`NetHsm`] users exist, that are defined by the variants of
1574/// [`UserMapping`].
1575///
1576/// Some system users require providing SSH authorized key(s), while others do not allow that at
1577/// all.
1578/// NetHSM users can be added in namespaces, or system-wide, depending on their use-case.
1579/// System and NetHSM users must be unique.
1580///
1581/// Key IDs must be unique per namespace or system-wide (depending on where they are used).
1582/// Tags, used to provide access to keys for NetHSM users must be unique per namespace or
1583/// system-wide (depending on in which scope the user and key are used)
1584///
1585/// # Examples
1586///
1587/// The below example provides a fully functional TOML configuration, outlining all available
1588/// functionalities.
1589///
1590/// ```
1591/// # use std::io::Write;
1592/// #
1593/// # use nethsm_config::{ConfigInteractivity, ConfigName, ConfigSettings, HermeticParallelConfig};
1594/// #
1595/// # fn main() -> testresult::TestResult {
1596/// # let config_file = testdir::testdir!().join("basic_parallel_config_example.conf");
1597/// # {
1598/// let config_string = r#"
1599/// ## A non-negative integer, that describes the iteration of the configuration.
1600/// ## The iteration should only ever be increased between changes to the config and only under the circumstance,
1601/// ## that user mappings are removed and should also be removed from the state of the system making use of this
1602/// ## configuration.
1603/// ## Applications reading the configuration are thereby enabled to compare existing state on the system with the
1604/// ## current iteration and remove user mappings and accompanying data accordingly.
1605/// iteration = 1
1606///
1607/// ## The handling of administrative secrets on the system.
1608/// ## One of:
1609/// ## - "shamirs-secret-sharing": Administrative secrets are never persisted on the system and only provided as shares of a shared secret.
1610/// ## - "systemd-creds": Administrative secrets are persisted on the system as host-specific files, encrypted using systemd-creds (only for testing).
1611/// ## - "plaintext": Administrative secrets are persisted on the system in unencrypted plaintext files (only for testing).
1612/// admin_secret_handling = "shamirs-secret-sharing"
1613///
1614/// ## The handling of non-administrative secrets on the system.
1615/// ## One of:
1616/// ## - "systemd-creds": Non-administrative secrets are persisted on the system as host-specific files, encrypted using systemd-creds (the default).
1617/// ## - "plaintext": Non-administrative secrets are persisted on the system in unencrypted plaintext files (only for testing).
1618/// non_admin_secret_handling = "systemd-creds"
1619///
1620/// [[connections]]
1621/// url = "https://localhost:8443/api/v1/"
1622/// tls_security = "Unsafe"
1623///
1624/// ## The NetHSM user "admin" is a system-wide Administrator
1625/// [[users]]
1626/// nethsm_only_admin = "admin"
1627///
1628/// ## The SSH-accessible system user "ssh-backup1" is used in conjunction with
1629/// ## the NetHSM user "backup1" (system-wide Backup)
1630/// [[users]]
1631///
1632/// [users.system_nethsm_backup]
1633/// nethsm_user = "backup1"
1634/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
1635/// system_user = "ssh-backup1"
1636///
1637/// ## The SSH-accessible system user "ssh-metrics1" is used with several NetHSM users:
1638/// ## - "metrics1" (system-wide Metrics)
1639/// ## - "keymetrics1" (system-wide Operator)
1640/// ## - "ns1~keymetrics1" (namespace Operator)
1641/// [[users]]
1642///
1643/// [users.system_nethsm_metrics]
1644/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
1645/// system_user = "ssh-metrics1"
1646///
1647/// [users.system_nethsm_metrics.nethsm_users]
1648/// metrics_user = "metrics1"
1649/// operator_users = ["keymetrics1", "ns1~keymetrics1"]
1650///
1651/// ## The SSH-accessible system user "ssh-operator1" is used in conjunction with
1652/// ## the NetHSM user "operator1" (system-wide Operator).
1653/// ## User "operator1" shares tag "tag1" with key "key1" and can therefore use it
1654/// ## (for OpenPGP signing).
1655/// [[users]]
1656///
1657/// [users.system_nethsm_operator_signing]
1658/// nethsm_user = "operator1"
1659/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
1660/// system_user = "ssh-operator1"
1661/// tag = "tag1"
1662///
1663/// [users.system_nethsm_operator_signing.nethsm_key_setup]
1664/// key_id = "key1"
1665/// key_type = "Curve25519"
1666/// key_mechanisms = ["EdDsaSignature"]
1667/// signature_type = "EdDsa"
1668///
1669/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1670/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1671/// version = "4"
1672///
1673/// ## The SSH-accessible system user "ssh-operator2" is used in conjunction with
1674/// ## the NetHSM user "operator2" (system-wide Operator).
1675/// ## User "operator2" shares tag "tag2" with key "key2" and can therefore use it
1676/// ## (for OpenPGP signing).
1677/// [[users]]
1678///
1679/// [users.system_nethsm_operator_signing]
1680/// nethsm_user = "operator2"
1681/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
1682/// system_user = "ssh-operator2"
1683/// tag = "tag2"
1684///
1685/// [users.system_nethsm_operator_signing.nethsm_key_setup]
1686/// key_id = "key2"
1687/// key_type = "Curve25519"
1688/// key_mechanisms = ["EdDsaSignature"]
1689/// signature_type = "EdDsa"
1690///
1691/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1692/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1693/// version = "4"
1694///
1695/// ## The NetHSM user "ns1~admin" is a namespace Administrator
1696/// [[users]]
1697/// nethsm_only_admin = "ns1~admin"
1698///
1699/// ## The SSH-accessible system user "ns1-ssh-operator1" is used in conjunction with
1700/// ## the NetHSM user "ns1~operator1" (namespace Operator).
1701/// ## User "ns1~operator1" shares tag "tag1" with key "key1" and can therefore use it
1702/// ## in its namespace (for OpenPGP signing).
1703/// [[users]]
1704///
1705/// [users.system_nethsm_operator_signing]
1706/// nethsm_user = "ns1~operator1"
1707/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host"
1708/// system_user = "ns1-ssh-operator1"
1709/// tag = "tag1"
1710///
1711/// [users.system_nethsm_operator_signing.nethsm_key_setup]
1712/// key_id = "key1"
1713/// key_type = "Curve25519"
1714/// key_mechanisms = ["EdDsaSignature"]
1715/// signature_type = "EdDsa"
1716///
1717/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1718/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1719/// version = "4"
1720///
1721/// ## The SSH-accessible system user "ns1-ssh-operator2" is used in conjunction with
1722/// ## the NetHSM user "ns2~operator1" (namespace Operator).
1723/// ## User "ns1~operator2" shares tag "tag2" with key "key1" and can therefore use it
1724/// ## in its namespace (for OpenPGP signing).
1725/// [[users]]
1726///
1727/// [users.system_nethsm_operator_signing]
1728/// nethsm_user = "ns1~operator2"
1729/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIYA+bfMBThUP5lKbMFEHiytmcCPhpkGrB/85n0mAN user@host"
1730/// system_user = "ns1-ssh-operator2"
1731/// tag = "tag2"
1732///
1733/// [users.system_nethsm_operator_signing.nethsm_key_setup]
1734/// key_id = "key2"
1735/// key_type = "Curve25519"
1736/// key_mechanisms = ["EdDsaSignature"]
1737/// signature_type = "EdDsa"
1738///
1739/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1740/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1741/// version = "4"
1742///
1743/// ## The hermetic system user "local-metrics1" is used with several NetHSM users:
1744/// ## - "metrics2" (system-wide Metrics)
1745/// ## - "keymetrics2" (system-wide Operator)
1746/// ## - "ns1~keymetrics2" (namespace Operator)
1747/// [[users]]
1748///
1749/// [users.hermetic_system_nethsm_metrics]
1750/// system_user = "local-metrics1"
1751///
1752/// [users.hermetic_system_nethsm_metrics.nethsm_users]
1753/// metrics_user = "metrics2"
1754/// operator_users = ["keymetrics2", "ns1~keymetrics2"]
1755///
1756/// ## The SSH-accessible system user "ssh-share-down" is used for the
1757/// ## download of shares of a shared secret (divided by Shamir's Secret Sharing).
1758/// [[users]]
1759///
1760/// [users.system_only_share_download]
1761/// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
1762/// system_user = "ssh-share-down"
1763///
1764/// ## The SSH-accessible system user "ssh-share-up" is used for the
1765/// ## upload of shares of a shared secret (divided by Shamir's Secret Sharing).
1766/// [[users]]
1767///
1768/// [users.system_only_share_upload]
1769/// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
1770/// system_user = "ssh-share-up"
1771///
1772/// ## The SSH-accessible system user "ssh-wireguard-down" is used for the
1773/// ## download of WireGuard configuration, used on the host.
1774/// [[users]]
1775///
1776/// [users.system_only_wireguard_download]
1777/// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"]
1778/// system_user = "ssh-wireguard-down"
1779/// "#;
1780/// #
1781/// #    let mut buffer = std::fs::File::create(&config_file)?;
1782/// #    buffer.write_all(config_string.as_bytes())?;
1783/// # }
1784/// # HermeticParallelConfig::new_from_file(
1785/// #    ConfigSettings::new(
1786/// #        "my_app".to_string(),
1787/// #        ConfigInteractivity::NonInteractive,
1788/// #        None,
1789/// #    ),
1790/// #    Some(&config_file),
1791/// # )?;
1792/// # Ok(())
1793/// # }
1794/// ```
1795#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1796pub struct HermeticParallelConfig {
1797    iteration: u32,
1798    admin_secret_handling: AdministrativeSecretHandling,
1799    non_admin_secret_handling: NonAdministrativeSecretHandling,
1800    connections: HashSet<Connection>,
1801    users: HashSet<UserMapping>,
1802    #[serde(skip)]
1803    settings: ConfigSettings,
1804}
1805
1806impl HermeticParallelConfig {
1807    /// Creates a new [`HermeticParallelConfig`] from a configuration file.
1808    ///
1809    /// # Errors
1810    ///
1811    /// Returns an error if the configuration file can not be loaded.
1812    ///
1813    /// # Examples
1814    ///
1815    /// ```
1816    /// # use std::io::Write;
1817    ///
1818    /// use nethsm_config::{ConfigInteractivity, ConfigName, ConfigSettings, HermeticParallelConfig};
1819    ///
1820    /// # fn main() -> testresult::TestResult {
1821    /// let config_file = testdir::testdir!().join("basic_parallel_config_new.conf");
1822    /// {
1823    ///     #[rustfmt::skip]
1824    ///     let config_string = r#"
1825    /// iteration = 1
1826    /// admin_secret_handling = "shamirs-secret-sharing"
1827    /// non_admin_secret_handling = "systemd-creds"
1828    /// [[connections]]
1829    /// url = "https://localhost:8443/api/v1/"
1830    /// tls_security = "Unsafe"
1831    ///
1832    /// [[users]]
1833    /// nethsm_only_admin = "admin"
1834    ///
1835    /// [[users]]
1836    /// [users.system_nethsm_backup]
1837    /// nethsm_user = "backup1"
1838    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
1839    /// system_user = "ssh-backup1"
1840    ///
1841    /// [[users]]
1842    ///
1843    /// [users.system_nethsm_metrics]
1844    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
1845    /// system_user = "ssh-metrics1"
1846    ///
1847    /// [users.system_nethsm_metrics.nethsm_users]
1848    /// metrics_user = "metrics1"
1849    /// operator_users = ["operator1metrics1"]
1850    ///
1851    /// [[users]]
1852    /// [users.system_nethsm_operator_signing]
1853    /// nethsm_user = "operator1"
1854    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
1855    /// system_user = "ssh-operator1"
1856    /// tag = "tag1"
1857    ///
1858    /// [users.system_nethsm_operator_signing.nethsm_key_setup]
1859    /// key_id = "key1"
1860    /// key_type = "Curve25519"
1861    /// key_mechanisms = ["EdDsaSignature"]
1862    /// signature_type = "EdDsa"
1863    ///
1864    /// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1865    /// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1866    /// version = "4"
1867    ///
1868    /// [[users]]
1869    /// [users.system_nethsm_operator_signing]
1870    /// nethsm_user = "operator2"
1871    /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
1872    /// system_user = "ssh-operator2"
1873    /// tag = "tag2"
1874    ///
1875    /// [users.system_nethsm_operator_signing.nethsm_key_setup]
1876    /// key_id = "key2"
1877    /// key_type = "Curve25519"
1878    /// key_mechanisms = ["EdDsaSignature"]
1879    /// signature_type = "EdDsa"
1880    ///
1881    /// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1882    /// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1883    /// version = "4"
1884    ///
1885    /// [[users]]
1886    ///
1887    /// [users.hermetic_system_nethsm_metrics]
1888    /// system_user = "local-metrics1"
1889    ///
1890    /// [users.hermetic_system_nethsm_metrics.nethsm_users]
1891    /// metrics_user = "metrics2"
1892    /// operator_users = ["operator2metrics1"]
1893    ///
1894    /// [[users]]
1895    /// [users.system_only_share_download]
1896    /// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
1897    /// system_user = "ssh-share-down"
1898    ///
1899    /// [[users]]
1900    /// [users.system_only_share_upload]
1901    /// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
1902    /// system_user = "ssh-share-up"
1903    ///
1904    /// [[users]]
1905    /// [users.system_only_wireguard_download]
1906    /// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"]
1907    /// system_user = "ssh-wireguard-down"
1908    /// "#;
1909    ///     let mut buffer = std::fs::File::create(&config_file)?;
1910    ///     buffer.write_all(config_string.as_bytes())?;
1911    /// }
1912    /// HermeticParallelConfig::new_from_file(
1913    ///     ConfigSettings::new(
1914    ///         "my_app".to_string(),
1915    ///         ConfigInteractivity::NonInteractive,
1916    ///         None,
1917    ///     ),
1918    ///     Some(&config_file),
1919    /// )?;
1920    /// # Ok(())
1921    /// # }
1922    /// ```
1923    pub fn new_from_file(
1924        config_settings: ConfigSettings,
1925        path: Option<&Path>,
1926    ) -> Result<Self, Error> {
1927        let mut config: HermeticParallelConfig = if let Some(path) = path {
1928            confy::load_path(path).map_err(|error| Error::Load {
1929                description: if let Some(error) = error.source() {
1930                    error.to_string()
1931                } else {
1932                    "".to_string()
1933                },
1934                source: error,
1935            })?
1936        } else {
1937            confy::load(
1938                &config_settings.app_name,
1939                Some(config_settings.config_name.0.as_str()),
1940            )
1941            .map_err(|error| Error::Load {
1942                description: if let Some(error) = error.source() {
1943                    error.to_string()
1944                } else {
1945                    "".to_string()
1946                },
1947                source: error,
1948            })?
1949        };
1950        config.settings = config_settings;
1951        config.validate()?;
1952
1953        Ok(config)
1954    }
1955
1956    /// Creates a new [`HermeticParallelConfig`].
1957    ///
1958    /// # Errors
1959    ///
1960    /// Returns an error if the configuration file can not be loaded.
1961    ///
1962    /// # Examples
1963    ///
1964    /// ```
1965    /// use std::collections::HashSet;
1966    ///
1967    /// use nethsm::{Connection, UserRole};
1968    /// use nethsm_config::{
1969    ///     AdministrativeSecretHandling,
1970    ///     AuthorizedKeyEntryList,
1971    ///     ConfigCredentials,
1972    ///     ConfigInteractivity,
1973    ///     ConfigName,
1974    ///     ConfigSettings,
1975    ///     HermeticParallelConfig,
1976    ///     NonAdministrativeSecretHandling,
1977    ///     UserMapping,
1978    /// };
1979    ///
1980    /// # fn main() -> testresult::TestResult {
1981    /// HermeticParallelConfig::new(
1982    ///     ConfigSettings::new(
1983    ///         "my_app".to_string(),
1984    ///         ConfigInteractivity::NonInteractive,
1985    ///         None,
1986    ///     ),
1987    ///     1,
1988    ///     AdministrativeSecretHandling::ShamirsSecretSharing,
1989    ///     NonAdministrativeSecretHandling::SystemdCreds,
1990    ///     HashSet::from([Connection::new(
1991    ///         "https://localhost:8443/api/v1/".parse()?,
1992    ///         "Unsafe".parse()?,
1993    ///     )]),
1994    ///     HashSet::from([
1995    ///         UserMapping::NetHsmOnlyAdmin("admin".parse()?),
1996    ///         UserMapping::SystemOnlyShareDownload {
1997    ///             system_user: "ssh-share-down".parse()?,
1998    ///             ssh_authorized_keys: AuthorizedKeyEntryList::new(vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?])?,
1999    ///         },
2000    ///         UserMapping::SystemOnlyShareUpload {
2001    ///             system_user: "ssh-share-up".parse()?,
2002    ///             ssh_authorized_keys: AuthorizedKeyEntryList::new(vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?])?,
2003    ///         }]),
2004    /// )?;
2005    /// # Ok(())
2006    /// # }
2007    /// ```
2008    pub fn new(
2009        config_settings: ConfigSettings,
2010        iteration: u32,
2011        admin_secret_handling: AdministrativeSecretHandling,
2012        non_admin_secret_handling: NonAdministrativeSecretHandling,
2013        connections: HashSet<Connection>,
2014        users: HashSet<UserMapping>,
2015    ) -> Result<Self, Error> {
2016        let config = Self {
2017            iteration,
2018            admin_secret_handling,
2019            non_admin_secret_handling,
2020            connections,
2021            users,
2022            settings: config_settings,
2023        };
2024        config.validate()?;
2025        Ok(config)
2026    }
2027
2028    /// Writes a [`HermeticParallelConfig`] to file.
2029    ///
2030    /// # Errors
2031    ///
2032    /// Returns an error if the configuration file can not be written.
2033    ///
2034    /// # Examples
2035    ///
2036    /// ```
2037    /// use std::collections::HashSet;
2038    ///
2039    /// use nethsm::{Connection,CryptographicKeyContext, OpenPgpUserIdList, SigningKeySetup, UserRole};
2040    /// use nethsm_config::{
2041    ///     AuthorizedKeyEntryList,
2042    ///     AdministrativeSecretHandling,
2043    ///     ConfigCredentials,
2044    ///     ConfigInteractivity,
2045    ///     ConfigName,
2046    ///     ConfigSettings,
2047    ///     HermeticParallelConfig,
2048    ///     NetHsmMetricsUsers,
2049    ///     NonAdministrativeSecretHandling,
2050    ///     UserMapping,
2051    /// };
2052    ///
2053    /// # fn main() -> testresult::TestResult {
2054    /// let config = HermeticParallelConfig::new(
2055    ///     ConfigSettings::new(
2056    ///         "my_app".to_string(),
2057    ///         ConfigInteractivity::NonInteractive,
2058    ///         None,
2059    ///     ),
2060    ///     1,
2061    ///     AdministrativeSecretHandling::ShamirsSecretSharing,
2062    ///     NonAdministrativeSecretHandling::SystemdCreds,
2063    ///     HashSet::from([Connection::new(
2064    ///         "https://localhost:8443/api/v1/".parse()?,
2065    ///         "Unsafe".parse()?,
2066    ///     )]),
2067    ///     HashSet::from([UserMapping::NetHsmOnlyAdmin("admin".parse()?),
2068    ///         UserMapping::SystemNetHsmBackup {
2069    ///             nethsm_user: "backup1".parse()?,
2070    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2071    ///             system_user: "ssh-backup1".parse()?,
2072    ///         },
2073    ///         UserMapping::SystemNetHsmMetrics {
2074    ///             nethsm_users: NetHsmMetricsUsers::new("metrics1".parse()?, vec!["operator2metrics1".parse()?])?,
2075    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIioJ9uvAxUPunFh89T+ENo7OQerqHE8SQ+2v4VWbfUZ user@host".parse()?,
2076    ///             system_user: "ssh-metrics1".parse()?,
2077    ///         },
2078    ///         UserMapping::SystemNetHsmOperatorSigning {
2079    ///             nethsm_user: "operator1".parse()?,
2080    ///             nethsm_key_setup: SigningKeySetup::new(
2081    ///                 "key1".parse()?,
2082    ///                 "Curve25519".parse()?,
2083    ///                 vec!["EdDsaSignature".parse()?],
2084    ///                 None,
2085    ///                 "EdDsa".parse()?,
2086    ///                 CryptographicKeyContext::OpenPgp{
2087    ///                     user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
2088    ///                     version: "4".parse()?,
2089    ///                 })?,
2090    ///             ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
2091    ///             system_user: "ssh-operator1".parse()?,
2092    ///             tag: "tag1".to_string(),
2093    ///         },
2094    ///         UserMapping::HermeticSystemNetHsmMetrics {
2095    ///             nethsm_users: NetHsmMetricsUsers::new("metrics2".parse()?, vec!["operator1metrics1".parse()?])?,
2096    ///             system_user: "local-metrics1".parse()?,
2097    ///         },
2098    ///         UserMapping::SystemOnlyShareDownload {
2099    ///             system_user: "ssh-share-down".parse()?,
2100    ///             ssh_authorized_keys: AuthorizedKeyEntryList::new(
2101    ///                 vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?],
2102    ///             )?,
2103    ///         },
2104    ///         UserMapping::SystemOnlyShareUpload {
2105    ///             system_user: "ssh-share-up".parse()?,
2106    ///             ssh_authorized_keys: AuthorizedKeyEntryList::new(
2107    ///                 vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?],
2108    ///             )?,
2109    ///         },
2110    ///         UserMapping::SystemOnlyWireGuardDownload {
2111    ///             system_user: "ssh-wireguard-down".parse()?,
2112    ///             ssh_authorized_keys: AuthorizedKeyEntryList::new(
2113    ///                 vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?],
2114    ///             )?,
2115    ///         },
2116    ///     ]),
2117    /// )?;
2118    ///
2119    /// let config_file = testdir::testdir!().join("basic_parallel_config_store.conf");
2120    /// config.store(Some(&config_file))?;
2121    /// # println!("{}", std::fs::read_to_string(&config_file)?);
2122    /// # Ok(())
2123    /// # }
2124    /// ```
2125    pub fn store(&self, path: Option<&Path>) -> Result<(), Error> {
2126        if let Some(path) = path {
2127            confy::store_path(path, self).map_err(Error::Store)
2128        } else {
2129            confy::store(&self.settings.app_name, "config", self).map_err(Error::Store)
2130        }
2131    }
2132
2133    /// Returns an Iterator over the available [`Connection`]s.
2134    pub fn iter_connections(&self) -> impl Iterator<Item = &Connection> {
2135        self.connections.iter()
2136    }
2137
2138    /// Returns an Iterator over the available [`UserMapping`]s.
2139    pub fn iter_user_mappings(&self) -> impl Iterator<Item = &UserMapping> {
2140        self.users.iter()
2141    }
2142
2143    /// Returns the [`AdministrativeSecretHandling`].
2144    pub fn get_administrative_secret_handling(&self) -> AdministrativeSecretHandling {
2145        self.admin_secret_handling
2146    }
2147
2148    /// Returns the [`NonAdministrativeSecretHandling`].
2149    pub fn get_non_administrative_secret_handling(&self) -> NonAdministrativeSecretHandling {
2150        self.non_admin_secret_handling
2151    }
2152
2153    /// Returns an [`ExtendedUserMapping`] for a system user of `name` if it exists.
2154    ///
2155    /// # Errors
2156    ///
2157    /// Returns an error if no [`UserMapping`] with a [`SystemUserId`] matching `name` is found.
2158    pub fn get_extended_mapping_for_user(&self, name: &str) -> Result<ExtendedUserMapping, Error> {
2159        for user_mapping in self.users.iter() {
2160            if user_mapping
2161                .get_system_user()
2162                .is_some_and(|system_user| system_user.as_ref() == name)
2163            {
2164                return Ok(ExtendedUserMapping::new(
2165                    self.admin_secret_handling,
2166                    self.non_admin_secret_handling,
2167                    self.connections.clone(),
2168                    user_mapping.clone(),
2169                ));
2170            }
2171        }
2172        Err(Error::NoMatchingMappingForSystemUser {
2173            name: name.to_string(),
2174        })
2175    }
2176
2177    /// Validates the components of the [`HermeticParallelConfig`].
2178    fn validate(&self) -> Result<(), Error> {
2179        // ensure there are no duplicate system users
2180        {
2181            let mut system_users = HashSet::new();
2182            for system_user_id in self
2183                .users
2184                .iter()
2185                .filter_map(|mapping| mapping.get_system_user())
2186            {
2187                if !system_users.insert(system_user_id.clone()) {
2188                    return Err(Error::DuplicateSystemUserId {
2189                        system_user_id: system_user_id.clone(),
2190                    });
2191                }
2192            }
2193        }
2194
2195        // ensure there are no duplicate NetHsm users
2196        {
2197            let mut nethsm_users = HashSet::new();
2198            for nethsm_user_id in self
2199                .users
2200                .iter()
2201                .flat_map(|mapping| mapping.get_nethsm_users())
2202            {
2203                if !nethsm_users.insert(nethsm_user_id.clone()) {
2204                    return Err(Error::DuplicateNetHsmUserId {
2205                        nethsm_user_id: nethsm_user_id.clone(),
2206                    });
2207                }
2208            }
2209        }
2210
2211        // ensure that there is at least one system-wide administrator
2212        if self
2213            .users
2214            .iter()
2215            .filter_map(|mapping| {
2216                if let UserMapping::NetHsmOnlyAdmin(user_id) = mapping {
2217                    if !user_id.is_namespaced() {
2218                        Some(user_id)
2219                    } else {
2220                        None
2221                    }
2222                } else {
2223                    None
2224                }
2225            })
2226            .next()
2227            .is_none()
2228        {
2229            return Err(Error::MissingAdministrator);
2230        }
2231
2232        // ensure that there is an Administrator in each used namespace
2233        {
2234            // namespaces for all users, that are not in the Administrator role
2235            let namespaces_users = self
2236                .users
2237                .iter()
2238                .filter(|mapping| !matches!(mapping, UserMapping::NetHsmOnlyAdmin(_)))
2239                .flat_map(|mapping| mapping.get_namespaces())
2240                .collect::<HashSet<String>>();
2241            // namespaces for all users, that are in the Administrator role
2242            let namespaces_admins = self
2243                .users
2244                .iter()
2245                .filter(|mapping| matches!(mapping, UserMapping::NetHsmOnlyAdmin(_)))
2246                .flat_map(|mapping| mapping.get_namespaces())
2247                .collect::<HashSet<String>>();
2248
2249            let namespaces: Vec<String> = namespaces_users
2250                .difference(&namespaces_admins)
2251                .cloned()
2252                .collect();
2253            if !namespaces.is_empty() {
2254                return Err(Error::MissingNamespaceAdministrators { namespaces });
2255            }
2256        }
2257
2258        if self.admin_secret_handling == AdministrativeSecretHandling::ShamirsSecretSharing {
2259            // ensure there is at least one system user for downloading shares of a shared
2260            // secret
2261            if !self
2262                .users
2263                .iter()
2264                .any(|mapping| matches!(mapping, UserMapping::SystemOnlyShareDownload { .. }))
2265            {
2266                return Err(Error::MissingShareDownloadUser);
2267            }
2268
2269            // ensure there is at least one system user for uploading shares of a shared secret
2270            if !self
2271                .users
2272                .iter()
2273                .any(|mapping| matches!(mapping, UserMapping::SystemOnlyShareUpload { .. }))
2274            {
2275                return Err(Error::MissingShareUploadUser);
2276            }
2277        } else {
2278            // ensure there is no system user setup for uploading or downloading of shares of a
2279            // shared secret
2280            let share_users: Vec<SystemUserId> = self
2281                .users
2282                .iter()
2283                .filter_map(|mapping| match mapping {
2284                    UserMapping::SystemOnlyShareUpload {
2285                        system_user,
2286                        ssh_authorized_keys: _,
2287                    }
2288                    | UserMapping::SystemOnlyShareDownload {
2289                        system_user,
2290                        ssh_authorized_keys: _,
2291                    } => Some(system_user.clone()),
2292                    _ => None,
2293                })
2294                .collect();
2295            if !share_users.is_empty() {
2296                return Err(Error::NoSssButShareUsers { share_users });
2297            }
2298        }
2299
2300        // ensure there are no duplicate authorized SSH keys in the set of uploading shareholders
2301        // and the rest (minus downloading shareholders)
2302        {
2303            let mut ssh_authorized_keys = HashSet::new();
2304            for ssh_authorized_key in self
2305                .users
2306                .iter()
2307                .filter(|mapping| {
2308                    !matches!(
2309                        mapping,
2310                        UserMapping::SystemOnlyShareDownload {
2311                            system_user: _,
2312                            ssh_authorized_keys: _,
2313                        }
2314                    )
2315                })
2316                .flat_map(|mapping| mapping.get_ssh_authorized_keys())
2317                // we know a valid Entry can be created from AuthorizedKeyEntry, because its
2318                // constructor ensures it, hence we discard Errors
2319                .filter_map(|authorized_key| {
2320                    ssh_key::authorized_keys::Entry::try_from(&authorized_key).ok()
2321                })
2322            {
2323                if !ssh_authorized_keys.insert(ssh_authorized_key.public_key().clone()) {
2324                    return Err(Error::DuplicateSshAuthorizedKey {
2325                        ssh_authorized_key: ssh_authorized_key.public_key().to_string(),
2326                    });
2327                }
2328            }
2329        }
2330
2331        // ensure there are no duplicate authorized SSH keys in the set of downloading shareholders
2332        // and the rest (minus uploading shareholders)
2333        {
2334            let mut ssh_authorized_keys = HashSet::new();
2335            for ssh_authorized_key in self
2336                .users
2337                .iter()
2338                .filter(|mapping| {
2339                    !matches!(
2340                        mapping,
2341                        UserMapping::SystemOnlyShareUpload {
2342                            system_user: _,
2343                            ssh_authorized_keys: _,
2344                        }
2345                    )
2346                })
2347                .flat_map(|mapping| mapping.get_ssh_authorized_keys())
2348                // we know a valid Entry can be created from AuthorizedKeyEntry, because its
2349                // constructor ensures it, hence we discard Errors
2350                .filter_map(|authorized_key| {
2351                    ssh_key::authorized_keys::Entry::try_from(&authorized_key).ok()
2352                })
2353            {
2354                if !ssh_authorized_keys.insert(ssh_authorized_key.public_key().clone()) {
2355                    return Err(Error::DuplicateSshAuthorizedKey {
2356                        ssh_authorized_key: ssh_authorized_key.public_key().to_string(),
2357                    });
2358                }
2359            }
2360        }
2361
2362        // ensure that only one-to-one relationships between users in the Operator role and keys
2363        // exist (system-wide and per-namespace)
2364        {
2365            // ensure that KeyIds are not reused system-wide
2366            let mut set = HashSet::new();
2367            for key_id in self
2368                .users
2369                .iter()
2370                .flat_map(|mapping| mapping.get_key_ids(None))
2371            {
2372                if !set.insert(key_id.clone()) {
2373                    return Err(Error::DuplicateKeyId { key_id });
2374                }
2375            }
2376
2377            // ensure that KeyIds are not reused per namespace
2378            for namespace in self
2379                .users
2380                .iter()
2381                .flat_map(|mapping| mapping.get_namespaces())
2382            {
2383                let mut set = HashSet::new();
2384                for key_id in self
2385                    .users
2386                    .iter()
2387                    .flat_map(|mapping| mapping.get_key_ids(Some(&namespace)))
2388                {
2389                    if !set.insert(key_id.clone()) {
2390                        return Err(Error::DuplicateKeyIdInNamespace { key_id, namespace });
2391                    }
2392                }
2393            }
2394        }
2395
2396        // ensure unique tags system-wide and per namespace
2397        {
2398            // ensure that tags are unique system-wide
2399            let mut set = HashSet::new();
2400            for tag in self.users.iter().flat_map(|mapping| mapping.get_tags(None)) {
2401                if !set.insert(tag) {
2402                    return Err(Error::DuplicateTag {
2403                        tag: tag.to_string(),
2404                    });
2405                }
2406            }
2407
2408            // ensure that tags are unique in each namespace
2409            for namespace in self
2410                .users
2411                .iter()
2412                .flat_map(|mapping| mapping.get_namespaces())
2413            {
2414                let mut set = HashSet::new();
2415                for tag in self
2416                    .users
2417                    .iter()
2418                    .flat_map(|mapping| mapping.get_tags(Some(&namespace)))
2419                {
2420                    if !set.insert(tag) {
2421                        return Err(Error::DuplicateTagInNamespace {
2422                            tag: tag.to_string(),
2423                            namespace,
2424                        });
2425                    }
2426                }
2427            }
2428        }
2429
2430        Ok(())
2431    }
2432}
2433
2434#[cfg(test)]
2435mod tests {
2436    use core::panic;
2437    use std::path::PathBuf;
2438
2439    use rstest::rstest;
2440    use testdir::testdir;
2441    use testresult::TestResult;
2442
2443    use super::*;
2444
2445    #[rstest]
2446    fn create_and_store_empty_config() -> TestResult {
2447        let config_file: PathBuf = testdir!().join("empty_config.toml");
2448        let config = Config::new(
2449            ConfigSettings::new("test".to_string(), ConfigInteractivity::Interactive, None),
2450            Some(&config_file),
2451        )?;
2452        println!("{:#?}", config);
2453        config.store(Some(&config_file))?;
2454        println!("config file:\n{}", std::fs::read_to_string(config_file)?);
2455        Ok(())
2456    }
2457
2458    #[rstest]
2459    fn roundtrip_config(
2460        #[files("basic-config*.toml")]
2461        #[base_dir = "tests/fixtures/roundtrip-config/"]
2462        config_file: PathBuf,
2463    ) -> TestResult {
2464        let output_config_file: PathBuf = testdir!().join(
2465            config_file
2466                .file_name()
2467                .expect("the input config file should have a file name"),
2468        );
2469        let config = Config::new(
2470            ConfigSettings::new("test".to_string(), ConfigInteractivity::Interactive, None),
2471            Some(&config_file),
2472        )?;
2473        config.store(Some(&output_config_file))?;
2474        assert_eq!(
2475            std::fs::read_to_string(&output_config_file)?,
2476            std::fs::read_to_string(&config_file)?
2477        );
2478
2479        Ok(())
2480    }
2481
2482    #[rstest]
2483    fn basic_parallel_config_new_from_file(
2484        #[files("basic-parallel-config-admin-*.toml")]
2485        #[base_dir = "tests/fixtures/working/"]
2486        config_file: PathBuf,
2487    ) -> TestResult {
2488        HermeticParallelConfig::new_from_file(
2489            ConfigSettings::new(
2490                "test".to_string(),
2491                ConfigInteractivity::NonInteractive,
2492                None,
2493            ),
2494            Some(&config_file),
2495        )?;
2496
2497        Ok(())
2498    }
2499
2500    #[rstest]
2501    fn basic_parallel_config_duplicate_system_user(
2502        #[files("basic-parallel-config-admin-*.toml")]
2503        #[base_dir = "tests/fixtures/duplicate-system-user/"]
2504        config_file: PathBuf,
2505    ) -> TestResult {
2506        println!("{config_file:?}");
2507        match HermeticParallelConfig::new_from_file(
2508            ConfigSettings::new(
2509                "test".to_string(),
2510                ConfigInteractivity::NonInteractive,
2511                None,
2512            ),
2513            Some(&config_file),
2514        ) {
2515            Err(Error::DuplicateSystemUserId { .. }) => Ok(()),
2516            Ok(_) => panic!("Did not trigger any Error!"),
2517            Err(error) => panic!("Did not trigger the correct Error: {:?}!", error),
2518        }
2519    }
2520
2521    #[rstest]
2522    fn basic_parallel_config_duplicate_nethsm_user(
2523        #[files("basic-parallel-config-admin-*.toml")]
2524        #[base_dir = "tests/fixtures/duplicate-nethsm-user/"]
2525        config_file: PathBuf,
2526    ) -> TestResult {
2527        if let Err(Error::DuplicateNetHsmUserId { .. }) = HermeticParallelConfig::new_from_file(
2528            ConfigSettings::new(
2529                "test".to_string(),
2530                ConfigInteractivity::NonInteractive,
2531                None,
2532            ),
2533            Some(&config_file),
2534        ) {
2535            Ok(())
2536        } else {
2537            panic!("Did not trigger the correct Error!")
2538        }
2539    }
2540
2541    #[rstest]
2542    fn basic_parallel_config_missing_administrator(
2543        #[files("basic-parallel-config-admin-*.toml")]
2544        #[base_dir = "tests/fixtures/missing-administrator/"]
2545        config_file: PathBuf,
2546    ) -> TestResult {
2547        if let Err(Error::MissingAdministrator) = HermeticParallelConfig::new_from_file(
2548            ConfigSettings::new(
2549                "test".to_string(),
2550                ConfigInteractivity::NonInteractive,
2551                None,
2552            ),
2553            Some(&config_file),
2554        ) {
2555            Ok(())
2556        } else {
2557            panic!("Did not trigger the correct Error!")
2558        }
2559    }
2560
2561    #[rstest]
2562    fn basic_parallel_config_missing_namespace_administrators(
2563        #[files("basic-parallel-config-admin-*.toml")]
2564        #[base_dir = "tests/fixtures/missing-namespace-administrator/"]
2565        config_file: PathBuf,
2566    ) -> TestResult {
2567        if let Err(Error::MissingNamespaceAdministrators { .. }) =
2568            HermeticParallelConfig::new_from_file(
2569                ConfigSettings::new(
2570                    "test".to_string(),
2571                    ConfigInteractivity::NonInteractive,
2572                    None,
2573                ),
2574                Some(&config_file),
2575            )
2576        {
2577            Ok(())
2578        } else {
2579            panic!("Did not trigger the correct Error!")
2580        }
2581    }
2582
2583    #[rstest]
2584    fn basic_parallel_config_duplicate_authorized_keys_share_uploader(
2585        #[files("basic-parallel-config-admin-*.toml")]
2586        #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-uploader/"]
2587        config_file: PathBuf,
2588    ) -> TestResult {
2589        println!("Using configuration {:?}", config_file);
2590        let config_file_string = config_file
2591            .clone()
2592            .into_os_string()
2593            .into_string()
2594            .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2595        // when using plaintext or systemd-creds for administrative credentials, there are no share
2596        // uploaders
2597        if config_file_string.ends_with("admin-plaintext.toml")
2598            || config_file_string.ends_with("admin-systemd-creds.toml")
2599        {
2600            let _config = HermeticParallelConfig::new_from_file(
2601                ConfigSettings::new(
2602                    "test".to_string(),
2603                    ConfigInteractivity::NonInteractive,
2604                    None,
2605                ),
2606                Some(&config_file),
2607            )?;
2608            Ok(())
2609        } else if let Err(Error::DuplicateSshAuthorizedKey { .. }) =
2610            HermeticParallelConfig::new_from_file(
2611                ConfigSettings::new(
2612                    "test".to_string(),
2613                    ConfigInteractivity::NonInteractive,
2614                    None,
2615                ),
2616                Some(&config_file),
2617            )
2618        {
2619            Ok(())
2620        } else {
2621            panic!("Did not trigger the correct Error!")
2622        }
2623    }
2624
2625    #[rstest]
2626    fn basic_parallel_config_duplicate_authorized_keys_share_downloader(
2627        #[files("basic-parallel-config-admin-*.toml")]
2628        #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-downloader/"]
2629        config_file: PathBuf,
2630    ) -> TestResult {
2631        println!("Using configuration {:?}", config_file);
2632        let config_file_string = config_file
2633            .clone()
2634            .into_os_string()
2635            .into_string()
2636            .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2637        // when using plaintext or systemd-creds for administrative credentials, there are no share
2638        // downloaders
2639        if config_file_string.ends_with("admin-plaintext.toml")
2640            || config_file_string.ends_with("admin-systemd-creds.toml")
2641        {
2642            let _config = HermeticParallelConfig::new_from_file(
2643                ConfigSettings::new(
2644                    "test".to_string(),
2645                    ConfigInteractivity::NonInteractive,
2646                    None,
2647                ),
2648                Some(&config_file),
2649            )?;
2650            Ok(())
2651        } else if let Err(Error::DuplicateSshAuthorizedKey { .. }) =
2652            HermeticParallelConfig::new_from_file(
2653                ConfigSettings::new(
2654                    "test".to_string(),
2655                    ConfigInteractivity::NonInteractive,
2656                    None,
2657                ),
2658                Some(&config_file),
2659            )
2660        {
2661            Ok(())
2662        } else {
2663            panic!("Did not trigger the correct Error!")
2664        }
2665    }
2666
2667    #[rstest]
2668    fn basic_parallel_config_duplicate_authorized_keys_users(
2669        #[files("basic-parallel-config-admin-*.toml")]
2670        #[base_dir = "tests/fixtures/duplicate-authorized-keys-users/"]
2671        config_file: PathBuf,
2672    ) -> TestResult {
2673        if let Err(Error::DuplicateSshAuthorizedKey { .. }) = HermeticParallelConfig::new_from_file(
2674            ConfigSettings::new(
2675                "test".to_string(),
2676                ConfigInteractivity::NonInteractive,
2677                None,
2678            ),
2679            Some(&config_file),
2680        ) {
2681            Ok(())
2682        } else {
2683            panic!("Did not trigger the correct Error!")
2684        }
2685    }
2686
2687    #[rstest]
2688    fn basic_parallel_config_missing_share_download_user(
2689        #[files("basic-parallel-config-admin-*.toml")]
2690        #[base_dir = "tests/fixtures/missing-share-download-user/"]
2691        config_file: PathBuf,
2692    ) -> TestResult {
2693        println!("Using configuration {:?}", config_file);
2694        let config_file_string = config_file
2695            .clone()
2696            .into_os_string()
2697            .into_string()
2698            .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2699        // when using plaintext or systemd-creds for administrative credentials, there are no share
2700        // downloaders
2701        if config_file_string.ends_with("admin-plaintext.toml")
2702            || config_file_string.ends_with("admin-systemd-creds.toml")
2703        {
2704            let _config = HermeticParallelConfig::new_from_file(
2705                ConfigSettings::new(
2706                    "test".to_string(),
2707                    ConfigInteractivity::NonInteractive,
2708                    None,
2709                ),
2710                Some(&config_file),
2711            )?;
2712            Ok(())
2713        } else if let Err(Error::MissingShareDownloadUser) = HermeticParallelConfig::new_from_file(
2714            ConfigSettings::new(
2715                "test".to_string(),
2716                ConfigInteractivity::NonInteractive,
2717                None,
2718            ),
2719            Some(&config_file),
2720        ) {
2721            Ok(())
2722        } else {
2723            panic!("Did not trigger the correct Error!")
2724        }
2725    }
2726
2727    #[rstest]
2728    fn basic_parallel_config_missing_share_upload_user(
2729        #[files("basic-parallel-config-admin-*.toml")]
2730        #[base_dir = "tests/fixtures/missing-share-upload-user/"]
2731        config_file: PathBuf,
2732    ) -> TestResult {
2733        println!("Using configuration {:?}", config_file);
2734        let config_file_string = config_file
2735            .clone()
2736            .into_os_string()
2737            .into_string()
2738            .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2739        // when using plaintext or systemd-creds for administrative credentials, there are no share
2740        // downloaders
2741        if config_file_string.ends_with("admin-plaintext.toml")
2742            || config_file_string.ends_with("admin-systemd-creds.toml")
2743        {
2744            let _config = HermeticParallelConfig::new_from_file(
2745                ConfigSettings::new(
2746                    "test".to_string(),
2747                    ConfigInteractivity::NonInteractive,
2748                    None,
2749                ),
2750                Some(&config_file),
2751            )?;
2752            Ok(())
2753        } else if let Err(Error::MissingShareUploadUser) = HermeticParallelConfig::new_from_file(
2754            ConfigSettings::new(
2755                "test".to_string(),
2756                ConfigInteractivity::NonInteractive,
2757                None,
2758            ),
2759            Some(&config_file),
2760        ) {
2761            Ok(())
2762        } else {
2763            panic!("Did not trigger the correct Error!")
2764        }
2765    }
2766
2767    #[rstest]
2768    fn basic_parallel_config_no_sss_but_shares(
2769        #[files("basic-parallel-config-admin-*.toml")]
2770        #[base_dir = "tests/fixtures/no-sss-but-shares/"]
2771        config_file: PathBuf,
2772    ) -> TestResult {
2773        println!("Using configuration {:?}", config_file);
2774        let config_file_string = config_file
2775            .clone()
2776            .into_os_string()
2777            .into_string()
2778            .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2779        // when using shamir's secret sharing for administrative credentials, there ought to be
2780        // share downloaders and uploaders
2781        if config_file_string.ends_with("admin-shamirs-secret-sharing.toml") {
2782            let _config = HermeticParallelConfig::new_from_file(
2783                ConfigSettings::new(
2784                    "test".to_string(),
2785                    ConfigInteractivity::NonInteractive,
2786                    None,
2787                ),
2788                Some(&config_file),
2789            )?;
2790            Ok(())
2791        } else if let Err(Error::NoSssButShareUsers { .. }) = HermeticParallelConfig::new_from_file(
2792            ConfigSettings::new(
2793                "test".to_string(),
2794                ConfigInteractivity::NonInteractive,
2795                None,
2796            ),
2797            Some(&config_file),
2798        ) {
2799            Ok(())
2800        } else {
2801            panic!("Did not trigger the correct Error!")
2802        }
2803    }
2804
2805    #[rstest]
2806    fn basic_parallel_config_duplicate_key_id(
2807        #[files("basic-parallel-config-admin-*.toml")]
2808        #[base_dir = "tests/fixtures/duplicate-key-id/"]
2809        config_file: PathBuf,
2810    ) -> TestResult {
2811        if let Err(Error::DuplicateKeyId { .. }) = HermeticParallelConfig::new_from_file(
2812            ConfigSettings::new(
2813                "test".to_string(),
2814                ConfigInteractivity::NonInteractive,
2815                None,
2816            ),
2817            Some(&config_file),
2818        ) {
2819            Ok(())
2820        } else {
2821            panic!("Did not trigger the correct Error!")
2822        }
2823    }
2824
2825    #[rstest]
2826    fn basic_parallel_config_duplicate_key_id_in_namespace(
2827        #[files("basic-parallel-config-admin-*.toml")]
2828        #[base_dir = "tests/fixtures/duplicate-key-id-in-namespace/"]
2829        config_file: PathBuf,
2830    ) -> TestResult {
2831        if let Err(Error::DuplicateKeyIdInNamespace { .. }) = HermeticParallelConfig::new_from_file(
2832            ConfigSettings::new(
2833                "test".to_string(),
2834                ConfigInteractivity::NonInteractive,
2835                None,
2836            ),
2837            Some(&config_file),
2838        ) {
2839            Ok(())
2840        } else {
2841            panic!("Did not trigger the correct Error!")
2842        }
2843    }
2844
2845    #[rstest]
2846    fn basic_parallel_config_duplicate_tag(
2847        #[files("basic-parallel-config-admin-*.toml")]
2848        #[base_dir = "tests/fixtures/duplicate-tag/"]
2849        config_file: PathBuf,
2850    ) -> TestResult {
2851        if let Err(Error::DuplicateTag { .. }) = HermeticParallelConfig::new_from_file(
2852            ConfigSettings::new(
2853                "test".to_string(),
2854                ConfigInteractivity::NonInteractive,
2855                None,
2856            ),
2857            Some(&config_file),
2858        ) {
2859            Ok(())
2860        } else {
2861            panic!("Did not trigger the correct Error!")
2862        }
2863    }
2864
2865    #[rstest]
2866    fn basic_parallel_config_duplicate_tag_in_namespace(
2867        #[files("basic-parallel-config-admin-*.toml")]
2868        #[base_dir = "tests/fixtures/duplicate-tag-in-namespace/"]
2869        config_file: PathBuf,
2870    ) -> TestResult {
2871        if let Err(Error::DuplicateTagInNamespace { .. }) = HermeticParallelConfig::new_from_file(
2872            ConfigSettings::new(
2873                "test".to_string(),
2874                ConfigInteractivity::NonInteractive,
2875                None,
2876            ),
2877            Some(&config_file),
2878        ) {
2879            Ok(())
2880        } else {
2881            panic!("Did not trigger the correct Error!")
2882        }
2883    }
2884}