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