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