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}