Skip to main content

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