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