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 KeyId,
15 NetHsm,
16 Passphrase,
17 Url,
18 UserId,
19 UserRole,
20};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 ConfigCredentials,
25 ExtendedUserMapping,
26 PassphrasePrompt,
27 SystemUserId,
28 UserMapping,
29 UserPrompt,
30};
31
32/// Errors related to configuration
33#[derive(Debug, thiserror::Error)]
34pub enum Error {
35 /// Issue getting the config file location
36 #[error("Config file issue: {0}")]
37 ConfigFileLocation(#[source] confy::ConfyError),
38
39 /// A config loading error
40 ///
41 /// The variant tracks a [`ConfyError`][`confy::ConfyError`] and an optional
42 /// description of an inner Error type.
43 /// The description is tracked separately, as otherwise we do not get to useful error messages
44 /// of wrapped Error types (e.g. those for loading TOML files).
45 #[error("Config loading issue: {source}\n{description}")]
46 Load {
47 source: confy::ConfyError,
48 description: String,
49 },
50
51 /// A config storing error
52 #[error("Config storing issue: {0}")]
53 Store(#[source] confy::ConfyError),
54
55 /// Credentials exist already
56 #[error("Credentials exist already: {0}")]
57 CredentialsExist(UserId),
58
59 /// Credentials do not exist
60 #[error("Credentials do not exist: {0}")]
61 CredentialsMissing(UserId),
62
63 /// None of the provided users map to one of the provided roles
64 #[error("None of the provided users ({names:?}) map to one of the provided roles ({roles:?})")]
65 MatchingCredentialsMissing {
66 names: Vec<UserId>,
67 roles: Vec<UserRole>,
68 },
69
70 /// Credentials do not exist
71 #[error("No user matching one of the requested roles ({0:?}) exists")]
72 NoMatchingCredentials(Vec<UserRole>),
73
74 /// There is no mapping for a provided system user name.
75 #[error("No mapping found where a system user matches the name {name}")]
76 NoMatchingMappingForSystemUser { name: String },
77
78 /// Shamir's Secret Sharing (SSS) is not used for administrative secret handling, but users for
79 /// handling of secret shares are defined
80 #[error(
81 "Shamir's Secret Sharing not used for administrative secret handling, but the following users are setup to handle shares: {share_users:?}"
82 )]
83 NoSssButShareUsers { share_users: Vec<SystemUserId> },
84
85 /// Device exists already
86 #[error("Device exist already: {0}")]
87 DeviceExists(String),
88
89 /// Device does not exist
90 #[error("Device does not exist: {0}")]
91 DeviceMissing(String),
92
93 /// Duplicate NetHsm user names
94 #[error("The NetHsm user ID {nethsm_user_id} is used more than once!")]
95 DuplicateNetHsmUserId { nethsm_user_id: UserId },
96
97 /// Duplicate system user names
98 #[error("The authorized SSH key {ssh_authorized_key} is used more than once!")]
99 DuplicateSshAuthorizedKey { ssh_authorized_key: String },
100
101 /// Duplicate key ID
102 #[error("The key ID {key_id} is used more than once!")]
103 DuplicateKeyId { key_id: KeyId },
104
105 /// Duplicate key ID in a namespace
106 #[error("The key ID {key_id} is used more than once in namespace {namespace}!")]
107 DuplicateKeyIdInNamespace { key_id: KeyId, namespace: String },
108
109 /// Duplicate system user names
110 #[error("The system user ID {system_user_id} is used more than once!")]
111 DuplicateSystemUserId { system_user_id: SystemUserId },
112
113 /// Duplicate tag
114 #[error("The tag {tag} is used more than once!")]
115 DuplicateTag { tag: String },
116
117 /// Duplicate tag
118 #[error("The tag {tag} is used more than once in namespace {namespace}!")]
119 DuplicateTagInNamespace { tag: String, namespace: String },
120
121 /// Missing system-wide user in the Administrator role (R-Administrator)
122 #[error("No system-wide user in the Administrator role exists.")]
123 MissingAdministrator,
124
125 /// Missing user in the Administrator role for a namespace (N-Administrator)
126 #[error("No user in the Administrator role exist for the namespaces {namespaces:?}")]
127 MissingNamespaceAdministrators { namespaces: Vec<String> },
128
129 /// Missing system user for downloading shares of a shared secret
130 #[error("No system user for downloading shares of a shared secret exists.")]
131 MissingShareDownloadUser,
132
133 /// Missing system user for uploading shares of a shared secret
134 #[error("No system user for uploading shares of a shared secret exists.")]
135 MissingShareUploadUser,
136
137 /// There is more than one device (but none has been specified)
138 #[error("There is more than one device")]
139 MoreThanOneDevice,
140
141 /// There is no device
142 #[error("There is no device")]
143 NoDevice,
144
145 /// The configuration can not be used interactively
146 #[error("The configuration can not be used interactively")]
147 NonInteractive,
148
149 /// NetHsm connection initialization error
150 #[error("NetHsm connection can not be created: {0}")]
151 NetHsm(#[from] nethsm::Error),
152
153 /// A prompt requesting user data failed
154 #[error("A prompt issue")]
155 Prompt(#[from] crate::prompt::Error),
156
157 /// User data is invalid
158 #[error("User data invalid: {0}")]
159 User(#[from] nethsm::UserError),
160}
161
162/// The interactivity of a configuration
163///
164/// This enum is used by [`Config`] and [`DeviceConfig`] to define whether missing items are
165/// prompted for interactively ([`ConfigInteractivity::Interactive`]) or not
166/// ([`ConfigInteractivity::NonInteractive`]).
167#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
168pub enum ConfigInteractivity {
169 /// The configuration may spawn interactive prompts to request more data (e.g. usernames or
170 /// passphrases)
171 Interactive,
172 /// The configuration will return an [`Error`] if interactive prompts need to be spawned to
173 /// request more data (e.g. usernames or passphrases)
174 #[default]
175 NonInteractive,
176}
177
178/// The name of a configuration
179///
180/// The name defines the file name (without file suffix) used by a [`Config`] object.
181/// It defaults to `"config"`, but may be set specifically when initializing a [`Config`].
182#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
183pub struct ConfigName(String);
184
185impl Default for ConfigName {
186 fn default() -> Self {
187 Self("config".to_string())
188 }
189}
190
191impl Display for ConfigName {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 self.0.fmt(f)
194 }
195}
196
197impl FromStr for ConfigName {
198 type Err = Error;
199
200 fn from_str(s: &str) -> Result<Self, Self::Err> {
201 Ok(Self(s.to_string()))
202 }
203}
204
205/// The settings for a [`Config`]
206///
207/// Settings contain the [`ConfigName`] by which the configuration file is loaded and stored, the
208/// application name which uses the configuration (and also influences the file path of the
209/// configuration) and the interactivity setting, which defines whether missing items are prompted
210/// for interactively or not.
211#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
212pub struct ConfigSettings {
213 /// The configuration name (file name without suffix)
214 config_name: ConfigName,
215 /// The name of the application using a [`Config`]
216 app_name: String,
217 /// The interactivity setting for the [`Config`] (and any [`DeviceConfig`] used by it)
218 interactivity: ConfigInteractivity,
219}
220
221impl ConfigSettings {
222 /// Creates a new [`ConfigSettings`]
223 ///
224 /// # Examples
225 ///
226 /// ```
227 /// use nethsm_config::{ConfigInteractivity, ConfigSettings};
228 ///
229 /// # fn main() -> testresult::TestResult {
230 /// // settings for an application called "my_app", that uses a custom configuration file named "my_app-config" interactively
231 /// let config_settings = ConfigSettings::new(
232 /// "my_app".to_string(),
233 /// ConfigInteractivity::Interactive,
234 /// Some("my_app-config".parse()?),
235 /// );
236 ///
237 /// // settings for an application called "my_app", that uses a default config file non-interactively
238 /// let config_settings = ConfigSettings::new(
239 /// "my_app".to_string(),
240 /// ConfigInteractivity::NonInteractive,
241 /// None,
242 /// );
243 /// # Ok(())
244 /// # }
245 /// ```
246 pub fn new(
247 app_name: String,
248 interactivity: ConfigInteractivity,
249 config_name: Option<ConfigName>,
250 ) -> Self {
251 Self {
252 app_name,
253 interactivity,
254 config_name: config_name.unwrap_or_default(),
255 }
256 }
257
258 /// Returns the configuration name
259 pub fn config_name(&self) -> ConfigName {
260 self.config_name.to_owned()
261 }
262
263 /// Returns the application name
264 pub fn app_name(&self) -> String {
265 self.app_name.clone()
266 }
267
268 /// Returns the interactivity setting
269 pub fn interactivity(&self) -> ConfigInteractivity {
270 self.interactivity
271 }
272}
273
274/// The configuration for a [`NetHsm`]
275///
276/// Tracks the [`Connection`] for a [`NetHsm`] as well as a set of [`ConfigCredentials`].
277#[derive(Clone, Debug, Deserialize, Serialize)]
278pub struct DeviceConfig {
279 connection: RefCell<Connection>,
280 credentials: RefCell<HashSet<ConfigCredentials>>,
281 #[serde(skip)]
282 interactivity: ConfigInteractivity,
283}
284
285impl DeviceConfig {
286 /// Creates a new [`DeviceConfig`]
287 ///
288 /// Creates a new [`DeviceConfig`] by providing a `connection`, an optional set of `credentials`
289 /// and the `interactivity` setting.
290 ///
291 /// # Errors
292 ///
293 /// Returns an [`Error::CredentialsExist`] if `credentials` contains duplicates.
294 ///
295 /// # Examples
296 ///
297 /// ```
298 /// use nethsm::{Connection, ConnectionSecurity, UserRole};
299 /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
300 ///
301 /// # fn main() -> testresult::TestResult {
302 /// let connection = Connection::new(
303 /// "https://example.org/api/v1".parse()?,
304 /// ConnectionSecurity::Unsafe,
305 /// );
306 ///
307 /// DeviceConfig::new(
308 /// connection.clone(),
309 /// vec![],
310 /// ConfigInteractivity::NonInteractive,
311 /// )?;
312 ///
313 /// DeviceConfig::new(
314 /// connection.clone(),
315 /// vec![ConfigCredentials::new(
316 /// UserRole::Operator,
317 /// "user1".parse()?,
318 /// Some("my-passphrase".to_string()),
319 /// )],
320 /// ConfigInteractivity::NonInteractive,
321 /// )?;
322 ///
323 /// // this fails because the provided credentials contain duplicates
324 /// assert!(
325 /// DeviceConfig::new(
326 /// connection.clone(),
327 /// vec![
328 /// ConfigCredentials::new(
329 /// UserRole::Operator,
330 /// "user1".parse()?,
331 /// Some("my-passphrase".to_string()),
332 /// ),
333 /// ConfigCredentials::new(
334 /// UserRole::Operator,
335 /// "user1".parse()?,
336 /// Some("my-passphrase".to_string()),
337 /// ),
338 /// ],
339 /// ConfigInteractivity::NonInteractive,
340 /// )
341 /// .is_err()
342 /// );
343 /// # Ok(())
344 /// # }
345 /// ```
346 pub fn new(
347 connection: Connection,
348 credentials: Vec<ConfigCredentials>,
349 interactivity: ConfigInteractivity,
350 ) -> Result<DeviceConfig, Error> {
351 let device_config = DeviceConfig {
352 connection: RefCell::new(connection),
353 credentials: RefCell::new(HashSet::new()),
354 interactivity,
355 };
356
357 if !credentials.is_empty() {
358 for creds in credentials.into_iter() {
359 device_config.add_credentials(creds)?
360 }
361 }
362
363 Ok(device_config)
364 }
365
366 /// Sets the interactivity setting
367 ///
368 /// **NOTE**: This method is not necessarily useful by itself, as one usually wants to use the
369 /// same [`ConfigInteractivity`] as that of a [`Config`], which holds the [`DeviceConfig`].
370 pub fn set_config_interactivity(&mut self, config_type: ConfigInteractivity) {
371 self.interactivity = config_type;
372 }
373
374 /// Adds credentials to the device
375 ///
376 /// Adds new [`ConfigCredentials`] to the [`DeviceConfig`].
377 ///
378 /// # Errors
379 ///
380 /// Returns an [`Error::CredentialsExist`] if the `credentials` exist already.
381 ///
382 /// # Examples
383 ///
384 /// ```
385 /// use nethsm::{Connection, ConnectionSecurity, UserRole};
386 /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
387 ///
388 /// # fn main() -> testresult::TestResult {
389 /// let connection = Connection::new(
390 /// "https://example.org/api/v1".parse()?,
391 /// ConnectionSecurity::Unsafe,
392 /// );
393 ///
394 /// let device_config = DeviceConfig::new(
395 /// connection.clone(),
396 /// vec![],
397 /// ConfigInteractivity::NonInteractive,
398 /// )?;
399 ///
400 /// device_config.add_credentials(ConfigCredentials::new(
401 /// UserRole::Operator,
402 /// "user1".parse()?,
403 /// Some("my-passphrase".to_string()),
404 /// ))?;
405 ///
406 /// // this fails because the credentials exist already
407 /// assert!(
408 /// device_config
409 /// .add_credentials(ConfigCredentials::new(
410 /// UserRole::Operator,
411 /// "user1".parse()?,
412 /// Some("my-passphrase".to_string()),
413 /// ))
414 /// .is_err()
415 /// );
416 /// # Ok(())
417 /// # }
418 /// ```
419 pub fn add_credentials(&self, credentials: ConfigCredentials) -> Result<(), Error> {
420 if !self
421 .credentials
422 .borrow()
423 .iter()
424 .any(|creds| creds.get_name() == credentials.get_name())
425 {
426 self.credentials.borrow_mut().insert(credentials);
427 Ok(())
428 } else {
429 Err(Error::CredentialsExist(credentials.get_name()))
430 }
431 }
432
433 /// Returns credentials by name
434 ///
435 /// Returns existing [`ConfigCredentials`] from the [`DeviceConfig`].
436 ///
437 /// # Errors
438 ///
439 /// Returns an [`Error::CredentialsMissing`] if no [`ConfigCredentials`] match the provided
440 /// `name`.
441 ///
442 /// # Examples
443 ///
444 /// ```
445 /// use nethsm::{Connection, ConnectionSecurity, UserRole};
446 /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
447 /// # fn main() -> testresult::TestResult {
448 /// let connection = Connection::new(
449 /// "https://example.org/api/v1".parse()?,
450 /// ConnectionSecurity::Unsafe,
451 /// );
452 ///
453 /// let device_config = DeviceConfig::new(
454 /// connection.clone(),
455 /// vec![],
456 /// ConfigInteractivity::NonInteractive,
457 /// )?;
458 ///
459 /// // this fails because the credentials do not exist
460 /// assert!(device_config.get_credentials(&"user1".parse()?).is_err());
461 ///
462 /// device_config.add_credentials(ConfigCredentials::new(
463 /// UserRole::Operator,
464 /// "user1".parse()?,
465 /// Some("my-passphrase".to_string()),
466 /// ))?;
467 ///
468 /// device_config.get_credentials(&"user1".parse()?)?;
469 /// # Ok(())
470 /// # }
471 /// ```
472 pub fn get_credentials(&self, name: &UserId) -> Result<ConfigCredentials, Error> {
473 if let Some(creds) = self
474 .credentials
475 .borrow()
476 .iter()
477 .find(|creds| &creds.get_name() == name)
478 {
479 Ok(creds.clone())
480 } else {
481 Err(Error::CredentialsMissing(name.to_owned()))
482 }
483 }
484
485 /// Deletes credentials by name
486 ///
487 /// Deletes [`ConfigCredentials`] identified by `name`.
488 ///
489 /// # Errors
490 ///
491 /// Returns an [`Error::CredentialsMissing`] if no [`ConfigCredentials`] match the provided
492 /// name.
493 ///
494 /// # Examples
495 ///
496 /// ```
497 /// use nethsm::{Connection, ConnectionSecurity, UserRole};
498 /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
499 ///
500 /// # fn main() -> testresult::TestResult {
501 /// let device_config = DeviceConfig::new(
502 /// Connection::new(
503 /// "https://example.org/api/v1".parse()?,
504 /// ConnectionSecurity::Unsafe,
505 /// ),
506 /// vec![],
507 /// ConfigInteractivity::NonInteractive,
508 /// )?;
509 /// device_config.add_credentials(ConfigCredentials::new(
510 /// UserRole::Operator,
511 /// "user1".parse()?,
512 /// Some("my-passphrase".to_string()),
513 /// ))?;
514 ///
515 /// device_config.delete_credentials(&"user1".parse()?)?;
516 ///
517 /// // this fails because the credentials do not exist
518 /// assert!(device_config.delete_credentials(&"user1".parse()?).is_err());
519 /// # Ok(())
520 /// # }
521 /// ```
522 pub fn delete_credentials(&self, name: &UserId) -> Result<(), Error> {
523 let before = self.credentials.borrow().len();
524 self.credentials
525 .borrow_mut()
526 .retain(|creds| &creds.get_name() != name);
527 let after = self.credentials.borrow().len();
528 if before == after {
529 Err(Error::CredentialsMissing(name.to_owned()))
530 } else {
531 Ok(())
532 }
533 }
534
535 /// Returns credentials machting one or several roles and a optionally a name
536 ///
537 /// Returns [`ConfigCredentials`] matching a list of [`UserRole`]s and/or a list of [`UserId`]s.
538 ///
539 /// If `names` is empty, the [`ConfigCredentials`] first found matching one of the [`UserRole`]s
540 /// provided using `roles` are returned.
541 /// If `names` contains at least one entry, the first [`ConfigCredentials`] with a matching
542 /// [`UserId`] that have at least one matching [`UserRole`] are returned.
543 ///
544 /// # Errors
545 ///
546 /// Returns an [`Error::NoMatchingCredentials`] if `names` is empty and no existing credentials
547 /// match any of the provided `roles`.
548 /// Returns an [`Error::CredentialsMissing`] if a [`UserId`] in `names` does not exist and no
549 /// [`ConfigCredentials`] have been returned yet.
550 /// Returns an [`Error::MatchingCredentialsMissing`] if no [`ConfigCredentials`] matching either
551 /// the provided `names` or `roles` can be found.
552 ///
553 /// # Examples
554 ///
555 /// ```
556 /// use nethsm::{Connection, ConnectionSecurity, UserRole};
557 /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
558 ///
559 /// # fn main() -> testresult::TestResult {
560 /// let device_config = DeviceConfig::new(
561 /// Connection::new(
562 /// "https://example.org/api/v1".parse()?,
563 /// ConnectionSecurity::Unsafe,
564 /// ),
565 /// vec![ConfigCredentials::new(
566 /// UserRole::Administrator,
567 /// "admin1".parse()?,
568 /// Some("my-passphrase".to_string()),
569 /// )],
570 /// ConfigInteractivity::NonInteractive,
571 /// )?;
572 /// device_config.add_credentials(ConfigCredentials::new(
573 /// UserRole::Operator,
574 /// "user1".parse()?,
575 /// Some("my-passphrase".to_string()),
576 /// ))?;
577 ///
578 /// device_config.get_matching_credentials(&[UserRole::Operator], &["user1".parse()?])?;
579 /// device_config.get_matching_credentials(&[UserRole::Administrator], &["admin1".parse()?])?;
580 /// assert_eq!(
581 /// device_config
582 /// .get_matching_credentials(&[UserRole::Operator], &[])?
583 /// .get_name(),
584 /// "user1".parse()?
585 /// );
586 /// assert_eq!(
587 /// device_config
588 /// .get_matching_credentials(&[UserRole::Administrator], &[])?
589 /// .get_name(),
590 /// "admin1".parse()?
591 /// );
592 ///
593 /// // this fails because we must provide a role to match against
594 /// assert!(
595 /// device_config
596 /// .get_matching_credentials(&[], &["user1".parse()?])
597 /// .is_err()
598 /// );
599 ///
600 /// // this fails because no user in the requested role exists
601 /// assert!(
602 /// device_config
603 /// .get_matching_credentials(&[UserRole::Metrics], &[])
604 /// .is_err()
605 /// );
606 ///
607 /// // this fails because no user with the name first provided exists
608 /// assert!(
609 /// device_config
610 /// .get_matching_credentials(&[UserRole::Operator], &["user2".parse()?, "user1".parse()?])
611 /// .is_err()
612 /// );
613 ///
614 /// // this fails because no user in the requested role with any of the provided names exists
615 /// assert!(
616 /// device_config
617 /// .get_matching_credentials(&[UserRole::Metrics], &["admin1".parse()?, "user1".parse()?])
618 /// .is_err()
619 /// );
620 /// # Ok(())
621 /// # }
622 /// ```
623 pub fn get_matching_credentials(
624 &self,
625 roles: &[UserRole],
626 names: &[UserId],
627 ) -> Result<ConfigCredentials, Error> {
628 if names.is_empty() {
629 let creds = self
630 .credentials
631 .borrow()
632 .iter()
633 .filter_map(|creds| {
634 if roles.contains(&creds.get_role()) {
635 Some(creds.clone())
636 } else {
637 None
638 }
639 })
640 .collect::<Vec<ConfigCredentials>>();
641 return creds
642 .first()
643 .ok_or_else(|| Error::NoMatchingCredentials(roles.to_vec()))
644 .cloned();
645 }
646
647 for name in names {
648 if let Ok(creds) = &self.get_credentials(name) {
649 if roles.contains(&creds.get_role()) {
650 return Ok(creds.clone());
651 }
652 } else {
653 return Err(Error::CredentialsMissing(name.to_owned()));
654 }
655 }
656
657 Err(Error::MatchingCredentialsMissing {
658 names: names.to_vec(),
659 roles: roles.to_vec(),
660 })
661 }
662
663 /// Returns a [`NetHsm`] based on the [`DeviceConfig`] (optionally with one set of credentials)
664 ///
665 /// Creates a [`NetHsm`] based on the [`DeviceConfig`].
666 /// Only if `roles` is not empty, one set of [`ConfigCredentials`] based on `roles`,
667 /// `names` and `passphrases` is added to the [`NetHsm`].
668 ///
669 /// **WARNING**: Depending on the [`ConfigInteractivity`] chosen when initializing the
670 /// [`DeviceConfig`] this method behaves differently with regards to adding credentials!
671 ///
672 /// # [`NonInteractive`][`ConfigInteractivity::NonInteractive`]
673 ///
674 /// If `roles` is not empty, optionally adds one set of [`ConfigCredentials`] found by
675 /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] to the returned
676 /// [`NetHsm`], based on `roles` and `names`.
677 /// If the found [`ConfigCredentials`] do not contain a passphrase, a [`Passphrase`] in
678 /// `pasphrases` with the same index as that of the [`UserId`] in `names` is used.
679 ///
680 /// # [`Interactive`][`ConfigInteractivity::Interactive`]
681 ///
682 /// If `roles` is not empty, optionally attempts to add one set of [`ConfigCredentials`] with
683 /// the help of [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] to the
684 /// returned [`NetHsm`], based on `roles` and `names`.
685 /// If no [`ConfigCredentials`] are found by
686 /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`], users are
687 /// interactively prompted for providing a user name.
688 /// If the found or prompted for [`UserId`] [`ConfigCredentials`] do not contain a passphrase, a
689 /// [`Passphrase`] in `pasphrases` with the same index as that of the [`UserId`] in `names`
690 /// is used. If [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`], or
691 /// those the user has been prompted for provides [`ConfigCredentials`] without a
692 /// passphrase, a [`Passphrase`] in `pasphrases` with the same index as that of the
693 /// [`UserId`] in `names` is used. If none is provided (at the right location) in `passphrases`,
694 /// the user is prompted for a passphrase interactively.
695 ///
696 /// # Errors
697 ///
698 /// Returns an [`Error::NoMatchingCredentials`], [`Error::CredentialsMissing`], or
699 /// [`Error::MatchingCredentialsMissing`] if the [`DeviceConfig`] is initialized with
700 /// [`Interactive`][`ConfigInteractivity::Interactive`] and
701 /// [`get_matching_credentials`][`DeviceConfig::get_matching_credentials`] is unable to return
702 /// [`ConfigCredentials`] based on `roles` and `names`.
703 ///
704 /// Returns an [`Error::NonInteractive`] if the [`DeviceConfig`] is initialized with
705 /// [`NonInteractive`][`ConfigInteractivity::NonInteractive`], but additional data would be
706 /// requested interactively.
707 ///
708 /// Returns an [`Error::Prompt`] if requesting additional data interactively leads to error.
709 ///
710 /// # Examples
711 ///
712 /// ```
713 /// use nethsm::{Connection, ConnectionSecurity, Passphrase, UserRole};
714 /// use nethsm_config::{ConfigCredentials, ConfigInteractivity, DeviceConfig};
715 ///
716 /// # fn main() -> testresult::TestResult {
717 /// let device_config = DeviceConfig::new(
718 /// Connection::new(
719 /// "https://example.org/api/v1".parse()?,
720 /// ConnectionSecurity::Unsafe,
721 /// ),
722 /// vec![ConfigCredentials::new(
723 /// UserRole::Administrator,
724 /// "admin1".parse()?,
725 /// Some("my-passphrase".to_string()),
726 /// )],
727 /// ConfigInteractivity::NonInteractive,
728 /// )?;
729 /// device_config.add_credentials(ConfigCredentials::new(
730 /// UserRole::Operator,
731 /// "user1".parse()?,
732 /// None,
733 /// ))?;
734 ///
735 /// // NetHsm with Operator credentials
736 /// // this works non-interactively, although the credentials in the config provide no passphrase, because we provide the passphrase manually
737 /// device_config.nethsm_with_matching_creds(
738 /// &[UserRole::Operator],
739 /// &["user1".parse()?],
740 /// &[Passphrase::new("my-passphrase".to_string())],
741 /// )?;
742 ///
743 /// // NetHsm with Administrator credentials
744 /// // this automatically selects "admin1" as it is the only user in the Administrator role
745 /// // this works non-interactively, because the credentials in the config provide a passphrase!
746 /// device_config.nethsm_with_matching_creds(
747 /// &[UserRole::Administrator],
748 /// &[],
749 /// &[],
750 /// )?;
751 ///
752 /// // a NetHsm without any credentials
753 /// device_config.nethsm_with_matching_creds(
754 /// &[],
755 /// &[],
756 /// &[],
757 /// )?;
758 ///
759 /// // this fails because the config is non-interactive, the targeted credentials do not offer a passphrase and we also provide none
760 /// assert!(device_config.nethsm_with_matching_creds(
761 /// &[UserRole::Operator],
762 /// &["user1".parse()?],
763 /// &[],
764 /// ).is_err());
765 ///
766 /// // this fails because the config is non-interactive and the targeted credentials do not exist
767 /// assert!(device_config.nethsm_with_matching_creds(
768 /// &[UserRole::Operator],
769 /// &["user2".parse()?],
770 /// &[],
771 /// ).is_err());
772 ///
773 /// // this fails because the config is non-interactive and no user in the targeted role exists
774 /// assert!(device_config.nethsm_with_matching_creds(
775 /// &[UserRole::Metrics],
776 /// &[],
777 /// &[],
778 /// ).is_err());
779 /// # Ok(())
780 /// # }
781 /// ```
782 pub fn nethsm_with_matching_creds(
783 &self,
784 roles: &[UserRole],
785 names: &[UserId],
786 passphrases: &[Passphrase],
787 ) -> Result<NetHsm, Error> {
788 let nethsm: NetHsm = self.try_into()?;
789
790 // do not add any users if no user roles are requested
791 if !roles.is_empty() {
792 // try to find a user name with a role in the requested set of credentials
793 let creds = if let Ok(creds) = self.get_matching_credentials(roles, names) {
794 creds
795 // or request a user name in the first requested role
796 } else {
797 // if running non-interactively, return Error
798 if self.interactivity == ConfigInteractivity::NonInteractive {
799 return Err(Error::NonInteractive);
800 }
801
802 let role = roles.first().expect("We have at least one user role");
803 ConfigCredentials::new(
804 role.to_owned(),
805 UserPrompt::new(role.to_owned()).prompt()?,
806 None,
807 )
808 };
809
810 // if no passphrase is set for the credentials, attempt to set it
811 let credentials = if !creds.has_passphrase() {
812 // get index of the found credentials name in the input names
813 let name_index = names.iter().position(|name| name == &creds.get_name());
814 if let Some(name_index) = name_index {
815 // if a passphrase index in passphrases matches the index of the user, use it
816 if let Some(passphrase) = passphrases.get(name_index) {
817 Credentials::new(creds.get_name(), Some(passphrase.clone()))
818 // else try to set the passphrase interactively
819 } else {
820 // if running non-interactively, return Error
821 if self.interactivity == ConfigInteractivity::NonInteractive {
822 return Err(Error::NonInteractive);
823 }
824 Credentials::new(
825 creds.get_name(),
826 Some(
827 PassphrasePrompt::User {
828 user_id: Some(creds.get_name()),
829 real_name: None,
830 }
831 .prompt()?,
832 ),
833 )
834 }
835 // else try to set the passphrase interactively
836 } else {
837 // if running non-interactively, return Error
838 if self.interactivity == ConfigInteractivity::NonInteractive {
839 return Err(Error::NonInteractive);
840 }
841 Credentials::new(
842 creds.get_name(),
843 Some(
844 PassphrasePrompt::User {
845 user_id: Some(creds.get_name()),
846 real_name: None,
847 }
848 .prompt()?,
849 ),
850 )
851 }
852 } else {
853 creds.into()
854 };
855
856 let user_id = credentials.user_id.clone();
857 // add the found credentials
858 nethsm.add_credentials(credentials);
859 // use the found credentials by default
860 nethsm.use_credentials(&user_id)?;
861 }
862
863 Ok(nethsm)
864 }
865}
866
867impl TryFrom<DeviceConfig> for NetHsm {
868 type Error = Error;
869 fn try_from(value: DeviceConfig) -> Result<Self, Error> {
870 let nethsm = NetHsm::new(value.connection.borrow().clone(), None, None, None)?;
871 for creds in value.credentials.borrow().clone().into_iter() {
872 nethsm.add_credentials(creds.into())
873 }
874 Ok(nethsm)
875 }
876}
877
878impl TryFrom<&DeviceConfig> for NetHsm {
879 type Error = Error;
880 fn try_from(value: &DeviceConfig) -> Result<Self, Error> {
881 let nethsm = NetHsm::new(value.connection.borrow().clone(), None, None, None)?;
882 for creds in value.credentials.borrow().clone().into_iter() {
883 nethsm.add_credentials(creds.into())
884 }
885 Ok(nethsm)
886 }
887}
888
889/// A configuration for NetHSM devices
890///
891/// Tracks a set of [`DeviceConfig`]s hashed by label.
892#[derive(Clone, Debug, Default, Deserialize, Serialize)]
893pub struct Config {
894 devices: RefCell<HashMap<String, DeviceConfig>>,
895 #[serde(skip)]
896 config_settings: ConfigSettings,
897}
898
899impl Config {
900 /// Loads the configuration
901 ///
902 /// If `path` is `Some`, the configuration is loaded from a specific file.
903 /// If `path` is `None`, a default location is assumed. The default location depends on the
904 /// chosen [`app_name`][`ConfigSettings::app_name`] and the OS platform. Assuming
905 /// [`app_name`][`ConfigSettings::app_name`] is `"my_app"` on Linux the default location is
906 /// `~/.config/my_app/config.toml`.
907 ///
908 /// If the targeted configuration file does not yet exist, an empty default [`Config`] is
909 /// assumed.
910 ///
911 /// # Errors
912 ///
913 /// Returns an [`Error::Load`] if loading the configuration file fails.
914 ///
915 /// # Examples
916 ///
917 /// ```
918 /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
919 ///
920 /// # fn main() -> testresult::TestResult {
921 /// let config_settings = ConfigSettings::new(
922 /// "my_app".to_string(),
923 /// ConfigInteractivity::NonInteractive,
924 /// None,
925 /// );
926 /// let config_from_default = Config::new(config_settings.clone(), None)?;
927 ///
928 /// let tmpfile = testdir::testdir!().join("my_app_new.conf");
929 /// let config_from_file = Config::new(config_settings, Some(&tmpfile))?;
930 /// # Ok(())
931 /// # }
932 /// ```
933 pub fn new(config_settings: ConfigSettings, path: Option<&Path>) -> Result<Self, Error> {
934 let mut config: Config = if let Some(path) = path {
935 confy::load_path(path).map_err(|error| Error::Load {
936 description: if let Some(error) = error.source() {
937 error.to_string()
938 } else {
939 "".to_string()
940 },
941 source: error,
942 })?
943 } else {
944 confy::load(
945 &config_settings.app_name,
946 Some(config_settings.config_name.0.as_str()),
947 )
948 .map_err(|error| Error::Load {
949 description: if let Some(error) = error.source() {
950 error.to_string()
951 } else {
952 "".to_string()
953 },
954 source: error,
955 })?
956 };
957 for (_label, device) in config.devices.borrow_mut().iter_mut() {
958 device.set_config_interactivity(config_settings.interactivity);
959 }
960 config.set_config_settings(config_settings);
961
962 Ok(config)
963 }
964
965 fn set_config_settings(&mut self, config_settings: ConfigSettings) {
966 self.config_settings = config_settings
967 }
968
969 /// Adds a [`DeviceConfig`]
970 ///
971 /// A device is defined by its `label`, the `url` to connect to and the chosen `tls_security`
972 /// for the connection.
973 ///
974 /// # Errors
975 ///
976 /// Returns an [`Error::DeviceExists`] if a [`DeviceConfig`] with the same `label` exists
977 /// already.
978 ///
979 /// # Examples
980 ///
981 /// ```
982 /// use nethsm::ConnectionSecurity;
983 /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
984 /// # fn main() -> testresult::TestResult {
985 /// # let config = Config::new(
986 /// # ConfigSettings::new(
987 /// # "my_app".to_string(),
988 /// # ConfigInteractivity::NonInteractive,
989 /// # None,
990 /// # ),
991 /// # Some(&testdir::testdir!().join("my_app_add_device.conf")),
992 /// # )?;
993 ///
994 /// config.add_device(
995 /// "device1".to_string(),
996 /// "https://example.org/api/v1".parse()?,
997 /// ConnectionSecurity::Unsafe,
998 /// )?;
999 ///
1000 /// // adding the same device again leads to error
1001 /// assert!(
1002 /// config
1003 /// .add_device(
1004 /// "device1".to_string(),
1005 /// "https://example.org/api/v1".parse()?,
1006 /// ConnectionSecurity::Unsafe,
1007 /// )
1008 /// .is_err()
1009 /// );
1010 /// # Ok(())
1011 /// # }
1012 /// ```
1013 pub fn add_device(
1014 &self,
1015 label: String,
1016 url: Url,
1017 tls_security: ConnectionSecurity,
1018 ) -> Result<(), Error> {
1019 if let Entry::Vacant(entry) = self.devices.borrow_mut().entry(label.clone()) {
1020 entry.insert(DeviceConfig::new(
1021 Connection::new(url, tls_security),
1022 vec![],
1023 self.config_settings.interactivity,
1024 )?);
1025 Ok(())
1026 } else {
1027 Err(Error::DeviceExists(label))
1028 }
1029 }
1030
1031 /// Deletes a [`DeviceConfig`] identified by `label`
1032 ///
1033 /// # Errors
1034 ///
1035 /// Returns an [`Error::DeviceMissing`] if no [`DeviceConfig`] with a matching `label` exists.
1036 ///
1037 /// # Examples
1038 ///
1039 /// ```
1040 /// use nethsm::ConnectionSecurity;
1041 /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1042 /// # fn main() -> testresult::TestResult {
1043 /// # let config = Config::new(
1044 /// # ConfigSettings::new(
1045 /// # "my_app".to_string(),
1046 /// # ConfigInteractivity::NonInteractive,
1047 /// # None,
1048 /// # ),
1049 /// # Some(&testdir::testdir!().join("my_app_delete_device.conf")),
1050 /// # )?;
1051 ///
1052 /// config.add_device(
1053 /// "device1".to_string(),
1054 /// "https://example.org/api/v1".parse()?,
1055 /// ConnectionSecurity::Unsafe,
1056 /// )?;
1057 ///
1058 /// config.delete_device("device1")?;
1059 ///
1060 /// // deleting a non-existent device leads to error
1061 /// assert!(config.delete_device("device1",).is_err());
1062 /// # Ok(())
1063 /// # }
1064 /// ```
1065 pub fn delete_device(&self, label: &str) -> Result<(), Error> {
1066 if self.devices.borrow_mut().remove(label).is_some() {
1067 Ok(())
1068 } else {
1069 Err(Error::DeviceMissing(label.to_string()))
1070 }
1071 }
1072
1073 /// Returns a single [`DeviceConfig`] from the [`Config`] based on an optional `label`
1074 ///
1075 /// If `label` is [`Some`], a specific [`DeviceConfig`] is retrieved.
1076 /// If `label` is [`None`] and only one device is defined in the config, then the
1077 /// [`DeviceConfig`] for that device is returned.
1078 ///
1079 /// # Errors
1080 ///
1081 /// Returns an [`Error::DeviceMissing`] if `label` is [`Some`] but it can not be found in the
1082 /// [`Config`].
1083 /// Returns an [`Error::NoDevice`], if `label` is [`None`] but the [`Config`] has no
1084 /// devices.
1085 /// Returns an [`Error::NoDevice`], if `label` is [`None`] and the [`Config`] has more than one
1086 /// device.
1087 ///
1088 /// # Examples
1089 ///
1090 /// ```
1091 /// use nethsm::ConnectionSecurity;
1092 /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1093 /// # fn main() -> testresult::TestResult {
1094 /// # let config = Config::new(
1095 /// # ConfigSettings::new(
1096 /// # "my_app".to_string(),
1097 /// # ConfigInteractivity::NonInteractive,
1098 /// # None,
1099 /// # ),
1100 /// # Some(&testdir::testdir!().join("my_app_get_device.conf")),
1101 /// # )?;
1102 ///
1103 /// config.add_device(
1104 /// "device1".to_string(),
1105 /// "https://example.org/api/v1".parse()?,
1106 /// ConnectionSecurity::Unsafe,
1107 /// )?;
1108 ///
1109 /// config.get_device(Some("device1"))?;
1110 ///
1111 /// // this fails because the device does not exist
1112 /// assert!(config.get_device(Some("device2")).is_err());
1113 ///
1114 /// config.add_device(
1115 /// "device2".to_string(),
1116 /// "https://example.org/other/api/v1".parse()?,
1117 /// ConnectionSecurity::Unsafe,
1118 /// )?;
1119 /// // this fails because there is more than one device
1120 /// assert!(config.get_device(None).is_err());
1121 ///
1122 /// config.delete_device("device1")?;
1123 /// config.delete_device("device2")?;
1124 /// // this fails because there is no device
1125 /// assert!(config.get_device(None).is_err());
1126 /// # Ok(())
1127 /// # }
1128 /// ```
1129 pub fn get_device(&self, label: Option<&str>) -> Result<DeviceConfig, Error> {
1130 if let Some(label) = label {
1131 if let Some(device_config) = self.devices.borrow().get(label) {
1132 Ok(device_config.clone())
1133 } else {
1134 Err(Error::DeviceMissing(label.to_string()))
1135 }
1136 } else {
1137 match self.devices.borrow().len() {
1138 0 => Err(Error::NoDevice),
1139 1 => Ok(self
1140 .devices
1141 .borrow()
1142 .values()
1143 .next()
1144 .expect("there should be one")
1145 .to_owned()),
1146 _ => Err(Error::MoreThanOneDevice),
1147 }
1148 }
1149 }
1150
1151 /// Returns a single [`DeviceConfig`] label from the [`Config`]
1152 ///
1153 /// # Errors
1154 ///
1155 /// Returns an error if not exactly one [`DeviceConfig`] is present.
1156 ///
1157 /// # Examples
1158 ///
1159 /// ```
1160 /// use nethsm::ConnectionSecurity;
1161 /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1162 /// # fn main() -> testresult::TestResult {
1163 /// # let config = Config::new(
1164 /// # ConfigSettings::new(
1165 /// # "my_app".to_string(),
1166 /// # ConfigInteractivity::NonInteractive,
1167 /// # None,
1168 /// # ),
1169 /// # Some(&testdir::testdir!().join("my_app_get_single_device_label.conf")),
1170 /// # )?;
1171 ///
1172 /// config.add_device(
1173 /// "device1".to_string(),
1174 /// "https://example.org/api/v1".parse()?,
1175 /// ConnectionSecurity::Unsafe,
1176 /// )?;
1177 ///
1178 /// assert_eq!(config.get_single_device_label()?, "device1".to_string());
1179 ///
1180 /// config.add_device(
1181 /// "device2".to_string(),
1182 /// "https://example.org/other/api/v1".parse()?,
1183 /// ConnectionSecurity::Unsafe,
1184 /// )?;
1185 /// // this fails because there is more than one device
1186 /// assert!(config.get_single_device_label().is_err());
1187 ///
1188 /// config.delete_device("device1")?;
1189 /// config.delete_device("device2")?;
1190 /// // this fails because there is no device
1191 /// assert!(config.get_single_device_label().is_err());
1192 /// # Ok(())
1193 /// # }
1194 /// ```
1195 pub fn get_single_device_label(&self) -> Result<String, Error> {
1196 if self.devices.borrow().keys().len() == 1 {
1197 self.devices
1198 .borrow()
1199 .keys()
1200 .next()
1201 .map(|label| label.to_string())
1202 .ok_or(Error::NoDevice)
1203 } else {
1204 Err(Error::MoreThanOneDevice)
1205 }
1206 }
1207
1208 /// Adds new credentials for a [`DeviceConfig`]
1209 ///
1210 /// Using a `label` that identifies a [`DeviceConfig`], new credentials tracking a [`UserRole`],
1211 /// a name and optionally a passphrase are added to it.
1212 ///
1213 /// # Errors
1214 ///
1215 /// Returns an [`Error::DeviceMissing`] if the targeted [`DeviceConfig`] does not exist.
1216 /// Returns an [`Error::CredentialsExist`] if the [`ConfigCredentials`] identified by `name`
1217 /// exist already.
1218 ///
1219 /// # Examples
1220 ///
1221 /// ```
1222 /// use nethsm::{ConnectionSecurity, UserRole};
1223 /// use nethsm_config::{Config, ConfigCredentials, ConfigInteractivity, ConfigSettings};
1224 /// # fn main() -> testresult::TestResult {
1225 /// # let config = Config::new(
1226 /// # ConfigSettings::new(
1227 /// # "my_app".to_string(),
1228 /// # ConfigInteractivity::NonInteractive,
1229 /// # None,
1230 /// # ),
1231 /// # Some(&testdir::testdir!().join("my_app_add_credentials.conf")),
1232 /// # )?;
1233 ///
1234 /// // this fails because the targeted device does not yet exist
1235 /// assert!(
1236 /// config
1237 /// .add_credentials(
1238 /// "device1".to_string(),
1239 /// ConfigCredentials::new(
1240 /// UserRole::Operator,
1241 /// "user1".parse()?,
1242 /// Some("my-passphrase".to_string()),
1243 /// ),
1244 /// )
1245 /// .is_err()
1246 /// );
1247 ///
1248 /// config.add_device(
1249 /// "device1".to_string(),
1250 /// "https://example.org/api/v1".parse()?,
1251 /// ConnectionSecurity::Unsafe,
1252 /// )?;
1253 ///
1254 /// config.add_credentials(
1255 /// "device1".to_string(),
1256 /// ConfigCredentials::new(
1257 /// UserRole::Operator,
1258 /// "user1".parse()?,
1259 /// Some("my-passphrase".to_string()),
1260 /// ),
1261 /// )?;
1262 ///
1263 /// // this fails because the credentials exist already
1264 /// assert!(
1265 /// config
1266 /// .add_credentials(
1267 /// "device1".to_string(),
1268 /// ConfigCredentials::new(
1269 /// UserRole::Operator,
1270 /// "user1".parse()?,
1271 /// Some("my-passphrase".to_string()),
1272 /// ),
1273 /// )
1274 /// .is_err()
1275 /// );
1276 /// # Ok(())
1277 /// # }
1278 /// ```
1279 pub fn add_credentials(
1280 &self,
1281 label: String,
1282 credentials: ConfigCredentials,
1283 ) -> Result<(), Error> {
1284 if let Some(device) = self.devices.borrow_mut().get_mut(&label) {
1285 device.add_credentials(credentials)?
1286 } else {
1287 return Err(Error::DeviceMissing(label));
1288 }
1289
1290 Ok(())
1291 }
1292
1293 /// Deletes credentials from a [`DeviceConfig`]
1294 ///
1295 /// The `label` identifies the [`DeviceConfig`] and the `name` the name of the credentials.
1296 ///
1297 /// # Errors
1298 ///
1299 /// Returns an [`Error::DeviceMissing`] if the targeted [`DeviceConfig`] does not exist.
1300 /// Returns an [`Error::CredentialsMissing`] if the targeted [`ConfigCredentials`] do not exist.
1301 ///
1302 /// # Examples
1303 ///
1304 /// ```
1305 /// use nethsm::{ConnectionSecurity, UserRole};
1306 /// use nethsm_config::{Config, ConfigCredentials, ConfigInteractivity, ConfigSettings};
1307 /// # fn main() -> testresult::TestResult {
1308 /// # let config = Config::new(
1309 /// # ConfigSettings::new(
1310 /// # "my_app".to_string(),
1311 /// # ConfigInteractivity::NonInteractive,
1312 /// # None,
1313 /// # ),
1314 /// # Some(&testdir::testdir!().join("my_app_delete_credentials.conf")),
1315 /// # )?;
1316 ///
1317 /// // this fails because the targeted device does not yet exist
1318 /// assert!(
1319 /// config
1320 /// .delete_credentials("device1", &"user1".parse()?)
1321 /// .is_err()
1322 /// );
1323 ///
1324 /// config.add_device(
1325 /// "device1".to_string(),
1326 /// "https://example.org/api/v1".parse()?,
1327 /// ConnectionSecurity::Unsafe,
1328 /// )?;
1329 ///
1330 /// // this fails because the targeted credentials does not yet exist
1331 /// assert!(
1332 /// config
1333 /// .delete_credentials("device1", &"user1".parse()?)
1334 /// .is_err()
1335 /// );
1336 ///
1337 /// config.add_credentials(
1338 /// "device1".to_string(),
1339 /// ConfigCredentials::new(
1340 /// UserRole::Operator,
1341 /// "user1".parse()?,
1342 /// Some("my-passphrase".to_string()),
1343 /// ),
1344 /// )?;
1345 ///
1346 /// config.delete_credentials("device1", &"user1".parse()?)?;
1347 /// # Ok(())
1348 /// # }
1349 /// ```
1350 pub fn delete_credentials(&self, label: &str, name: &UserId) -> Result<(), Error> {
1351 if let Some(device) = self.devices.borrow_mut().get_mut(label) {
1352 device.delete_credentials(name)?
1353 } else {
1354 return Err(Error::DeviceMissing(label.to_string()));
1355 }
1356
1357 Ok(())
1358 }
1359
1360 /// Returns the [`ConfigSettings`] of the [`Config`]
1361 ///
1362 /// # Examples
1363 ///
1364 /// ```
1365 /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1366 /// # fn main() -> testresult::TestResult {
1367 /// let config_settings = ConfigSettings::new(
1368 /// "my_app".to_string(),
1369 /// ConfigInteractivity::NonInteractive,
1370 /// None,
1371 /// );
1372 /// let config = Config::new(
1373 /// config_settings.clone(),
1374 /// Some(&testdir::testdir!().join("my_app_get_config_settings.conf")),
1375 /// )?;
1376 ///
1377 /// println!("{:?}", config.get_config_settings());
1378 /// # assert_eq!(config.get_config_settings(), config_settings);
1379 /// # Ok(())
1380 /// # }
1381 /// ```
1382 pub fn get_config_settings(&self) -> ConfigSettings {
1383 self.config_settings.clone()
1384 }
1385
1386 /// Returns the default config file location
1387 ///
1388 /// # Errors
1389 ///
1390 /// Returns an [`Error::ConfigFileLocation`] if the config file location can not be retrieved.
1391 ///
1392 /// # Examples
1393 ///
1394 /// ```
1395 /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1396 /// # fn main() -> testresult::TestResult {
1397 /// let config = Config::new(
1398 /// ConfigSettings::new(
1399 /// "my_app".to_string(),
1400 /// ConfigInteractivity::NonInteractive,
1401 /// None,
1402 /// ),
1403 /// Some(&testdir::testdir!().join("my_app_get_default_config_file_path.conf")),
1404 /// )?;
1405 ///
1406 /// println!("{:?}", config.get_default_config_file_path()?);
1407 /// # assert_eq!(
1408 /// # config.get_default_config_file_path()?,
1409 /// # dirs::config_dir()
1410 /// # .ok_or("Platform does not support config dir")?
1411 /// # .join(config.get_config_settings().app_name())
1412 /// # .join(format!(
1413 /// # "{}.toml",
1414 /// # config.get_config_settings().config_name()
1415 /// # ))
1416 /// # );
1417 /// # Ok(())
1418 /// # }
1419 /// ```
1420 pub fn get_default_config_file_path(&self) -> Result<PathBuf, Error> {
1421 confy::get_configuration_file_path(
1422 &self.config_settings.app_name,
1423 Some(self.config_settings.config_name().0.as_str()),
1424 )
1425 .map_err(Error::ConfigFileLocation)
1426 }
1427
1428 /// Writes the configuration to file
1429 ///
1430 /// # Errors
1431 ///
1432 /// Returns an [`Error::Store`] if the configuration can not be written to file.
1433 ///
1434 /// # Examples
1435 ///
1436 /// ```
1437 /// use nethsm::ConnectionSecurity;
1438 /// use nethsm_config::{Config, ConfigInteractivity, ConfigSettings};
1439 ///
1440 /// # fn main() -> testresult::TestResult {
1441 /// let config_file = testdir::testdir!().join("my_app_store.conf");
1442 /// # let config = Config::new(
1443 /// # ConfigSettings::new(
1444 /// # "my_app".to_string(),
1445 /// # ConfigInteractivity::NonInteractive,
1446 /// # None,
1447 /// # ),
1448 /// # Some(&config_file),
1449 /// # )?;
1450 /// # config.add_device(
1451 /// # "device1".to_string(),
1452 /// # "https://example.org/api/v1".parse()?,
1453 /// # ConnectionSecurity::Unsafe,
1454 /// # )?;
1455 /// config.store(Some(&config_file))?;
1456 ///
1457 /// // this fails because we can not write the configuration to a directory
1458 /// assert!(config.store(Some(&testdir::testdir!())).is_err());
1459 /// # // remove the config file again as we otherwise influence other tests
1460 /// # std::fs::remove_file(&config_file);
1461 /// # Ok(())
1462 /// # }
1463 /// ```
1464 pub fn store(&self, path: Option<&Path>) -> Result<(), Error> {
1465 if let Some(path) = path {
1466 confy::store_path(path, self).map_err(Error::Store)
1467 } else {
1468 confy::store(&self.config_settings.app_name, "config", self).map_err(Error::Store)
1469 }
1470 }
1471}
1472
1473/// The handling of administrative secrets.
1474///
1475/// Administrative secrets may be handled in different ways (e.g. persistent or non-persistent).
1476#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1477#[serde(rename_all = "kebab-case")]
1478pub enum AdministrativeSecretHandling {
1479 /// The administrative secrets are handled in a plaintext file in a non-volatile directory.
1480 ///
1481 /// ## Warning
1482 ///
1483 /// This variant should only be used in non-production test setups, as it implies the
1484 /// persistence of unencrypted administrative secrets on a file system.
1485 Plaintext,
1486
1487 /// The administrative secrets are handled in a file encrypted using [systemd-creds] in a
1488 /// non-volatile directory.
1489 ///
1490 /// ## Warning
1491 ///
1492 /// This variant should only be used in non-production test setups, as it implies the
1493 /// persistence of (host-specific) encrypted administrative secrets on a file system, that
1494 /// could be extracted if the host is compromised.
1495 ///
1496 /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
1497 SystemdCreds,
1498
1499 /// The administrative secrets are handled using [Shamir's Secret Sharing] (SSS).
1500 ///
1501 /// This variant is the default for production use, as the administrative secrets are only ever
1502 /// exposed on a volatile filesystem for the time of their use.
1503 /// The secrets are only made available to the system as shares of a shared secret, split using
1504 /// SSS.
1505 /// This way no holder of a share is aware of the administrative secrets and the system only
1506 /// for as long as it needs to use the administrative secrets.
1507 ///
1508 /// [Shamir's Secret Sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
1509 #[default]
1510 ShamirsSecretSharing,
1511}
1512
1513/// The handling of non-administrative secrets.
1514///
1515/// Non-administrative secrets represent passphrases for (non-Administrator) NetHSM users and may be
1516/// handled in different ways (e.g. encrypted or not encrypted).
1517#[derive(
1518 Clone,
1519 Copy,
1520 Debug,
1521 Default,
1522 Deserialize,
1523 strum::Display,
1524 strum::EnumString,
1525 Eq,
1526 PartialEq,
1527 Serialize,
1528)]
1529#[serde(rename_all = "kebab-case")]
1530#[strum(serialize_all = "kebab-case")]
1531pub enum NonAdministrativeSecretHandling {
1532 /// Each non-administrative secret is handled in a plaintext file in a non-volatile
1533 /// directory.
1534 ///
1535 /// ## Warning
1536 ///
1537 /// This variant should only be used in non-production test setups, as it implies the
1538 /// persistence of unencrypted non-administrative secrets on a file system.
1539 Plaintext,
1540
1541 /// Each non-administrative secret is encrypted for a specific system user using
1542 /// [systemd-creds] and the resulting files are stored in a non-volatile directory.
1543 ///
1544 /// ## Note
1545 ///
1546 /// Although secrets are stored as encrypted strings in dedicated files, they may be extracted
1547 /// under certain circumstances:
1548 ///
1549 /// - the root account is compromised
1550 /// - decrypts and exfiltrates _all_ secrets
1551 /// - the secret is not encrypted using a [TPM] and the file
1552 /// `/var/lib/systemd/credential.secret` as well as _any_ encrypted secret is exfiltrated
1553 /// - a specific user is compromised, decrypts and exfiltrates its own ssecret
1554 ///
1555 /// It is therefore crucial to follow common best-practices:
1556 ///
1557 /// - rely on a [TPM] for encrypting secrets, so that files become host-specific
1558 /// - heavily guard access to all users, especially root
1559 ///
1560 /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
1561 /// [TPM]: https://en.wikipedia.org/wiki/Trusted_Platform_Module
1562 #[default]
1563 SystemdCreds,
1564}
1565
1566/// A configuration for parallel use of connections with a set of system and NetHSM users.
1567///
1568/// This configuration type is meant to be used in a read-only fashion and does not support tracking
1569/// the passphrases for users.
1570/// As such, it is useful for tools, that create system users, as well as NetHSM users and keys
1571/// according to it.
1572///
1573/// Various mappings of system and [`NetHsm`] users exist, that are defined by the variants of
1574/// [`UserMapping`].
1575///
1576/// Some system users require providing SSH authorized key(s), while others do not allow that at
1577/// all.
1578/// NetHSM users can be added in namespaces, or system-wide, depending on their use-case.
1579/// System and NetHSM users must be unique.
1580///
1581/// Key IDs must be unique per namespace or system-wide (depending on where they are used).
1582/// Tags, used to provide access to keys for NetHSM users must be unique per namespace or
1583/// system-wide (depending on in which scope the user and key are used)
1584///
1585/// # Examples
1586///
1587/// The below example provides a fully functional TOML configuration, outlining all available
1588/// functionalities.
1589///
1590/// ```
1591/// # use std::io::Write;
1592/// #
1593/// # use nethsm_config::{ConfigInteractivity, ConfigName, ConfigSettings, HermeticParallelConfig};
1594/// #
1595/// # fn main() -> testresult::TestResult {
1596/// # let config_file = testdir::testdir!().join("basic_parallel_config_example.conf");
1597/// # {
1598/// let config_string = r#"
1599/// ## A non-negative integer, that describes the iteration of the configuration.
1600/// ## The iteration should only ever be increased between changes to the config and only under the circumstance,
1601/// ## that user mappings are removed and should also be removed from the state of the system making use of this
1602/// ## configuration.
1603/// ## Applications reading the configuration are thereby enabled to compare existing state on the system with the
1604/// ## current iteration and remove user mappings and accompanying data accordingly.
1605/// iteration = 1
1606///
1607/// ## The handling of administrative secrets on the system.
1608/// ## One of:
1609/// ## - "shamirs-secret-sharing": Administrative secrets are never persisted on the system and only provided as shares of a shared secret.
1610/// ## - "systemd-creds": Administrative secrets are persisted on the system as host-specific files, encrypted using systemd-creds (only for testing).
1611/// ## - "plaintext": Administrative secrets are persisted on the system in unencrypted plaintext files (only for testing).
1612/// admin_secret_handling = "shamirs-secret-sharing"
1613///
1614/// ## The handling of non-administrative secrets on the system.
1615/// ## One of:
1616/// ## - "systemd-creds": Non-administrative secrets are persisted on the system as host-specific files, encrypted using systemd-creds (the default).
1617/// ## - "plaintext": Non-administrative secrets are persisted on the system in unencrypted plaintext files (only for testing).
1618/// non_admin_secret_handling = "systemd-creds"
1619///
1620/// [[connections]]
1621/// url = "https://localhost:8443/api/v1/"
1622/// tls_security = "Unsafe"
1623///
1624/// ## The NetHSM user "admin" is a system-wide Administrator
1625/// [[users]]
1626/// nethsm_only_admin = "admin"
1627///
1628/// ## The SSH-accessible system user "ssh-backup1" is used in conjunction with
1629/// ## the NetHSM user "backup1" (system-wide Backup)
1630/// [[users]]
1631///
1632/// [users.system_nethsm_backup]
1633/// nethsm_user = "backup1"
1634/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
1635/// system_user = "ssh-backup1"
1636///
1637/// ## The SSH-accessible system user "ssh-metrics1" is used with several NetHSM users:
1638/// ## - "metrics1" (system-wide Metrics)
1639/// ## - "keymetrics1" (system-wide Operator)
1640/// ## - "ns1~keymetrics1" (namespace Operator)
1641/// [[users]]
1642///
1643/// [users.system_nethsm_metrics]
1644/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
1645/// system_user = "ssh-metrics1"
1646///
1647/// [users.system_nethsm_metrics.nethsm_users]
1648/// metrics_user = "metrics1"
1649/// operator_users = ["keymetrics1", "ns1~keymetrics1"]
1650///
1651/// ## The SSH-accessible system user "ssh-operator1" is used in conjunction with
1652/// ## the NetHSM user "operator1" (system-wide Operator).
1653/// ## User "operator1" shares tag "tag1" with key "key1" and can therefore use it
1654/// ## (for OpenPGP signing).
1655/// [[users]]
1656///
1657/// [users.system_nethsm_operator_signing]
1658/// nethsm_user = "operator1"
1659/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
1660/// system_user = "ssh-operator1"
1661/// tag = "tag1"
1662///
1663/// [users.system_nethsm_operator_signing.nethsm_key_setup]
1664/// key_id = "key1"
1665/// key_type = "Curve25519"
1666/// key_mechanisms = ["EdDsaSignature"]
1667/// signature_type = "EdDsa"
1668///
1669/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1670/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1671/// version = "4"
1672///
1673/// ## The SSH-accessible system user "ssh-operator2" is used in conjunction with
1674/// ## the NetHSM user "operator2" (system-wide Operator).
1675/// ## User "operator2" shares tag "tag2" with key "key2" and can therefore use it
1676/// ## (for OpenPGP signing).
1677/// [[users]]
1678///
1679/// [users.system_nethsm_operator_signing]
1680/// nethsm_user = "operator2"
1681/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
1682/// system_user = "ssh-operator2"
1683/// tag = "tag2"
1684///
1685/// [users.system_nethsm_operator_signing.nethsm_key_setup]
1686/// key_id = "key2"
1687/// key_type = "Curve25519"
1688/// key_mechanisms = ["EdDsaSignature"]
1689/// signature_type = "EdDsa"
1690///
1691/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1692/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1693/// version = "4"
1694///
1695/// ## The NetHSM user "ns1~admin" is a namespace Administrator
1696/// [[users]]
1697/// nethsm_only_admin = "ns1~admin"
1698///
1699/// ## The SSH-accessible system user "ns1-ssh-operator1" is used in conjunction with
1700/// ## the NetHSM user "ns1~operator1" (namespace Operator).
1701/// ## User "ns1~operator1" shares tag "tag1" with key "key1" and can therefore use it
1702/// ## in its namespace (for OpenPGP signing).
1703/// [[users]]
1704///
1705/// [users.system_nethsm_operator_signing]
1706/// nethsm_user = "ns1~operator1"
1707/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host"
1708/// system_user = "ns1-ssh-operator1"
1709/// tag = "tag1"
1710///
1711/// [users.system_nethsm_operator_signing.nethsm_key_setup]
1712/// key_id = "key1"
1713/// key_type = "Curve25519"
1714/// key_mechanisms = ["EdDsaSignature"]
1715/// signature_type = "EdDsa"
1716///
1717/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1718/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1719/// version = "4"
1720///
1721/// ## The SSH-accessible system user "ns1-ssh-operator2" is used in conjunction with
1722/// ## the NetHSM user "ns2~operator1" (namespace Operator).
1723/// ## User "ns1~operator2" shares tag "tag2" with key "key1" and can therefore use it
1724/// ## in its namespace (for OpenPGP signing).
1725/// [[users]]
1726///
1727/// [users.system_nethsm_operator_signing]
1728/// nethsm_user = "ns1~operator2"
1729/// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIYA+bfMBThUP5lKbMFEHiytmcCPhpkGrB/85n0mAN user@host"
1730/// system_user = "ns1-ssh-operator2"
1731/// tag = "tag2"
1732///
1733/// [users.system_nethsm_operator_signing.nethsm_key_setup]
1734/// key_id = "key2"
1735/// key_type = "Curve25519"
1736/// key_mechanisms = ["EdDsaSignature"]
1737/// signature_type = "EdDsa"
1738///
1739/// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1740/// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1741/// version = "4"
1742///
1743/// ## The hermetic system user "local-metrics1" is used with several NetHSM users:
1744/// ## - "metrics2" (system-wide Metrics)
1745/// ## - "keymetrics2" (system-wide Operator)
1746/// ## - "ns1~keymetrics2" (namespace Operator)
1747/// [[users]]
1748///
1749/// [users.hermetic_system_nethsm_metrics]
1750/// system_user = "local-metrics1"
1751///
1752/// [users.hermetic_system_nethsm_metrics.nethsm_users]
1753/// metrics_user = "metrics2"
1754/// operator_users = ["keymetrics2", "ns1~keymetrics2"]
1755///
1756/// ## The SSH-accessible system user "ssh-share-down" is used for the
1757/// ## download of shares of a shared secret (divided by Shamir's Secret Sharing).
1758/// [[users]]
1759///
1760/// [users.system_only_share_download]
1761/// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
1762/// system_user = "ssh-share-down"
1763///
1764/// ## The SSH-accessible system user "ssh-share-up" is used for the
1765/// ## upload of shares of a shared secret (divided by Shamir's Secret Sharing).
1766/// [[users]]
1767///
1768/// [users.system_only_share_upload]
1769/// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
1770/// system_user = "ssh-share-up"
1771///
1772/// ## The SSH-accessible system user "ssh-wireguard-down" is used for the
1773/// ## download of WireGuard configuration, used on the host.
1774/// [[users]]
1775///
1776/// [users.system_only_wireguard_download]
1777/// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"]
1778/// system_user = "ssh-wireguard-down"
1779/// "#;
1780/// #
1781/// # let mut buffer = std::fs::File::create(&config_file)?;
1782/// # buffer.write_all(config_string.as_bytes())?;
1783/// # }
1784/// # HermeticParallelConfig::new_from_file(
1785/// # ConfigSettings::new(
1786/// # "my_app".to_string(),
1787/// # ConfigInteractivity::NonInteractive,
1788/// # None,
1789/// # ),
1790/// # Some(&config_file),
1791/// # )?;
1792/// # Ok(())
1793/// # }
1794/// ```
1795#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1796pub struct HermeticParallelConfig {
1797 iteration: u32,
1798 admin_secret_handling: AdministrativeSecretHandling,
1799 non_admin_secret_handling: NonAdministrativeSecretHandling,
1800 connections: HashSet<Connection>,
1801 users: HashSet<UserMapping>,
1802 #[serde(skip)]
1803 settings: ConfigSettings,
1804}
1805
1806impl HermeticParallelConfig {
1807 /// Creates a new [`HermeticParallelConfig`] from a configuration file.
1808 ///
1809 /// # Errors
1810 ///
1811 /// Returns an error if the configuration file can not be loaded.
1812 ///
1813 /// # Examples
1814 ///
1815 /// ```
1816 /// # use std::io::Write;
1817 ///
1818 /// use nethsm_config::{ConfigInteractivity, ConfigName, ConfigSettings, HermeticParallelConfig};
1819 ///
1820 /// # fn main() -> testresult::TestResult {
1821 /// let config_file = testdir::testdir!().join("basic_parallel_config_new.conf");
1822 /// {
1823 /// #[rustfmt::skip]
1824 /// let config_string = r#"
1825 /// iteration = 1
1826 /// admin_secret_handling = "shamirs-secret-sharing"
1827 /// non_admin_secret_handling = "systemd-creds"
1828 /// [[connections]]
1829 /// url = "https://localhost:8443/api/v1/"
1830 /// tls_security = "Unsafe"
1831 ///
1832 /// [[users]]
1833 /// nethsm_only_admin = "admin"
1834 ///
1835 /// [[users]]
1836 /// [users.system_nethsm_backup]
1837 /// nethsm_user = "backup1"
1838 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
1839 /// system_user = "ssh-backup1"
1840 ///
1841 /// [[users]]
1842 ///
1843 /// [users.system_nethsm_metrics]
1844 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
1845 /// system_user = "ssh-metrics1"
1846 ///
1847 /// [users.system_nethsm_metrics.nethsm_users]
1848 /// metrics_user = "metrics1"
1849 /// operator_users = ["operator1metrics1"]
1850 ///
1851 /// [[users]]
1852 /// [users.system_nethsm_operator_signing]
1853 /// nethsm_user = "operator1"
1854 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
1855 /// system_user = "ssh-operator1"
1856 /// tag = "tag1"
1857 ///
1858 /// [users.system_nethsm_operator_signing.nethsm_key_setup]
1859 /// key_id = "key1"
1860 /// key_type = "Curve25519"
1861 /// key_mechanisms = ["EdDsaSignature"]
1862 /// signature_type = "EdDsa"
1863 ///
1864 /// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1865 /// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1866 /// version = "4"
1867 ///
1868 /// [[users]]
1869 /// [users.system_nethsm_operator_signing]
1870 /// nethsm_user = "operator2"
1871 /// ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
1872 /// system_user = "ssh-operator2"
1873 /// tag = "tag2"
1874 ///
1875 /// [users.system_nethsm_operator_signing.nethsm_key_setup]
1876 /// key_id = "key2"
1877 /// key_type = "Curve25519"
1878 /// key_mechanisms = ["EdDsaSignature"]
1879 /// signature_type = "EdDsa"
1880 ///
1881 /// [users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
1882 /// user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
1883 /// version = "4"
1884 ///
1885 /// [[users]]
1886 ///
1887 /// [users.hermetic_system_nethsm_metrics]
1888 /// system_user = "local-metrics1"
1889 ///
1890 /// [users.hermetic_system_nethsm_metrics.nethsm_users]
1891 /// metrics_user = "metrics2"
1892 /// operator_users = ["operator2metrics1"]
1893 ///
1894 /// [[users]]
1895 /// [users.system_only_share_download]
1896 /// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
1897 /// system_user = "ssh-share-down"
1898 ///
1899 /// [[users]]
1900 /// [users.system_only_share_upload]
1901 /// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
1902 /// system_user = "ssh-share-up"
1903 ///
1904 /// [[users]]
1905 /// [users.system_only_wireguard_download]
1906 /// ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"]
1907 /// system_user = "ssh-wireguard-down"
1908 /// "#;
1909 /// let mut buffer = std::fs::File::create(&config_file)?;
1910 /// buffer.write_all(config_string.as_bytes())?;
1911 /// }
1912 /// HermeticParallelConfig::new_from_file(
1913 /// ConfigSettings::new(
1914 /// "my_app".to_string(),
1915 /// ConfigInteractivity::NonInteractive,
1916 /// None,
1917 /// ),
1918 /// Some(&config_file),
1919 /// )?;
1920 /// # Ok(())
1921 /// # }
1922 /// ```
1923 pub fn new_from_file(
1924 config_settings: ConfigSettings,
1925 path: Option<&Path>,
1926 ) -> Result<Self, Error> {
1927 let mut config: HermeticParallelConfig = if let Some(path) = path {
1928 confy::load_path(path).map_err(|error| Error::Load {
1929 description: if let Some(error) = error.source() {
1930 error.to_string()
1931 } else {
1932 "".to_string()
1933 },
1934 source: error,
1935 })?
1936 } else {
1937 confy::load(
1938 &config_settings.app_name,
1939 Some(config_settings.config_name.0.as_str()),
1940 )
1941 .map_err(|error| Error::Load {
1942 description: if let Some(error) = error.source() {
1943 error.to_string()
1944 } else {
1945 "".to_string()
1946 },
1947 source: error,
1948 })?
1949 };
1950 config.settings = config_settings;
1951 config.validate()?;
1952
1953 Ok(config)
1954 }
1955
1956 /// Creates a new [`HermeticParallelConfig`].
1957 ///
1958 /// # Errors
1959 ///
1960 /// Returns an error if the configuration file can not be loaded.
1961 ///
1962 /// # Examples
1963 ///
1964 /// ```
1965 /// use std::collections::HashSet;
1966 ///
1967 /// use nethsm::{Connection, UserRole};
1968 /// use nethsm_config::{
1969 /// AdministrativeSecretHandling,
1970 /// AuthorizedKeyEntryList,
1971 /// ConfigCredentials,
1972 /// ConfigInteractivity,
1973 /// ConfigName,
1974 /// ConfigSettings,
1975 /// HermeticParallelConfig,
1976 /// NonAdministrativeSecretHandling,
1977 /// UserMapping,
1978 /// };
1979 ///
1980 /// # fn main() -> testresult::TestResult {
1981 /// HermeticParallelConfig::new(
1982 /// ConfigSettings::new(
1983 /// "my_app".to_string(),
1984 /// ConfigInteractivity::NonInteractive,
1985 /// None,
1986 /// ),
1987 /// 1,
1988 /// AdministrativeSecretHandling::ShamirsSecretSharing,
1989 /// NonAdministrativeSecretHandling::SystemdCreds,
1990 /// HashSet::from([Connection::new(
1991 /// "https://localhost:8443/api/v1/".parse()?,
1992 /// "Unsafe".parse()?,
1993 /// )]),
1994 /// HashSet::from([
1995 /// UserMapping::NetHsmOnlyAdmin("admin".parse()?),
1996 /// UserMapping::SystemOnlyShareDownload {
1997 /// system_user: "ssh-share-down".parse()?,
1998 /// ssh_authorized_keys: AuthorizedKeyEntryList::new(vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?])?,
1999 /// },
2000 /// UserMapping::SystemOnlyShareUpload {
2001 /// system_user: "ssh-share-up".parse()?,
2002 /// ssh_authorized_keys: AuthorizedKeyEntryList::new(vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?])?,
2003 /// }]),
2004 /// )?;
2005 /// # Ok(())
2006 /// # }
2007 /// ```
2008 pub fn new(
2009 config_settings: ConfigSettings,
2010 iteration: u32,
2011 admin_secret_handling: AdministrativeSecretHandling,
2012 non_admin_secret_handling: NonAdministrativeSecretHandling,
2013 connections: HashSet<Connection>,
2014 users: HashSet<UserMapping>,
2015 ) -> Result<Self, Error> {
2016 let config = Self {
2017 iteration,
2018 admin_secret_handling,
2019 non_admin_secret_handling,
2020 connections,
2021 users,
2022 settings: config_settings,
2023 };
2024 config.validate()?;
2025 Ok(config)
2026 }
2027
2028 /// Writes a [`HermeticParallelConfig`] to file.
2029 ///
2030 /// # Errors
2031 ///
2032 /// Returns an error if the configuration file can not be written.
2033 ///
2034 /// # Examples
2035 ///
2036 /// ```
2037 /// use std::collections::HashSet;
2038 ///
2039 /// use nethsm::{Connection,CryptographicKeyContext, OpenPgpUserIdList, SigningKeySetup, UserRole};
2040 /// use nethsm_config::{
2041 /// AuthorizedKeyEntryList,
2042 /// AdministrativeSecretHandling,
2043 /// ConfigCredentials,
2044 /// ConfigInteractivity,
2045 /// ConfigName,
2046 /// ConfigSettings,
2047 /// HermeticParallelConfig,
2048 /// NetHsmMetricsUsers,
2049 /// NonAdministrativeSecretHandling,
2050 /// UserMapping,
2051 /// };
2052 ///
2053 /// # fn main() -> testresult::TestResult {
2054 /// let config = HermeticParallelConfig::new(
2055 /// ConfigSettings::new(
2056 /// "my_app".to_string(),
2057 /// ConfigInteractivity::NonInteractive,
2058 /// None,
2059 /// ),
2060 /// 1,
2061 /// AdministrativeSecretHandling::ShamirsSecretSharing,
2062 /// NonAdministrativeSecretHandling::SystemdCreds,
2063 /// HashSet::from([Connection::new(
2064 /// "https://localhost:8443/api/v1/".parse()?,
2065 /// "Unsafe".parse()?,
2066 /// )]),
2067 /// HashSet::from([UserMapping::NetHsmOnlyAdmin("admin".parse()?),
2068 /// UserMapping::SystemNetHsmBackup {
2069 /// nethsm_user: "backup1".parse()?,
2070 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2071 /// system_user: "ssh-backup1".parse()?,
2072 /// },
2073 /// UserMapping::SystemNetHsmMetrics {
2074 /// nethsm_users: NetHsmMetricsUsers::new("metrics1".parse()?, vec!["operator2metrics1".parse()?])?,
2075 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIioJ9uvAxUPunFh89T+ENo7OQerqHE8SQ+2v4VWbfUZ user@host".parse()?,
2076 /// system_user: "ssh-metrics1".parse()?,
2077 /// },
2078 /// UserMapping::SystemNetHsmOperatorSigning {
2079 /// nethsm_user: "operator1".parse()?,
2080 /// nethsm_key_setup: SigningKeySetup::new(
2081 /// "key1".parse()?,
2082 /// "Curve25519".parse()?,
2083 /// vec!["EdDsaSignature".parse()?],
2084 /// None,
2085 /// "EdDsa".parse()?,
2086 /// CryptographicKeyContext::OpenPgp{
2087 /// user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
2088 /// version: "4".parse()?,
2089 /// })?,
2090 /// ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
2091 /// system_user: "ssh-operator1".parse()?,
2092 /// tag: "tag1".to_string(),
2093 /// },
2094 /// UserMapping::HermeticSystemNetHsmMetrics {
2095 /// nethsm_users: NetHsmMetricsUsers::new("metrics2".parse()?, vec!["operator1metrics1".parse()?])?,
2096 /// system_user: "local-metrics1".parse()?,
2097 /// },
2098 /// UserMapping::SystemOnlyShareDownload {
2099 /// system_user: "ssh-share-down".parse()?,
2100 /// ssh_authorized_keys: AuthorizedKeyEntryList::new(
2101 /// vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?],
2102 /// )?,
2103 /// },
2104 /// UserMapping::SystemOnlyShareUpload {
2105 /// system_user: "ssh-share-up".parse()?,
2106 /// ssh_authorized_keys: AuthorizedKeyEntryList::new(
2107 /// vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?],
2108 /// )?,
2109 /// },
2110 /// UserMapping::SystemOnlyWireGuardDownload {
2111 /// system_user: "ssh-wireguard-down".parse()?,
2112 /// ssh_authorized_keys: AuthorizedKeyEntryList::new(
2113 /// vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?],
2114 /// )?,
2115 /// },
2116 /// ]),
2117 /// )?;
2118 ///
2119 /// let config_file = testdir::testdir!().join("basic_parallel_config_store.conf");
2120 /// config.store(Some(&config_file))?;
2121 /// # println!("{}", std::fs::read_to_string(&config_file)?);
2122 /// # Ok(())
2123 /// # }
2124 /// ```
2125 pub fn store(&self, path: Option<&Path>) -> Result<(), Error> {
2126 if let Some(path) = path {
2127 confy::store_path(path, self).map_err(Error::Store)
2128 } else {
2129 confy::store(&self.settings.app_name, "config", self).map_err(Error::Store)
2130 }
2131 }
2132
2133 /// Returns an Iterator over the available [`Connection`]s.
2134 pub fn iter_connections(&self) -> impl Iterator<Item = &Connection> {
2135 self.connections.iter()
2136 }
2137
2138 /// Returns an Iterator over the available [`UserMapping`]s.
2139 pub fn iter_user_mappings(&self) -> impl Iterator<Item = &UserMapping> {
2140 self.users.iter()
2141 }
2142
2143 /// Returns the [`AdministrativeSecretHandling`].
2144 pub fn get_administrative_secret_handling(&self) -> AdministrativeSecretHandling {
2145 self.admin_secret_handling
2146 }
2147
2148 /// Returns the [`NonAdministrativeSecretHandling`].
2149 pub fn get_non_administrative_secret_handling(&self) -> NonAdministrativeSecretHandling {
2150 self.non_admin_secret_handling
2151 }
2152
2153 /// Returns an [`ExtendedUserMapping`] for a system user of `name` if it exists.
2154 ///
2155 /// # Errors
2156 ///
2157 /// Returns an error if no [`UserMapping`] with a [`SystemUserId`] matching `name` is found.
2158 pub fn get_extended_mapping_for_user(&self, name: &str) -> Result<ExtendedUserMapping, Error> {
2159 for user_mapping in self.users.iter() {
2160 if user_mapping
2161 .get_system_user()
2162 .is_some_and(|system_user| system_user.as_ref() == name)
2163 {
2164 return Ok(ExtendedUserMapping::new(
2165 self.admin_secret_handling,
2166 self.non_admin_secret_handling,
2167 self.connections.clone(),
2168 user_mapping.clone(),
2169 ));
2170 }
2171 }
2172 Err(Error::NoMatchingMappingForSystemUser {
2173 name: name.to_string(),
2174 })
2175 }
2176
2177 /// Validates the components of the [`HermeticParallelConfig`].
2178 fn validate(&self) -> Result<(), Error> {
2179 // ensure there are no duplicate system users
2180 {
2181 let mut system_users = HashSet::new();
2182 for system_user_id in self
2183 .users
2184 .iter()
2185 .filter_map(|mapping| mapping.get_system_user())
2186 {
2187 if !system_users.insert(system_user_id.clone()) {
2188 return Err(Error::DuplicateSystemUserId {
2189 system_user_id: system_user_id.clone(),
2190 });
2191 }
2192 }
2193 }
2194
2195 // ensure there are no duplicate NetHsm users
2196 {
2197 let mut nethsm_users = HashSet::new();
2198 for nethsm_user_id in self
2199 .users
2200 .iter()
2201 .flat_map(|mapping| mapping.get_nethsm_users())
2202 {
2203 if !nethsm_users.insert(nethsm_user_id.clone()) {
2204 return Err(Error::DuplicateNetHsmUserId {
2205 nethsm_user_id: nethsm_user_id.clone(),
2206 });
2207 }
2208 }
2209 }
2210
2211 // ensure that there is at least one system-wide administrator
2212 if self
2213 .users
2214 .iter()
2215 .filter_map(|mapping| {
2216 if let UserMapping::NetHsmOnlyAdmin(user_id) = mapping {
2217 if !user_id.is_namespaced() {
2218 Some(user_id)
2219 } else {
2220 None
2221 }
2222 } else {
2223 None
2224 }
2225 })
2226 .next()
2227 .is_none()
2228 {
2229 return Err(Error::MissingAdministrator);
2230 }
2231
2232 // ensure that there is an Administrator in each used namespace
2233 {
2234 // namespaces for all users, that are not in the Administrator role
2235 let namespaces_users = self
2236 .users
2237 .iter()
2238 .filter(|mapping| !matches!(mapping, UserMapping::NetHsmOnlyAdmin(_)))
2239 .flat_map(|mapping| mapping.get_namespaces())
2240 .collect::<HashSet<String>>();
2241 // namespaces for all users, that are in the Administrator role
2242 let namespaces_admins = self
2243 .users
2244 .iter()
2245 .filter(|mapping| matches!(mapping, UserMapping::NetHsmOnlyAdmin(_)))
2246 .flat_map(|mapping| mapping.get_namespaces())
2247 .collect::<HashSet<String>>();
2248
2249 let namespaces: Vec<String> = namespaces_users
2250 .difference(&namespaces_admins)
2251 .cloned()
2252 .collect();
2253 if !namespaces.is_empty() {
2254 return Err(Error::MissingNamespaceAdministrators { namespaces });
2255 }
2256 }
2257
2258 if self.admin_secret_handling == AdministrativeSecretHandling::ShamirsSecretSharing {
2259 // ensure there is at least one system user for downloading shares of a shared
2260 // secret
2261 if !self
2262 .users
2263 .iter()
2264 .any(|mapping| matches!(mapping, UserMapping::SystemOnlyShareDownload { .. }))
2265 {
2266 return Err(Error::MissingShareDownloadUser);
2267 }
2268
2269 // ensure there is at least one system user for uploading shares of a shared secret
2270 if !self
2271 .users
2272 .iter()
2273 .any(|mapping| matches!(mapping, UserMapping::SystemOnlyShareUpload { .. }))
2274 {
2275 return Err(Error::MissingShareUploadUser);
2276 }
2277 } else {
2278 // ensure there is no system user setup for uploading or downloading of shares of a
2279 // shared secret
2280 let share_users: Vec<SystemUserId> = self
2281 .users
2282 .iter()
2283 .filter_map(|mapping| match mapping {
2284 UserMapping::SystemOnlyShareUpload {
2285 system_user,
2286 ssh_authorized_keys: _,
2287 }
2288 | UserMapping::SystemOnlyShareDownload {
2289 system_user,
2290 ssh_authorized_keys: _,
2291 } => Some(system_user.clone()),
2292 _ => None,
2293 })
2294 .collect();
2295 if !share_users.is_empty() {
2296 return Err(Error::NoSssButShareUsers { share_users });
2297 }
2298 }
2299
2300 // ensure there are no duplicate authorized SSH keys in the set of uploading shareholders
2301 // and the rest (minus downloading shareholders)
2302 {
2303 let mut ssh_authorized_keys = HashSet::new();
2304 for ssh_authorized_key in self
2305 .users
2306 .iter()
2307 .filter(|mapping| {
2308 !matches!(
2309 mapping,
2310 UserMapping::SystemOnlyShareDownload {
2311 system_user: _,
2312 ssh_authorized_keys: _,
2313 }
2314 )
2315 })
2316 .flat_map(|mapping| mapping.get_ssh_authorized_keys())
2317 // we know a valid Entry can be created from AuthorizedKeyEntry, because its
2318 // constructor ensures it, hence we discard Errors
2319 .filter_map(|authorized_key| {
2320 ssh_key::authorized_keys::Entry::try_from(&authorized_key).ok()
2321 })
2322 {
2323 if !ssh_authorized_keys.insert(ssh_authorized_key.public_key().clone()) {
2324 return Err(Error::DuplicateSshAuthorizedKey {
2325 ssh_authorized_key: ssh_authorized_key.public_key().to_string(),
2326 });
2327 }
2328 }
2329 }
2330
2331 // ensure there are no duplicate authorized SSH keys in the set of downloading shareholders
2332 // and the rest (minus uploading shareholders)
2333 {
2334 let mut ssh_authorized_keys = HashSet::new();
2335 for ssh_authorized_key in self
2336 .users
2337 .iter()
2338 .filter(|mapping| {
2339 !matches!(
2340 mapping,
2341 UserMapping::SystemOnlyShareUpload {
2342 system_user: _,
2343 ssh_authorized_keys: _,
2344 }
2345 )
2346 })
2347 .flat_map(|mapping| mapping.get_ssh_authorized_keys())
2348 // we know a valid Entry can be created from AuthorizedKeyEntry, because its
2349 // constructor ensures it, hence we discard Errors
2350 .filter_map(|authorized_key| {
2351 ssh_key::authorized_keys::Entry::try_from(&authorized_key).ok()
2352 })
2353 {
2354 if !ssh_authorized_keys.insert(ssh_authorized_key.public_key().clone()) {
2355 return Err(Error::DuplicateSshAuthorizedKey {
2356 ssh_authorized_key: ssh_authorized_key.public_key().to_string(),
2357 });
2358 }
2359 }
2360 }
2361
2362 // ensure that only one-to-one relationships between users in the Operator role and keys
2363 // exist (system-wide and per-namespace)
2364 {
2365 // ensure that KeyIds are not reused system-wide
2366 let mut set = HashSet::new();
2367 for key_id in self
2368 .users
2369 .iter()
2370 .flat_map(|mapping| mapping.get_key_ids(None))
2371 {
2372 if !set.insert(key_id.clone()) {
2373 return Err(Error::DuplicateKeyId { key_id });
2374 }
2375 }
2376
2377 // ensure that KeyIds are not reused per namespace
2378 for namespace in self
2379 .users
2380 .iter()
2381 .flat_map(|mapping| mapping.get_namespaces())
2382 {
2383 let mut set = HashSet::new();
2384 for key_id in self
2385 .users
2386 .iter()
2387 .flat_map(|mapping| mapping.get_key_ids(Some(&namespace)))
2388 {
2389 if !set.insert(key_id.clone()) {
2390 return Err(Error::DuplicateKeyIdInNamespace { key_id, namespace });
2391 }
2392 }
2393 }
2394 }
2395
2396 // ensure unique tags system-wide and per namespace
2397 {
2398 // ensure that tags are unique system-wide
2399 let mut set = HashSet::new();
2400 for tag in self.users.iter().flat_map(|mapping| mapping.get_tags(None)) {
2401 if !set.insert(tag) {
2402 return Err(Error::DuplicateTag {
2403 tag: tag.to_string(),
2404 });
2405 }
2406 }
2407
2408 // ensure that tags are unique in each namespace
2409 for namespace in self
2410 .users
2411 .iter()
2412 .flat_map(|mapping| mapping.get_namespaces())
2413 {
2414 let mut set = HashSet::new();
2415 for tag in self
2416 .users
2417 .iter()
2418 .flat_map(|mapping| mapping.get_tags(Some(&namespace)))
2419 {
2420 if !set.insert(tag) {
2421 return Err(Error::DuplicateTagInNamespace {
2422 tag: tag.to_string(),
2423 namespace,
2424 });
2425 }
2426 }
2427 }
2428 }
2429
2430 Ok(())
2431 }
2432}
2433
2434#[cfg(test)]
2435mod tests {
2436 use core::panic;
2437 use std::path::PathBuf;
2438
2439 use rstest::rstest;
2440 use testdir::testdir;
2441 use testresult::TestResult;
2442
2443 use super::*;
2444
2445 #[rstest]
2446 fn create_and_store_empty_config() -> TestResult {
2447 let config_file: PathBuf = testdir!().join("empty_config.toml");
2448 let config = Config::new(
2449 ConfigSettings::new("test".to_string(), ConfigInteractivity::Interactive, None),
2450 Some(&config_file),
2451 )?;
2452 println!("{:#?}", config);
2453 config.store(Some(&config_file))?;
2454 println!("config file:\n{}", std::fs::read_to_string(config_file)?);
2455 Ok(())
2456 }
2457
2458 #[rstest]
2459 fn roundtrip_config(
2460 #[files("basic-config*.toml")]
2461 #[base_dir = "tests/fixtures/roundtrip-config/"]
2462 config_file: PathBuf,
2463 ) -> TestResult {
2464 let output_config_file: PathBuf = testdir!().join(
2465 config_file
2466 .file_name()
2467 .expect("the input config file should have a file name"),
2468 );
2469 let config = Config::new(
2470 ConfigSettings::new("test".to_string(), ConfigInteractivity::Interactive, None),
2471 Some(&config_file),
2472 )?;
2473 config.store(Some(&output_config_file))?;
2474 assert_eq!(
2475 std::fs::read_to_string(&output_config_file)?,
2476 std::fs::read_to_string(&config_file)?
2477 );
2478
2479 Ok(())
2480 }
2481
2482 #[rstest]
2483 fn basic_parallel_config_new_from_file(
2484 #[files("basic-parallel-config-admin-*.toml")]
2485 #[base_dir = "tests/fixtures/working/"]
2486 config_file: PathBuf,
2487 ) -> TestResult {
2488 HermeticParallelConfig::new_from_file(
2489 ConfigSettings::new(
2490 "test".to_string(),
2491 ConfigInteractivity::NonInteractive,
2492 None,
2493 ),
2494 Some(&config_file),
2495 )?;
2496
2497 Ok(())
2498 }
2499
2500 #[rstest]
2501 fn basic_parallel_config_duplicate_system_user(
2502 #[files("basic-parallel-config-admin-*.toml")]
2503 #[base_dir = "tests/fixtures/duplicate-system-user/"]
2504 config_file: PathBuf,
2505 ) -> TestResult {
2506 println!("{config_file:?}");
2507 match HermeticParallelConfig::new_from_file(
2508 ConfigSettings::new(
2509 "test".to_string(),
2510 ConfigInteractivity::NonInteractive,
2511 None,
2512 ),
2513 Some(&config_file),
2514 ) {
2515 Err(Error::DuplicateSystemUserId { .. }) => Ok(()),
2516 Ok(_) => panic!("Did not trigger any Error!"),
2517 Err(error) => panic!("Did not trigger the correct Error: {:?}!", error),
2518 }
2519 }
2520
2521 #[rstest]
2522 fn basic_parallel_config_duplicate_nethsm_user(
2523 #[files("basic-parallel-config-admin-*.toml")]
2524 #[base_dir = "tests/fixtures/duplicate-nethsm-user/"]
2525 config_file: PathBuf,
2526 ) -> TestResult {
2527 if let Err(Error::DuplicateNetHsmUserId { .. }) = HermeticParallelConfig::new_from_file(
2528 ConfigSettings::new(
2529 "test".to_string(),
2530 ConfigInteractivity::NonInteractive,
2531 None,
2532 ),
2533 Some(&config_file),
2534 ) {
2535 Ok(())
2536 } else {
2537 panic!("Did not trigger the correct Error!")
2538 }
2539 }
2540
2541 #[rstest]
2542 fn basic_parallel_config_missing_administrator(
2543 #[files("basic-parallel-config-admin-*.toml")]
2544 #[base_dir = "tests/fixtures/missing-administrator/"]
2545 config_file: PathBuf,
2546 ) -> TestResult {
2547 if let Err(Error::MissingAdministrator) = HermeticParallelConfig::new_from_file(
2548 ConfigSettings::new(
2549 "test".to_string(),
2550 ConfigInteractivity::NonInteractive,
2551 None,
2552 ),
2553 Some(&config_file),
2554 ) {
2555 Ok(())
2556 } else {
2557 panic!("Did not trigger the correct Error!")
2558 }
2559 }
2560
2561 #[rstest]
2562 fn basic_parallel_config_missing_namespace_administrators(
2563 #[files("basic-parallel-config-admin-*.toml")]
2564 #[base_dir = "tests/fixtures/missing-namespace-administrator/"]
2565 config_file: PathBuf,
2566 ) -> TestResult {
2567 if let Err(Error::MissingNamespaceAdministrators { .. }) =
2568 HermeticParallelConfig::new_from_file(
2569 ConfigSettings::new(
2570 "test".to_string(),
2571 ConfigInteractivity::NonInteractive,
2572 None,
2573 ),
2574 Some(&config_file),
2575 )
2576 {
2577 Ok(())
2578 } else {
2579 panic!("Did not trigger the correct Error!")
2580 }
2581 }
2582
2583 #[rstest]
2584 fn basic_parallel_config_duplicate_authorized_keys_share_uploader(
2585 #[files("basic-parallel-config-admin-*.toml")]
2586 #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-uploader/"]
2587 config_file: PathBuf,
2588 ) -> TestResult {
2589 println!("Using configuration {:?}", config_file);
2590 let config_file_string = config_file
2591 .clone()
2592 .into_os_string()
2593 .into_string()
2594 .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2595 // when using plaintext or systemd-creds for administrative credentials, there are no share
2596 // uploaders
2597 if config_file_string.ends_with("admin-plaintext.toml")
2598 || config_file_string.ends_with("admin-systemd-creds.toml")
2599 {
2600 let _config = HermeticParallelConfig::new_from_file(
2601 ConfigSettings::new(
2602 "test".to_string(),
2603 ConfigInteractivity::NonInteractive,
2604 None,
2605 ),
2606 Some(&config_file),
2607 )?;
2608 Ok(())
2609 } else if let Err(Error::DuplicateSshAuthorizedKey { .. }) =
2610 HermeticParallelConfig::new_from_file(
2611 ConfigSettings::new(
2612 "test".to_string(),
2613 ConfigInteractivity::NonInteractive,
2614 None,
2615 ),
2616 Some(&config_file),
2617 )
2618 {
2619 Ok(())
2620 } else {
2621 panic!("Did not trigger the correct Error!")
2622 }
2623 }
2624
2625 #[rstest]
2626 fn basic_parallel_config_duplicate_authorized_keys_share_downloader(
2627 #[files("basic-parallel-config-admin-*.toml")]
2628 #[base_dir = "tests/fixtures/duplicate-authorized-keys-share-downloader/"]
2629 config_file: PathBuf,
2630 ) -> TestResult {
2631 println!("Using configuration {:?}", config_file);
2632 let config_file_string = config_file
2633 .clone()
2634 .into_os_string()
2635 .into_string()
2636 .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2637 // when using plaintext or systemd-creds for administrative credentials, there are no share
2638 // downloaders
2639 if config_file_string.ends_with("admin-plaintext.toml")
2640 || config_file_string.ends_with("admin-systemd-creds.toml")
2641 {
2642 let _config = HermeticParallelConfig::new_from_file(
2643 ConfigSettings::new(
2644 "test".to_string(),
2645 ConfigInteractivity::NonInteractive,
2646 None,
2647 ),
2648 Some(&config_file),
2649 )?;
2650 Ok(())
2651 } else if let Err(Error::DuplicateSshAuthorizedKey { .. }) =
2652 HermeticParallelConfig::new_from_file(
2653 ConfigSettings::new(
2654 "test".to_string(),
2655 ConfigInteractivity::NonInteractive,
2656 None,
2657 ),
2658 Some(&config_file),
2659 )
2660 {
2661 Ok(())
2662 } else {
2663 panic!("Did not trigger the correct Error!")
2664 }
2665 }
2666
2667 #[rstest]
2668 fn basic_parallel_config_duplicate_authorized_keys_users(
2669 #[files("basic-parallel-config-admin-*.toml")]
2670 #[base_dir = "tests/fixtures/duplicate-authorized-keys-users/"]
2671 config_file: PathBuf,
2672 ) -> TestResult {
2673 if let Err(Error::DuplicateSshAuthorizedKey { .. }) = HermeticParallelConfig::new_from_file(
2674 ConfigSettings::new(
2675 "test".to_string(),
2676 ConfigInteractivity::NonInteractive,
2677 None,
2678 ),
2679 Some(&config_file),
2680 ) {
2681 Ok(())
2682 } else {
2683 panic!("Did not trigger the correct Error!")
2684 }
2685 }
2686
2687 #[rstest]
2688 fn basic_parallel_config_missing_share_download_user(
2689 #[files("basic-parallel-config-admin-*.toml")]
2690 #[base_dir = "tests/fixtures/missing-share-download-user/"]
2691 config_file: PathBuf,
2692 ) -> TestResult {
2693 println!("Using configuration {:?}", config_file);
2694 let config_file_string = config_file
2695 .clone()
2696 .into_os_string()
2697 .into_string()
2698 .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2699 // when using plaintext or systemd-creds for administrative credentials, there are no share
2700 // downloaders
2701 if config_file_string.ends_with("admin-plaintext.toml")
2702 || config_file_string.ends_with("admin-systemd-creds.toml")
2703 {
2704 let _config = HermeticParallelConfig::new_from_file(
2705 ConfigSettings::new(
2706 "test".to_string(),
2707 ConfigInteractivity::NonInteractive,
2708 None,
2709 ),
2710 Some(&config_file),
2711 )?;
2712 Ok(())
2713 } else if let Err(Error::MissingShareDownloadUser) = HermeticParallelConfig::new_from_file(
2714 ConfigSettings::new(
2715 "test".to_string(),
2716 ConfigInteractivity::NonInteractive,
2717 None,
2718 ),
2719 Some(&config_file),
2720 ) {
2721 Ok(())
2722 } else {
2723 panic!("Did not trigger the correct Error!")
2724 }
2725 }
2726
2727 #[rstest]
2728 fn basic_parallel_config_missing_share_upload_user(
2729 #[files("basic-parallel-config-admin-*.toml")]
2730 #[base_dir = "tests/fixtures/missing-share-upload-user/"]
2731 config_file: PathBuf,
2732 ) -> TestResult {
2733 println!("Using configuration {:?}", config_file);
2734 let config_file_string = config_file
2735 .clone()
2736 .into_os_string()
2737 .into_string()
2738 .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2739 // when using plaintext or systemd-creds for administrative credentials, there are no share
2740 // downloaders
2741 if config_file_string.ends_with("admin-plaintext.toml")
2742 || config_file_string.ends_with("admin-systemd-creds.toml")
2743 {
2744 let _config = HermeticParallelConfig::new_from_file(
2745 ConfigSettings::new(
2746 "test".to_string(),
2747 ConfigInteractivity::NonInteractive,
2748 None,
2749 ),
2750 Some(&config_file),
2751 )?;
2752 Ok(())
2753 } else if let Err(Error::MissingShareUploadUser) = HermeticParallelConfig::new_from_file(
2754 ConfigSettings::new(
2755 "test".to_string(),
2756 ConfigInteractivity::NonInteractive,
2757 None,
2758 ),
2759 Some(&config_file),
2760 ) {
2761 Ok(())
2762 } else {
2763 panic!("Did not trigger the correct Error!")
2764 }
2765 }
2766
2767 #[rstest]
2768 fn basic_parallel_config_no_sss_but_shares(
2769 #[files("basic-parallel-config-admin-*.toml")]
2770 #[base_dir = "tests/fixtures/no-sss-but-shares/"]
2771 config_file: PathBuf,
2772 ) -> TestResult {
2773 println!("Using configuration {:?}", config_file);
2774 let config_file_string = config_file
2775 .clone()
2776 .into_os_string()
2777 .into_string()
2778 .map_err(|_x| format!("Can't convert {:?}", config_file))?;
2779 // when using shamir's secret sharing for administrative credentials, there ought to be
2780 // share downloaders and uploaders
2781 if config_file_string.ends_with("admin-shamirs-secret-sharing.toml") {
2782 let _config = HermeticParallelConfig::new_from_file(
2783 ConfigSettings::new(
2784 "test".to_string(),
2785 ConfigInteractivity::NonInteractive,
2786 None,
2787 ),
2788 Some(&config_file),
2789 )?;
2790 Ok(())
2791 } else if let Err(Error::NoSssButShareUsers { .. }) = HermeticParallelConfig::new_from_file(
2792 ConfigSettings::new(
2793 "test".to_string(),
2794 ConfigInteractivity::NonInteractive,
2795 None,
2796 ),
2797 Some(&config_file),
2798 ) {
2799 Ok(())
2800 } else {
2801 panic!("Did not trigger the correct Error!")
2802 }
2803 }
2804
2805 #[rstest]
2806 fn basic_parallel_config_duplicate_key_id(
2807 #[files("basic-parallel-config-admin-*.toml")]
2808 #[base_dir = "tests/fixtures/duplicate-key-id/"]
2809 config_file: PathBuf,
2810 ) -> TestResult {
2811 if let Err(Error::DuplicateKeyId { .. }) = HermeticParallelConfig::new_from_file(
2812 ConfigSettings::new(
2813 "test".to_string(),
2814 ConfigInteractivity::NonInteractive,
2815 None,
2816 ),
2817 Some(&config_file),
2818 ) {
2819 Ok(())
2820 } else {
2821 panic!("Did not trigger the correct Error!")
2822 }
2823 }
2824
2825 #[rstest]
2826 fn basic_parallel_config_duplicate_key_id_in_namespace(
2827 #[files("basic-parallel-config-admin-*.toml")]
2828 #[base_dir = "tests/fixtures/duplicate-key-id-in-namespace/"]
2829 config_file: PathBuf,
2830 ) -> TestResult {
2831 if let Err(Error::DuplicateKeyIdInNamespace { .. }) = HermeticParallelConfig::new_from_file(
2832 ConfigSettings::new(
2833 "test".to_string(),
2834 ConfigInteractivity::NonInteractive,
2835 None,
2836 ),
2837 Some(&config_file),
2838 ) {
2839 Ok(())
2840 } else {
2841 panic!("Did not trigger the correct Error!")
2842 }
2843 }
2844
2845 #[rstest]
2846 fn basic_parallel_config_duplicate_tag(
2847 #[files("basic-parallel-config-admin-*.toml")]
2848 #[base_dir = "tests/fixtures/duplicate-tag/"]
2849 config_file: PathBuf,
2850 ) -> TestResult {
2851 if let Err(Error::DuplicateTag { .. }) = HermeticParallelConfig::new_from_file(
2852 ConfigSettings::new(
2853 "test".to_string(),
2854 ConfigInteractivity::NonInteractive,
2855 None,
2856 ),
2857 Some(&config_file),
2858 ) {
2859 Ok(())
2860 } else {
2861 panic!("Did not trigger the correct Error!")
2862 }
2863 }
2864
2865 #[rstest]
2866 fn basic_parallel_config_duplicate_tag_in_namespace(
2867 #[files("basic-parallel-config-admin-*.toml")]
2868 #[base_dir = "tests/fixtures/duplicate-tag-in-namespace/"]
2869 config_file: PathBuf,
2870 ) -> TestResult {
2871 if let Err(Error::DuplicateTagInNamespace { .. }) = HermeticParallelConfig::new_from_file(
2872 ConfigSettings::new(
2873 "test".to_string(),
2874 ConfigInteractivity::NonInteractive,
2875 None,
2876 ),
2877 Some(&config_file),
2878 ) {
2879 Ok(())
2880 } else {
2881 panic!("Did not trigger the correct Error!")
2882 }
2883 }
2884}