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    NetHsm,
15    Passphrase,
16    Url,
17    UserId,
18    UserRole,
19};
20use serde::{Deserialize, Serialize};
21
22use crate::{ConfigCredentials, PassphrasePrompt, UserPrompt};
23
24/// Errors related to configuration
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27    /// Issue getting the config file location
28    #[error("Config file issue: {0}")]
29    ConfigFileLocation(#[source] confy::ConfyError),
30
31    /// A config loading error
32    ///
33    /// The variant tracks a [`ConfyError`][`confy::ConfyError`] and an optional
34    /// description of an inner Error type.
35    /// The description is tracked separately, as otherwise we do not get to useful error messages
36    /// of wrapped Error types (e.g. those for loading TOML files).
37    #[error("Config loading issue: {source}\n{description}")]
38    Load {
39        source: confy::ConfyError,
40        description: String,
41    },
42
43    /// A config storing error
44    #[error("Config storing issue: {0}")]
45    Store(#[source] confy::ConfyError),
46
47    /// Credentials exist already
48    #[error("Credentials exist already: {0}")]
49    CredentialsExist(UserId),
50
51    /// Credentials do not exist
52    #[error("Credentials do not exist: {0}")]
53    CredentialsMissing(UserId),
54
55    /// None of the provided users map to one of the provided roles
56    #[error("None of the provided users ({names:?}) map to one of the provided roles ({roles:?})")]
57    MatchingCredentialsMissing {
58        names: Vec<UserId>,
59        roles: Vec<UserRole>,
60    },
61
62    /// Credentials do not exist
63    #[error("No user matching one of the requested roles ({0:?}) exists")]
64    NoMatchingCredentials(Vec<UserRole>),
65
66    /// Device exists already
67    #[error("Device exist already: {0}")]
68    DeviceExists(String),
69
70    /// Device does not exist
71    #[error("Device does not exist: {0}")]
72    DeviceMissing(String),
73
74    /// There is more than one device (but none has been specified)
75    #[error("There is more than one device")]
76    MoreThanOneDevice,
77
78    /// There is no device
79    #[error("There is no device")]
80    NoDevice,
81
82    /// The configuration can not be used interactively
83    #[error("The configuration can not be used interactively")]
84    NonInteractive,
85
86    /// NetHsm connection initialization error
87    #[error("NetHsm connection can not be created: {0}")]
88    NetHsm(#[from] nethsm::Error),
89
90    /// A prompt requesting user data failed
91    #[error("A prompt issue")]
92    Prompt(#[from] crate::prompt::Error),
93}
94
95/// The interactivity of a configuration
96///
97/// This enum is used by [`Config`] and [`DeviceConfig`] to define whether missing items are
98/// prompted for interactively ([`ConfigInteractivity::Interactive`]) or not
99/// ([`ConfigInteractivity::NonInteractive`]).
100#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
101pub enum ConfigInteractivity {
102    /// The configuration may spawn interactive prompts to request more data (e.g. usernames or
103    /// passphrases)
104    Interactive,
105    /// The configuration will return an [`Error`] if interactive prompts need to be spawned to
106    /// request more data (e.g. usernames or passphrases)
107    #[default]
108    NonInteractive,
109}
110
111/// The name of a configuration
112///
113/// The name defines the file name (without file suffix) used by a [`Config`] object.
114/// It defaults to `"config"`, but may be set specifically when initializing a [`Config`].
115#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
116pub struct ConfigName(String);
117
118impl Default for ConfigName {
119    fn default() -> Self {
120        Self("config".to_string())
121    }
122}
123
124impl Display for ConfigName {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        self.0.fmt(f)
127    }
128}
129
130impl FromStr for ConfigName {
131    type Err = Error;
132
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        Ok(Self(s.to_string()))
135    }
136}
137
138/// The settings for a [`Config`]
139///
140/// Settings contain the [`ConfigName`] by which the configuration file is loaded and stored, the
141/// application name which uses the configuration (and also influences the file path of the
142/// configuration) and the interactivity setting, which defines whether missing items are prompted
143/// for interactively or not.
144#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
145pub struct ConfigSettings {
146    /// The configuration name (file name without suffix)
147    config_name: ConfigName,
148    /// The name of the application using a [`Config`]
149    app_name: String,
150    /// The interactivity setting for the [`Config`] (and any [`DeviceConfig`] used by it)
151    interactivity: ConfigInteractivity,
152}
153
154impl ConfigSettings {
155    /// Creates a new [`ConfigSettings`]
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use nethsm_config::{ConfigInteractivity, ConfigSettings};
161    ///
162    /// # fn main() -> testresult::TestResult {
163    /// // settings for an application called "my_app", that uses a custom configuration file named "my_app-config" interactively
164    /// let config_settings = ConfigSettings::new(
165    ///     "my_app".to_string(),
166    ///     ConfigInteractivity::Interactive,
167    ///     Some("my_app-config".parse()?),
168    /// );
169    ///
170    /// // settings for an application called "my_app", that uses a default config file non-interactively
171    /// let config_settings = ConfigSettings::new(
172    ///     "my_app".to_string(),
173    ///     ConfigInteractivity::NonInteractive,
174    ///     None,
175    /// );
176    /// # Ok(())
177    /// # }
178    /// ```
179    pub fn new(
180        app_name: String,
181        interactivity: ConfigInteractivity,
182        config_name: Option<ConfigName>,
183    ) -> Self {
184        Self {
185            app_name,
186            interactivity,
187            config_name: config_name.unwrap_or_default(),
188        }
189    }
190
191    /// Returns the configuration name
192    pub fn config_name(&self) -> ConfigName {
193        self.config_name.to_owned()
194    }
195
196    /// Returns the application name
197    pub fn app_name(&self) -> String {
198        self.app_name.clone()
199    }
200
201    /// Returns the interactivity setting
202    pub fn interactivity(&self) -> ConfigInteractivity {
203        self.interactivity
204    }
205}
206
207/// The configuration for a [`NetHsm`]
208///
209/// Tracks the [`Connection`] for a [`NetHsm`] as well as a set of [`ConfigCredentials`].
210#[derive(Clone, Debug, Deserialize, Serialize)]
211pub struct DeviceConfig {
212    connection: RefCell<Connection>,
213    credentials: RefCell<HashSet<ConfigCredentials>>,
214    #[serde(skip)]
215    interactivity: ConfigInteractivity,
216}
217
218impl DeviceConfig {
219    /// Creates a new [`DeviceConfig`]
220    ///
221    /// Creates a new [`DeviceConfig`] by providing a `connection`, an optional set of `credentials`
222    /// and the `interactivity` setting.
223    ///
224    /// # Errors
225    ///
226    /// Returns an [`Error::CredentialsExist`] if `credentials` contains duplicates.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
232    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
233    ///
234    /// # fn main() -> testresult::TestResult {
235    /// let connection = Connection::new(
236    ///     "https://example.org/api/v1".parse()?,
237    ///     ConnectionSecurity::Unsafe,
238    /// );
239    ///
240    /// DeviceConfig::new(
241    ///     connection.clone(),
242    ///     vec![],
243    ///     ConfigInteractivity::NonInteractive,
244    /// )?;
245    ///
246    /// DeviceConfig::new(
247    ///     connection.clone(),
248    ///     vec![ConfigCredentials::new(
249    ///         UserRole::Operator,
250    ///         "user1".parse()?,
251    ///         Some("my-passphrase".to_string()),
252    ///     )],
253    ///     ConfigInteractivity::NonInteractive,
254    /// )?;
255    ///
256    /// // this fails because the provided credentials contain duplicates
257    /// assert!(
258    ///     DeviceConfig::new(
259    ///         connection.clone(),
260    ///         vec![
261    ///             ConfigCredentials::new(
262    ///                 UserRole::Operator,
263    ///                 "user1".parse()?,
264    ///                 Some("my-passphrase".to_string()),
265    ///             ),
266    ///             ConfigCredentials::new(
267    ///                 UserRole::Operator,
268    ///                 "user1".parse()?,
269    ///                 Some("my-passphrase".to_string()),
270    ///             ),
271    ///         ],
272    ///         ConfigInteractivity::NonInteractive,
273    ///     )
274    ///     .is_err()
275    /// );
276    /// # Ok(())
277    /// # }
278    /// ```
279    pub fn new(
280        connection: Connection,
281        credentials: Vec<ConfigCredentials>,
282        interactivity: ConfigInteractivity,
283    ) -> Result<DeviceConfig, Error> {
284        let device_config = DeviceConfig {
285            connection: RefCell::new(connection),
286            credentials: RefCell::new(HashSet::new()),
287            interactivity,
288        };
289
290        if !credentials.is_empty() {
291            for creds in credentials.into_iter() {
292                device_config.add_credentials(creds)?
293            }
294        }
295
296        Ok(device_config)
297    }
298
299    /// Sets the interactivity setting
300    ///
301    /// **NOTE**: This method is not necessarily useful by itself, as one usually wants to use the
302    /// same [`ConfigInteractivity`] as that of a [`Config`], which holds the [`DeviceConfig`].
303    pub fn set_config_interactivity(&mut self, config_type: ConfigInteractivity) {
304        self.interactivity = config_type;
305    }
306
307    /// Adds credentials to the device
308    ///
309    /// Adds new [`ConfigCredentials`] to the [`DeviceConfig`].
310    ///
311    /// # Errors
312    ///
313    /// Returns an [`Error::CredentialsExist`] if the `credentials` exist already.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
319    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
320    ///
321    /// # fn main() -> testresult::TestResult {
322    /// let connection = Connection::new(
323    ///     "https://example.org/api/v1".parse()?,
324    ///     ConnectionSecurity::Unsafe,
325    /// );
326    ///
327    /// let device_config = DeviceConfig::new(
328    ///     connection.clone(),
329    ///     vec![],
330    ///     ConfigInteractivity::NonInteractive,
331    /// )?;
332    ///
333    /// device_config.add_credentials(ConfigCredentials::new(
334    ///     UserRole::Operator,
335    ///     "user1".parse()?,
336    ///     Some("my-passphrase".to_string()),
337    /// ))?;
338    ///
339    /// // this fails because the credentials exist already
340    /// assert!(
341    ///     device_config
342    ///         .add_credentials(ConfigCredentials::new(
343    ///             UserRole::Operator,
344    ///             "user1".parse()?,
345    ///             Some("my-passphrase".to_string()),
346    ///         ))
347    ///         .is_err()
348    /// );
349    /// # Ok(())
350    /// # }
351    /// ```
352    pub fn add_credentials(&self, credentials: ConfigCredentials) -> Result<(), Error> {
353        if !self
354            .credentials
355            .borrow()
356            .iter()
357            .any(|creds| creds.get_name() == credentials.get_name())
358        {
359            self.credentials.borrow_mut().insert(credentials);
360            Ok(())
361        } else {
362            Err(Error::CredentialsExist(credentials.get_name()))
363        }
364    }
365
366    /// Returns credentials by name
367    ///
368    /// Returns existing [`ConfigCredentials`] from the [`DeviceConfig`].
369    ///
370    /// # Errors
371    ///
372    /// Returns an [`Error::CredentialsMissing`] if no [`ConfigCredentials`] match the provided
373    /// `name`.
374    ///
375    /// # Examples
376    ///
377    /// ```
378    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
379    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
380    /// # fn main() -> testresult::TestResult {
381    /// let connection = Connection::new(
382    ///     "https://example.org/api/v1".parse()?,
383    ///     ConnectionSecurity::Unsafe,
384    /// );
385    ///
386    /// let device_config = DeviceConfig::new(
387    ///     connection.clone(),
388    ///     vec![],
389    ///     ConfigInteractivity::NonInteractive,
390    /// )?;
391    ///
392    /// // this fails because the credentials do not exist
393    /// assert!(device_config.get_credentials(&"user1".parse()?).is_err());
394    ///
395    /// device_config.add_credentials(ConfigCredentials::new(
396    ///     UserRole::Operator,
397    ///     "user1".parse()?,
398    ///     Some("my-passphrase".to_string()),
399    /// ))?;
400    ///
401    /// device_config.get_credentials(&"user1".parse()?)?;
402    /// # Ok(())
403    /// # }
404    /// ```
405    pub fn get_credentials(&self, name: &UserId) -> Result<ConfigCredentials, Error> {
406        if let Some(creds) = self
407            .credentials
408            .borrow()
409            .iter()
410            .find(|creds| &creds.get_name() == name)
411        {
412            Ok(creds.clone())
413        } else {
414            Err(Error::CredentialsMissing(name.to_owned()))
415        }
416    }
417
418    /// Deletes credentials by name
419    ///
420    /// Deletes [`ConfigCredentials`] identified by `name`.
421    ///
422    /// # Errors
423    ///
424    /// Returns an [`Error::CredentialsMissing`] if no [`ConfigCredentials`] match the provided
425    /// name.
426    ///
427    /// # Examples
428    ///
429    /// ```
430    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
431    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
432    ///
433    /// # fn main() -> testresult::TestResult {
434    /// let device_config = DeviceConfig::new(
435    ///     Connection::new(
436    ///         "https://example.org/api/v1".parse()?,
437    ///         ConnectionSecurity::Unsafe,
438    ///     ),
439    ///     vec![],
440    ///     ConfigInteractivity::NonInteractive,
441    /// )?;
442    /// device_config.add_credentials(ConfigCredentials::new(
443    ///     UserRole::Operator,
444    ///     "user1".parse()?,
445    ///     Some("my-passphrase".to_string()),
446    /// ))?;
447    ///
448    /// device_config.delete_credentials(&"user1".parse()?)?;
449    ///
450    /// // this fails because the credentials do not exist
451    /// assert!(device_config.delete_credentials(&"user1".parse()?).is_err());
452    /// # Ok(())
453    /// # }
454    /// ```
455    pub fn delete_credentials(&self, name: &UserId) -> Result<(), Error> {
456        let before = self.credentials.borrow().len();
457        self.credentials
458            .borrow_mut()
459            .retain(|creds| &creds.get_name() != name);
460        let after = self.credentials.borrow().len();
461        if before == after {
462            Err(Error::CredentialsMissing(name.to_owned()))
463        } else {
464            Ok(())
465        }
466    }
467
468    /// Returns credentials machting one or several roles and a optionally a name
469    ///
470    /// Returns [`ConfigCredentials`] matching a list of [`UserRole`]s and/or a list of [`UserId`]s.
471    ///
472    /// If `names` is empty, the [`ConfigCredentials`] first found matching one of the [`UserRole`]s
473    /// provided using `roles` are returned.
474    /// If `names` contains at least one entry, the first [`ConfigCredentials`] with a matching
475    /// [`UserId`] that have at least one matching [`UserRole`] are returned.
476    ///
477    /// # Errors
478    ///
479    /// Returns an [`Error::NoMatchingCredentials`] if `names` is empty and no existing credentials
480    /// match any of the provided `roles`.
481    /// Returns an [`Error::CredentialsMissing`] if a [`UserId`] in `names` does not exist and no
482    /// [`ConfigCredentials`] have been returned yet.
483    /// Returns an [`Error::MatchingCredentialsMissing`] if no [`ConfigCredentials`] matching either
484    /// the provided `names` or `roles` can be found.
485    ///
486    /// # Examples
487    ///
488    /// ```
489    /// use nethsm::{Connection, ConnectionSecurity, UserRole};
490    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
491    ///
492    /// # fn main() -> testresult::TestResult {
493    /// let device_config = DeviceConfig::new(
494    ///     Connection::new(
495    ///         "https://example.org/api/v1".parse()?,
496    ///         ConnectionSecurity::Unsafe,
497    ///     ),
498    ///     vec![ConfigCredentials::new(
499    ///         UserRole::Administrator,
500    ///         "admin1".parse()?,
501    ///         Some("my-passphrase".to_string()),
502    ///     )],
503    ///     ConfigInteractivity::NonInteractive,
504    /// )?;
505    /// device_config.add_credentials(ConfigCredentials::new(
506    ///     UserRole::Operator,
507    ///     "user1".parse()?,
508    ///     Some("my-passphrase".to_string()),
509    /// ))?;
510    ///
511    /// device_config.get_matching_credentials(&[UserRole::Operator], &["user1".parse()?])?;
512    /// device_config.get_matching_credentials(&[UserRole::Administrator], &["admin1".parse()?])?;
513    /// assert_eq!(
514    ///     device_config
515    ///         .get_matching_credentials(&[UserRole::Operator], &[])?
516    ///         .get_name(),
517    ///     "user1".parse()?
518    /// );
519    /// assert_eq!(
520    ///     device_config
521    ///         .get_matching_credentials(&[UserRole::Administrator], &[])?
522    ///         .get_name(),
523    ///     "admin1".parse()?
524    /// );
525    ///
526    /// // this fails because we must provide a role to match against
527    /// assert!(
528    ///     device_config
529    ///         .get_matching_credentials(&[], &["user1".parse()?])
530    ///         .is_err()
531    /// );
532    ///
533    /// // this fails because no user in the requested role exists
534    /// assert!(
535    ///     device_config
536    ///         .get_matching_credentials(&[UserRole::Metrics], &[])
537    ///         .is_err()
538    /// );
539    ///
540    /// // this fails because no user with the name first provided exists
541    /// assert!(
542    ///     device_config
543    ///         .get_matching_credentials(&[UserRole::Operator], &["user2".parse()?, "user1".parse()?])
544    ///         .is_err()
545    /// );
546    ///
547    /// // this fails because no user in the requested role with any of the provided names exists
548    /// assert!(
549    ///     device_config
550    ///         .get_matching_credentials(&[UserRole::Metrics], &["admin1".parse()?, "user1".parse()?])
551    ///         .is_err()
552    /// );
553    /// # Ok(())
554    /// # }
555    /// ```
556    pub fn get_matching_credentials(
557        &self,
558        roles: &[UserRole],
559        names: &[UserId],
560    ) -> Result<ConfigCredentials, Error> {
561        if names.is_empty() {
562            let creds = self
563                .credentials
564                .borrow()
565                .iter()
566                .filter_map(|creds| {
567                    if roles.contains(&creds.get_role()) {
568                        Some(creds.clone())
569                    } else {
570                        None
571                    }
572                })
573                .collect::<Vec<ConfigCredentials>>();
574            return creds
575                .first()
576                .ok_or_else(|| Error::NoMatchingCredentials(roles.to_vec()))
577                .cloned();
578        }
579
580        for name in names {
581            if let Ok(creds) = &self.get_credentials(name) {
582                if roles.contains(&creds.get_role()) {
583                    return Ok(creds.clone());
584                }
585            } else {
586                return Err(Error::CredentialsMissing(name.to_owned()));
587            }
588        }
589
590        Err(Error::MatchingCredentialsMissing {
591            names: names.to_vec(),
592            roles: roles.to_vec(),
593        })
594    }
595
596    /// Returns a [`NetHsm`] based on the [`DeviceConfig`] (optionally with one set of credentials)
597    ///
598    /// Creates a [`NetHsm`] based on the [`DeviceConfig`].
599    /// Only if `roles` is not empty, one set of [`ConfigCredentials`] based on `roles`,
600    /// `names` and `passphrases` is added to the [`NetHsm`].
601    ///
602    /// **WARNING**: Depending on the [`ConfigInteractivity`] chosen when initializing the
603    /// [`DeviceConfig`] this method behaves differently with regards to adding credentials!
604    ///
605    /// # [`NonInteractive`][`ConfigInteractivity::NonInteractive`]
606    ///
607    /// If `roles` is not empty, optionally adds one set of [`ConfigCredentials`] found by
608    /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] to the returned
609    /// [`NetHsm`], based on `roles` and `names`.
610    /// If the found [`ConfigCredentials`] do not contain a passphrase, a [`Passphrase`] in
611    /// `pasphrases` with the same index as that of the [`UserId`] in `names` is used.
612    ///
613    /// # [`Interactive`][`ConfigInteractivity::Interactive`]
614    ///
615    /// If `roles` is not empty, optionally attempts to add one set of [`ConfigCredentials`] with
616    /// the help of [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] to the
617    /// returned [`NetHsm`], based on `roles` and `names`.
618    /// If no [`ConfigCredentials`] are found by
619    /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`], users are
620    /// interactively prompted for providing a user name.
621    /// If the found or prompted for [`UserId`] [`ConfigCredentials`] do not contain a passphrase, a
622    /// [`Passphrase`] in `pasphrases` with the same index as that of the [`UserId`] in `names`
623    /// is used. If [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`], or
624    /// those the user has been prompted for provides [`ConfigCredentials`] without a
625    /// passphrase, a [`Passphrase`] in `pasphrases` with the same index as that of the
626    /// [`UserId`] in `names` is used. If none is provided (at the right location) in `passphrases`,
627    /// the user is prompted for a passphrase interactively.
628    ///
629    /// # Errors
630    ///
631    /// Returns an [`Error::NoMatchingCredentials`], [`Error::CredentialsMissing`], or
632    /// [`Error::MatchingCredentialsMissing`] if the [`DeviceConfig`] is initialized with
633    /// [`Interactive`][`ConfigInteractivity::Interactive`] and
634    /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] is unable to return
635    /// [`ConfigCredentials`] based on `roles` and `names`.
636    ///
637    /// Returns an [`Error::NonInteractive`] if the [`DeviceConfig`] is initialized with
638    /// [`NonInteractive`][`ConfigInteractivity::NonInteractive`], but additional data would be
639    /// requested interactively.
640    ///
641    /// Returns an [`Error::Prompt`] if requesting additional data interactively leads to error.
642    ///
643    /// # Examples
644    ///
645    /// ```
646    /// use nethsm::{Connection, ConnectionSecurity, Passphrase, UserRole};
647    /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
648    ///
649    /// # fn main() -> testresult::TestResult {
650    /// let device_config = DeviceConfig::new(
651    ///     Connection::new(
652    ///         "https://example.org/api/v1".parse()?,
653    ///         ConnectionSecurity::Unsafe,
654    ///     ),
655    ///     vec![ConfigCredentials::new(
656    ///         UserRole::Administrator,
657    ///         "admin1".parse()?,
658    ///         Some("my-passphrase".to_string()),
659    ///     )],
660    ///     ConfigInteractivity::NonInteractive,
661    /// )?;
662    /// device_config.add_credentials(ConfigCredentials::new(
663    ///     UserRole::Operator,
664    ///     "user1".parse()?,
665    ///     None,
666    /// ))?;
667    ///
668    /// // NetHsm with Operator credentials
669    /// // this works non-interactively, although the credentials in the config provide no passphrase, because we provide the passphrase manually
670    /// device_config.nethsm_with_matching_creds(
671    ///     &[UserRole::Operator],
672    ///     &["user1".parse()?],
673    ///     &[Passphrase::new("my-passphrase".to_string())],
674    /// )?;
675    ///
676    /// // NetHsm with Administrator credentials
677    /// // this automatically selects "admin1" as it is the only user in the Administrator role
678    /// // this works non-interactively, because the credentials in the config provide a passphrase!
679    /// device_config.nethsm_with_matching_creds(
680    ///     &[UserRole::Administrator],
681    ///     &[],
682    ///     &[],
683    /// )?;
684    ///
685    /// // a NetHsm without any credentials
686    /// device_config.nethsm_with_matching_creds(
687    ///     &[],
688    ///     &[],
689    ///     &[],
690    /// )?;
691    ///
692    /// // this fails because the config is non-interactive, the targeted credentials do not offer a passphrase and we also provide none
693    /// assert!(device_config.nethsm_with_matching_creds(
694    ///     &[UserRole::Operator],
695    ///     &["user1".parse()?],
696    ///     &[],
697    /// ).is_err());
698    ///
699    /// // this fails because the config is non-interactive and the targeted credentials do not exist
700    /// assert!(device_config.nethsm_with_matching_creds(
701    ///     &[UserRole::Operator],
702    ///     &["user2".parse()?],
703    ///     &[],
704    /// ).is_err());
705    ///
706    /// // this fails because the config is non-interactive and no user in the targeted role exists
707    /// assert!(device_config.nethsm_with_matching_creds(
708    ///     &[UserRole::Metrics],
709    ///     &[],
710    ///     &[],
711    /// ).is_err());
712    /// # Ok(())
713    /// # }
714    /// ```
715    pub fn nethsm_with_matching_creds(
716        &self,
717        roles: &[UserRole],
718        names: &[UserId],
719        passphrases: &[Passphrase],
720    ) -> Result<NetHsm, Error> {
721        let nethsm: NetHsm = self.try_into()?;
722
723        // do not add any users if no user roles are requested
724        if !roles.is_empty() {
725            // try to find a user name with a role in the requested set of credentials
726            let creds = if let Ok(creds) = self.get_matching_credentials(roles, names) {
727                creds
728            // or request a user name in the first requested role
729            } else {
730                // if running non-interactively, return Error
731                if self.interactivity == ConfigInteractivity::NonInteractive {
732                    return Err(Error::NonInteractive);
733                }
734
735                let role = roles.first().expect("We have at least one user role");
736                ConfigCredentials::new(
737                    role.to_owned(),
738                    UserPrompt::new(role.to_owned()).prompt()?,
739                    None,
740                )
741            };
742
743            // if no passphrase is set for the credentials, attempt to set it
744            let credentials = if !creds.has_passphrase() {
745                // get index of the found credentials name in the input names
746                let name_index = names.iter().position(|name| name == &creds.get_name());
747                if let Some(name_index) = name_index {
748                    // if a passphrase index in passphrases matches the index of the user, use it
749                    if let Some(passphrase) = passphrases.get(name_index) {
750                        Credentials::new(creds.get_name(), Some(passphrase.clone()))
751                        // else try to set the passphrase interactively
752                    } else {
753                        // if running non-interactively, return Error
754                        if self.interactivity == ConfigInteractivity::NonInteractive {
755                            return Err(Error::NonInteractive);
756                        }
757                        Credentials::new(
758                            creds.get_name(),
759                            Some(
760                                PassphrasePrompt::User {
761                                    user_id: Some(creds.get_name()),
762                                    real_name: None,
763                                }
764                                .prompt()?,
765                            ),
766                        )
767                    }
768                    // else try to set the passphrase interactively
769                } else {
770                    // if running non-interactively, return Error
771                    if self.interactivity == ConfigInteractivity::NonInteractive {
772                        return Err(Error::NonInteractive);
773                    }
774                    Credentials::new(
775                        creds.get_name(),
776                        Some(
777                            PassphrasePrompt::User {
778                                user_id: Some(creds.get_name()),
779                                real_name: None,
780                            }
781                            .prompt()?,
782                        ),
783                    )
784                }
785            } else {
786                creds.into()
787            };
788
789            let user_id = credentials.user_id.clone();
790            // add the found credentials
791            nethsm.add_credentials(credentials);
792            // use the found credentials by default
793            nethsm.use_credentials(&user_id)?;
794        }
795
796        Ok(nethsm)
797    }
798}
799
800impl TryFrom<DeviceConfig> for NetHsm {
801    type Error = Error;
802    fn try_from(value: DeviceConfig) -> Result<Self, Error> {
803        let nethsm = NetHsm::new(value.connection.borrow().clone(), None, None, None)?;
804        for creds in value.credentials.borrow().clone().into_iter() {
805            nethsm.add_credentials(creds.into())
806        }
807        Ok(nethsm)
808    }
809}
810
811impl TryFrom<&DeviceConfig> for NetHsm {
812    type Error = Error;
813    fn try_from(value: &DeviceConfig) -> Result<Self, Error> {
814        let nethsm = NetHsm::new(value.connection.borrow().clone(), None, None, None)?;
815        for creds in value.credentials.borrow().clone().into_iter() {
816            nethsm.add_credentials(creds.into())
817        }
818        Ok(nethsm)
819    }
820}
821
822/// A configuration for NetHSM devices
823///
824/// Tracks a set of [`DeviceConfig`]s hashed by label.
825#[derive(Clone, Debug, Default, Deserialize, Serialize)]
826pub struct Config {
827    devices: RefCell<HashMap<String, DeviceConfig>>,
828    #[serde(skip)]
829    config_settings: ConfigSettings,
830}
831
832impl Config {
833    /// Loads the configuration
834    ///
835    /// If `path` is `Some`, the configuration is loaded from a specific file.
836    /// If `path` is `None`, a default location is assumed. The default location depends on the
837    /// chosen [`app_name`][`ConfigSettings::app_name`] and the OS platform. Assuming
838    /// [`app_name`][`ConfigSettings::app_name`] is `"my_app"` on Linux the default location is
839    /// `~/.config/my_app/config.toml`.
840    ///
841    /// If the targeted configuration file does not yet exist, an empty default [`Config`] is
842    /// assumed.
843    ///
844    /// # Errors
845    ///
846    /// Returns an [`Error::Load`] if loading the configuration file fails.
847    ///
848    /// # Examples
849    ///
850    /// ```
851    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
852    ///
853    /// # fn main() -> testresult::TestResult {
854    /// let config_settings = ConfigSettings::new(
855    ///     "my_app".to_string(),
856    ///     ConfigInteractivity::NonInteractive,
857    ///     None,
858    /// );
859    /// let config_from_default = Config::new(config_settings.clone(), None)?;
860    ///
861    /// let tmpfile = testdir::testdir!().join("my_app_new.conf");
862    /// let config_from_file = Config::new(config_settings, Some(&tmpfile))?;
863    /// # Ok(())
864    /// # }
865    /// ```
866    pub fn new(config_settings: ConfigSettings, path: Option<&Path>) -> Result<Self, Error> {
867        let mut config: Config = if let Some(path) = path {
868            confy::load_path(path).map_err(|error| Error::Load {
869                description: if let Some(error) = error.source() {
870                    error.to_string()
871                } else {
872                    "".to_string()
873                },
874                source: error,
875            })?
876        } else {
877            confy::load(
878                &config_settings.app_name,
879                Some(config_settings.config_name.0.as_str()),
880            )
881            .map_err(|error| Error::Load {
882                description: if let Some(error) = error.source() {
883                    error.to_string()
884                } else {
885                    "".to_string()
886                },
887                source: error,
888            })?
889        };
890        for (_label, device) in config.devices.borrow_mut().iter_mut() {
891            device.set_config_interactivity(config_settings.interactivity);
892        }
893        config.set_config_settings(config_settings);
894
895        Ok(config)
896    }
897
898    fn set_config_settings(&mut self, config_settings: ConfigSettings) {
899        self.config_settings = config_settings
900    }
901
902    /// Adds a [`DeviceConfig`]
903    ///
904    /// A device is defined by its `label`, the `url` to connect to and the chosen `tls_security`
905    /// for the connection.
906    ///
907    /// # Errors
908    ///
909    /// Returns an [`Error::DeviceExists`] if a [`DeviceConfig`] with the same `label` exists
910    /// already.
911    ///
912    /// # Examples
913    ///
914    /// ```
915    /// use nethsm::ConnectionSecurity;
916    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
917    /// # fn main() -> testresult::TestResult {
918    /// # let config = Config::new(
919    /// #    ConfigSettings::new(
920    /// #        "my_app".to_string(),
921    /// #        ConfigInteractivity::NonInteractive,
922    /// #        None,
923    /// #    ),
924    /// #    Some(&testdir::testdir!().join("my_app_add_device.conf")),
925    /// # )?;
926    ///
927    /// config.add_device(
928    ///     "device1".to_string(),
929    ///     "https://example.org/api/v1".parse()?,
930    ///     ConnectionSecurity::Unsafe,
931    /// )?;
932    ///
933    /// // adding the same device again leads to error
934    /// assert!(
935    ///     config
936    ///         .add_device(
937    ///             "device1".to_string(),
938    ///             "https://example.org/api/v1".parse()?,
939    ///             ConnectionSecurity::Unsafe,
940    ///         )
941    ///         .is_err()
942    /// );
943    /// # Ok(())
944    /// # }
945    /// ```
946    pub fn add_device(
947        &self,
948        label: String,
949        url: Url,
950        tls_security: ConnectionSecurity,
951    ) -> Result<(), Error> {
952        if let Entry::Vacant(entry) = self.devices.borrow_mut().entry(label.clone()) {
953            entry.insert(DeviceConfig::new(
954                Connection::new(url, tls_security),
955                vec![],
956                self.config_settings.interactivity,
957            )?);
958            Ok(())
959        } else {
960            Err(Error::DeviceExists(label))
961        }
962    }
963
964    /// Deletes a [`DeviceConfig`] identified by `label`
965    ///
966    /// # Errors
967    ///
968    /// Returns an [`Error::DeviceMissing`] if no [`DeviceConfig`] with a matching `label` exists.
969    ///
970    /// # Examples
971    ///
972    /// ```
973    /// use nethsm::ConnectionSecurity;
974    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
975    /// # fn main() -> testresult::TestResult {
976    /// # let config = Config::new(
977    /// #    ConfigSettings::new(
978    /// #        "my_app".to_string(),
979    /// #        ConfigInteractivity::NonInteractive,
980    /// #        None,
981    /// #    ),
982    /// #    Some(&testdir::testdir!().join("my_app_delete_device.conf")),
983    /// # )?;
984    ///
985    /// config.add_device(
986    ///     "device1".to_string(),
987    ///     "https://example.org/api/v1".parse()?,
988    ///     ConnectionSecurity::Unsafe,
989    /// )?;
990    ///
991    /// config.delete_device("device1")?;
992    ///
993    /// // deleting a non-existent device leads to error
994    /// assert!(config.delete_device("device1",).is_err());
995    /// # Ok(())
996    /// # }
997    /// ```
998    pub fn delete_device(&self, label: &str) -> Result<(), Error> {
999        if self.devices.borrow_mut().remove(label).is_some() {
1000            Ok(())
1001        } else {
1002            Err(Error::DeviceMissing(label.to_string()))
1003        }
1004    }
1005
1006    /// Returns a single [`DeviceConfig`] from the [`Config`] based on an optional `label`
1007    ///
1008    /// If `label` is [`Some`], a specific [`DeviceConfig`] is retrieved.
1009    /// If `label` is [`None`] and only one device is defined in the config, then the
1010    /// [`DeviceConfig`] for that device is returned.
1011    ///
1012    /// # Errors
1013    ///
1014    /// Returns an [`Error::DeviceMissing`] if `label` is [`Some`] but it can not be found in the
1015    /// [`Config`].
1016    /// Returns an [`Error::NoDevice`], if `label` is [`None`] but the [`Config`] has no
1017    /// devices.
1018    /// Returns an [`Error::NoDevice`], if `label` is [`None`] and the [`Config`] has more than one
1019    /// device.
1020    ///
1021    /// # Examples
1022    ///
1023    /// ```
1024    /// use nethsm::ConnectionSecurity;
1025    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1026    /// # fn main() -> testresult::TestResult {
1027    /// # let config = Config::new(
1028    /// #    ConfigSettings::new(
1029    /// #        "my_app".to_string(),
1030    /// #        ConfigInteractivity::NonInteractive,
1031    /// #        None,
1032    /// #    ),
1033    /// #    Some(&testdir::testdir!().join("my_app_get_device.conf")),
1034    /// # )?;
1035    ///
1036    /// config.add_device(
1037    ///     "device1".to_string(),
1038    ///     "https://example.org/api/v1".parse()?,
1039    ///     ConnectionSecurity::Unsafe,
1040    /// )?;
1041    ///
1042    /// config.get_device(Some("device1"))?;
1043    ///
1044    /// // this fails because the device does not exist
1045    /// assert!(config.get_device(Some("device2")).is_err());
1046    ///
1047    /// config.add_device(
1048    ///     "device2".to_string(),
1049    ///     "https://example.org/other/api/v1".parse()?,
1050    ///     ConnectionSecurity::Unsafe,
1051    /// )?;
1052    /// // this fails because there is more than one device
1053    /// assert!(config.get_device(None).is_err());
1054    ///
1055    /// config.delete_device("device1")?;
1056    /// config.delete_device("device2")?;
1057    /// // this fails because there is no device
1058    /// assert!(config.get_device(None).is_err());
1059    /// # Ok(())
1060    /// # }
1061    /// ```
1062    pub fn get_device(&self, label: Option<&str>) -> Result<DeviceConfig, Error> {
1063        if let Some(label) = label {
1064            if let Some(device_config) = self.devices.borrow().get(label) {
1065                Ok(device_config.clone())
1066            } else {
1067                Err(Error::DeviceMissing(label.to_string()))
1068            }
1069        } else {
1070            match self.devices.borrow().len() {
1071                0 => Err(Error::NoDevice),
1072                1 => Ok(self
1073                    .devices
1074                    .borrow()
1075                    .values()
1076                    .next()
1077                    .expect("there should be one")
1078                    .to_owned()),
1079                _ => Err(Error::MoreThanOneDevice),
1080            }
1081        }
1082    }
1083
1084    /// Returns a single [`DeviceConfig`] label from the [`Config`]
1085    ///
1086    /// # Errors
1087    ///
1088    /// Returns an error if not exactly one [`DeviceConfig`] is present.
1089    ///
1090    /// # Examples
1091    ///
1092    /// ```
1093    /// use nethsm::ConnectionSecurity;
1094    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1095    /// # fn main() -> testresult::TestResult {
1096    /// # let config = Config::new(
1097    /// #    ConfigSettings::new(
1098    /// #        "my_app".to_string(),
1099    /// #        ConfigInteractivity::NonInteractive,
1100    /// #        None,
1101    /// #    ),
1102    /// #    Some(&testdir::testdir!().join("my_app_get_single_device_label.conf")),
1103    /// # )?;
1104    ///
1105    /// config.add_device(
1106    ///     "device1".to_string(),
1107    ///     "https://example.org/api/v1".parse()?,
1108    ///     ConnectionSecurity::Unsafe,
1109    /// )?;
1110    ///
1111    /// assert_eq!(config.get_single_device_label()?, "device1".to_string());
1112    ///
1113    /// config.add_device(
1114    ///     "device2".to_string(),
1115    ///     "https://example.org/other/api/v1".parse()?,
1116    ///     ConnectionSecurity::Unsafe,
1117    /// )?;
1118    /// // this fails because there is more than one device
1119    /// assert!(config.get_single_device_label().is_err());
1120    ///
1121    /// config.delete_device("device1")?;
1122    /// config.delete_device("device2")?;
1123    /// // this fails because there is no device
1124    /// assert!(config.get_single_device_label().is_err());
1125    /// # Ok(())
1126    /// # }
1127    /// ```
1128    pub fn get_single_device_label(&self) -> Result<String, Error> {
1129        if self.devices.borrow().keys().len() == 1 {
1130            self.devices
1131                .borrow()
1132                .keys()
1133                .next()
1134                .map(|label| label.to_string())
1135                .ok_or(Error::NoDevice)
1136        } else {
1137            Err(Error::MoreThanOneDevice)
1138        }
1139    }
1140
1141    /// Adds new credentials for a [`DeviceConfig`]
1142    ///
1143    /// Using a `label` that identifies a [`DeviceConfig`], new credentials tracking a [`UserRole`],
1144    /// a name and optionally a passphrase are added to it.
1145    ///
1146    /// # Errors
1147    ///
1148    /// Returns an [`Error::DeviceMissing`] if the targeted [`DeviceConfig`] does not exist.
1149    /// Returns an [`Error::CredentialsExist`] if the [`ConfigCredentials`] identified by `name`
1150    /// exist already.
1151    ///
1152    /// # Examples
1153    ///
1154    /// ```
1155    /// use nethsm::{ConnectionSecurity, UserRole};
1156    /// use nethsm_config::{Config, ConfigCredentials, ConfigInteractivity, ConfigSettings};
1157    /// # fn main() -> testresult::TestResult {
1158    /// # let config = Config::new(
1159    /// #    ConfigSettings::new(
1160    /// #        "my_app".to_string(),
1161    /// #        ConfigInteractivity::NonInteractive,
1162    /// #        None,
1163    /// #    ),
1164    /// #    Some(&testdir::testdir!().join("my_app_add_credentials.conf")),
1165    /// # )?;
1166    ///
1167    /// // this fails because the targeted device does not yet exist
1168    /// assert!(
1169    ///     config
1170    ///         .add_credentials(
1171    ///             "device1".to_string(),
1172    ///             ConfigCredentials::new(
1173    ///                 UserRole::Operator,
1174    ///                 "user1".parse()?,
1175    ///                 Some("my-passphrase".to_string()),
1176    ///             ),
1177    ///         )
1178    ///         .is_err()
1179    /// );
1180    ///
1181    /// config.add_device(
1182    ///     "device1".to_string(),
1183    ///     "https://example.org/api/v1".parse()?,
1184    ///     ConnectionSecurity::Unsafe,
1185    /// )?;
1186    ///
1187    /// config.add_credentials(
1188    ///     "device1".to_string(),
1189    ///     ConfigCredentials::new(
1190    ///         UserRole::Operator,
1191    ///         "user1".parse()?,
1192    ///         Some("my-passphrase".to_string()),
1193    ///     ),
1194    /// )?;
1195    ///
1196    /// // this fails because the credentials exist already
1197    /// assert!(
1198    ///     config
1199    ///         .add_credentials(
1200    ///             "device1".to_string(),
1201    ///             ConfigCredentials::new(
1202    ///                 UserRole::Operator,
1203    ///                 "user1".parse()?,
1204    ///                 Some("my-passphrase".to_string()),
1205    ///             ),
1206    ///         )
1207    ///         .is_err()
1208    /// );
1209    /// # Ok(())
1210    /// # }
1211    /// ```
1212    pub fn add_credentials(
1213        &self,
1214        label: String,
1215        credentials: ConfigCredentials,
1216    ) -> Result<(), Error> {
1217        if let Some(device) = self.devices.borrow_mut().get_mut(&label) {
1218            device.add_credentials(credentials)?
1219        } else {
1220            return Err(Error::DeviceMissing(label));
1221        }
1222
1223        Ok(())
1224    }
1225
1226    /// Deletes credentials from a [`DeviceConfig`]
1227    ///
1228    /// The `label` identifies the [`DeviceConfig`] and the `name` the name of the credentials.
1229    ///
1230    /// # Errors
1231    ///
1232    /// Returns an [`Error::DeviceMissing`] if the targeted [`DeviceConfig`] does not exist.
1233    /// Returns an [`Error::CredentialsMissing`] if the targeted [`ConfigCredentials`] do not exist.
1234    ///
1235    /// # Examples
1236    ///
1237    /// ```
1238    /// use nethsm::{ConnectionSecurity, UserRole};
1239    /// use nethsm_config::{Config, ConfigCredentials, ConfigInteractivity, ConfigSettings};
1240    /// # fn main() -> testresult::TestResult {
1241    /// # let config = Config::new(
1242    /// #    ConfigSettings::new(
1243    /// #        "my_app".to_string(),
1244    /// #        ConfigInteractivity::NonInteractive,
1245    /// #        None,
1246    /// #    ),
1247    /// #    Some(&testdir::testdir!().join("my_app_delete_credentials.conf")),
1248    /// # )?;
1249    ///
1250    /// // this fails because the targeted device does not yet exist
1251    /// assert!(
1252    ///     config
1253    ///         .delete_credentials("device1", &"user1".parse()?)
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    /// // this fails because the targeted credentials does not yet exist
1264    /// assert!(
1265    ///     config
1266    ///         .delete_credentials("device1", &"user1".parse()?)
1267    ///         .is_err()
1268    /// );
1269    ///
1270    /// config.add_credentials(
1271    ///     "device1".to_string(),
1272    ///     ConfigCredentials::new(
1273    ///         UserRole::Operator,
1274    ///         "user1".parse()?,
1275    ///         Some("my-passphrase".to_string()),
1276    ///     ),
1277    /// )?;
1278    ///
1279    /// config.delete_credentials("device1", &"user1".parse()?)?;
1280    /// # Ok(())
1281    /// # }
1282    /// ```
1283    pub fn delete_credentials(&self, label: &str, name: &UserId) -> Result<(), Error> {
1284        if let Some(device) = self.devices.borrow_mut().get_mut(label) {
1285            device.delete_credentials(name)?
1286        } else {
1287            return Err(Error::DeviceMissing(label.to_string()));
1288        }
1289
1290        Ok(())
1291    }
1292
1293    /// Returns the [`ConfigSettings`] of the [`Config`]
1294    ///
1295    /// # Examples
1296    ///
1297    /// ```
1298    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1299    /// # fn main() -> testresult::TestResult {
1300    /// let config_settings = ConfigSettings::new(
1301    ///     "my_app".to_string(),
1302    ///     ConfigInteractivity::NonInteractive,
1303    ///     None,
1304    /// );
1305    /// let config = Config::new(
1306    ///     config_settings.clone(),
1307    ///     Some(&testdir::testdir!().join("my_app_get_config_settings.conf")),
1308    /// )?;
1309    ///
1310    /// println!("{:?}", config.get_config_settings());
1311    /// # assert_eq!(config.get_config_settings(), config_settings);
1312    /// # Ok(())
1313    /// # }
1314    /// ```
1315    pub fn get_config_settings(&self) -> ConfigSettings {
1316        self.config_settings.clone()
1317    }
1318
1319    /// Returns the default config file location
1320    ///
1321    /// # Errors
1322    ///
1323    /// Returns an [`Error::ConfigFileLocation`] if the config file location can not be retrieved.
1324    ///
1325    /// # Examples
1326    ///
1327    /// ```
1328    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1329    /// # fn main() -> testresult::TestResult {
1330    /// let config = Config::new(
1331    ///     ConfigSettings::new(
1332    ///         "my_app".to_string(),
1333    ///         ConfigInteractivity::NonInteractive,
1334    ///         None,
1335    ///     ),
1336    ///     Some(&testdir::testdir!().join("my_app_get_default_config_file_path.conf")),
1337    /// )?;
1338    ///
1339    /// println!("{:?}", config.get_default_config_file_path()?);
1340    /// # assert_eq!(
1341    /// #     config.get_default_config_file_path()?,
1342    /// #     dirs::config_dir()
1343    /// #         .ok_or("Platform does not support config dir")?
1344    /// #         .join(config.get_config_settings().app_name())
1345    /// #         .join(format!(
1346    /// #             "{}.toml",
1347    /// #             config.get_config_settings().config_name()
1348    /// #         ))
1349    /// # );
1350    /// # Ok(())
1351    /// # }
1352    /// ```
1353    pub fn get_default_config_file_path(&self) -> Result<PathBuf, Error> {
1354        confy::get_configuration_file_path(
1355            &self.config_settings.app_name,
1356            Some(self.config_settings.config_name().0.as_str()),
1357        )
1358        .map_err(Error::ConfigFileLocation)
1359    }
1360
1361    /// Writes the configuration to file
1362    ///
1363    /// # Errors
1364    ///
1365    /// Returns an [`Error::Store`] if the configuration can not be written to file.
1366    ///
1367    /// # Examples
1368    ///
1369    /// ```
1370    /// use nethsm::ConnectionSecurity;
1371    /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1372    ///
1373    /// # fn main() -> testresult::TestResult {
1374    /// let config_file = testdir::testdir!().join("my_app_store.conf");
1375    /// # let config = Config::new(
1376    /// #    ConfigSettings::new(
1377    /// #        "my_app".to_string(),
1378    /// #        ConfigInteractivity::NonInteractive,
1379    /// #        None,
1380    /// #    ),
1381    /// #    Some(&config_file),
1382    /// # )?;
1383    /// # config.add_device(
1384    /// #     "device1".to_string(),
1385    /// #     "https://example.org/api/v1".parse()?,
1386    /// #     ConnectionSecurity::Unsafe,
1387    /// # )?;
1388    /// config.store(Some(&config_file))?;
1389    ///
1390    /// // this fails because we can not write the configuration to a directory
1391    /// assert!(config.store(Some(&testdir::testdir!())).is_err());
1392    /// # // remove the config file again as we otherwise influence other tests
1393    /// # std::fs::remove_file(&config_file);
1394    /// # Ok(())
1395    /// # }
1396    /// ```
1397    pub fn store(&self, path: Option<&Path>) -> Result<(), Error> {
1398        if let Some(path) = path {
1399            confy::store_path(path, self).map_err(Error::Store)
1400        } else {
1401            confy::store(&self.config_settings.app_name, "config", self).map_err(Error::Store)
1402        }
1403    }
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408    use std::path::PathBuf;
1409
1410    use rstest::rstest;
1411    use testdir::testdir;
1412    use testresult::TestResult;
1413
1414    use super::*;
1415
1416    #[rstest]
1417    fn create_and_store_empty_config() -> TestResult {
1418        let config_file: PathBuf = testdir!().join("empty_config.toml");
1419        let config = Config::new(
1420            ConfigSettings::new("test".to_string(), ConfigInteractivity::Interactive, None),
1421            Some(&config_file),
1422        )?;
1423        println!("{config:#?}");
1424        config.store(Some(&config_file))?;
1425        println!("config file:\n{}", std::fs::read_to_string(config_file)?);
1426        Ok(())
1427    }
1428
1429    #[rstest]
1430    fn roundtrip_config(
1431        #[files("basic-config*.toml")]
1432        #[base_dir = "tests/fixtures/roundtrip-config/"]
1433        config_file: PathBuf,
1434    ) -> TestResult {
1435        let output_config_file: PathBuf = testdir!().join(
1436            config_file
1437                .file_name()
1438                .expect("the input config file should have a file name"),
1439        );
1440        let config = Config::new(
1441            ConfigSettings::new("test".to_string(), ConfigInteractivity::Interactive, None),
1442            Some(&config_file),
1443        )?;
1444        config.store(Some(&output_config_file))?;
1445        assert_eq!(
1446            std::fs::read_to_string(&output_config_file)?,
1447            std::fs::read_to_string(&config_file)?
1448        );
1449
1450        Ok(())
1451    }
1452}