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