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