signstar_config/nethsm/
config.rs

1//! [`NetHsm`] specific integration for the [`crate::config`] module.
2
3use std::collections::{BTreeSet, HashSet};
4
5use garde::Validate;
6#[cfg(doc)]
7use nethsm::NetHsm;
8use nethsm::{
9    Connection,
10    FullCredentials,
11    KeyId,
12    NamespaceId,
13    Passphrase,
14    SystemWideUserId,
15    UserId,
16    UserRole,
17};
18use serde::{Deserialize, Serialize};
19use signstar_crypto::{key::SigningKeySetup, traits::UserWithPassphrase};
20
21use crate::{
22    AuthorizedKeyEntry,
23    SystemUserId,
24    config::{
25        BackendDomainFilter,
26        BackendKeyIdFilter,
27        BackendUserIdFilter,
28        BackendUserIdKind,
29        ConfigAuthorizedKeyEntries,
30        ConfigSystemUserIds,
31        MappingAuthorizedKeyEntry,
32        MappingBackendDomain,
33        MappingBackendKeyId,
34        MappingBackendUserIds,
35        MappingBackendUserSecrets,
36        MappingSystemUserId,
37        duplicate_authorized_keys,
38        duplicate_backend_user_ids,
39        duplicate_domains,
40        duplicate_key_ids,
41        duplicate_system_user_ids,
42    },
43};
44
45/// An error that may occur when using NetHsm config objects.
46#[derive(Debug, thiserror::Error)]
47pub enum Error {
48    /// A [`UserId`] is used both for a user in the [`Metrics`][`nethsm::UserRole::Metrics`] and
49    /// [`Operator`][`nethsm::UserRole::Operator`] role.
50    #[error("The NetHSM user {metrics_user} is both in the Metrics and Operator role!")]
51    MetricsAlsoOperator {
52        /// The system-wide User ID of a NetHSM user that is both in the
53        /// [`Metrics`][`nethsm::UserRole::Metrics`] and
54        /// [`Operator`][`nethsm::UserRole::Operator`] role.
55        metrics_user: SystemWideUserId,
56    },
57
58    /// A NetHSM [`UserId`] is not found.
59    #[error("The NetHSM user {user} cannot be found")]
60    UserIdNotFound {
61        /// The name of the NetHSM user that cannot be found.
62        user: String,
63    },
64}
65
66/// A filter for retrieving information about users and keys.
67#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
68pub enum FilterUserKeys {
69    /// Consider both system-wide and namespaced users and keys.
70    All,
71
72    /// Only consider users and keys that are in a namespace.
73    Namespaced,
74
75    /// Only consider users and keys that match a specific [`NamespaceId`].
76    Namespace(NamespaceId),
77
78    /// Only consider system-wide users and keys.
79    SystemWide,
80
81    /// Only consider users and keys that match a specific tag.
82    Tag(String),
83}
84
85/// A set of users with unique [`UserId`]s, used for metrics retrieval
86///
87/// This struct tracks a user that is intended for the use in the
88/// [`Metrics`][`nethsm::UserRole::Metrics`] role and a list of users, that are intended to be used
89/// in the [`Operator`][`nethsm::UserRole::Operator`] role.
90#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
91pub struct NetHsmMetricsUsers {
92    metrics_user: SystemWideUserId,
93    operator_users: Vec<UserId>,
94}
95
96impl NetHsmMetricsUsers {
97    /// Creates a new [`NetHsmMetricsUsers`]
98    ///
99    /// # Error
100    ///
101    /// Returns an error, if the provided [`UserId`] of the `metrics_user` is duplicated in the
102    /// provided `operator_users`.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use signstar_config::NetHsmMetricsUsers;
108    ///
109    /// # fn main() -> testresult::TestResult {
110    /// NetHsmMetricsUsers::new(
111    ///     "metrics1".parse()?,
112    ///     vec!["user1".parse()?, "user2".parse()?],
113    /// )?;
114    ///
115    /// // this fails because there are duplicate UserIds
116    /// assert!(
117    ///     NetHsmMetricsUsers::new(
118    ///         "metrics1".parse()?,
119    ///         vec!["metrics1".parse()?, "user2".parse()?,],
120    ///     )
121    ///     .is_err()
122    /// );
123    /// # Ok(())
124    /// # }
125    /// ```
126    pub fn new(
127        metrics_user: SystemWideUserId,
128        operator_users: Vec<UserId>,
129    ) -> Result<Self, crate::Error> {
130        // prevent duplicate metrics and operator users
131        if operator_users.contains(metrics_user.as_ref()) {
132            return Err(Error::MetricsAlsoOperator { metrics_user }.into());
133        }
134
135        Ok(Self {
136            metrics_user,
137            operator_users,
138        })
139    }
140
141    /// Returns all tracked [`UserId`]s of the [`NetHsmMetricsUsers`]
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use nethsm::UserId;
147    /// use signstar_config::NetHsmMetricsUsers;
148    ///
149    /// # fn main() -> testresult::TestResult {
150    /// let nethsm_metrics_users = NetHsmMetricsUsers::new(
151    ///     "metrics1".parse()?,
152    ///     vec!["user1".parse()?, "user2".parse()?],
153    /// )?;
154    ///
155    /// assert_eq!(
156    ///     nethsm_metrics_users.get_users(),
157    ///     vec![
158    ///         UserId::new("metrics1".to_string())?,
159    ///         UserId::new("user1".to_string())?,
160    ///         UserId::new("user2".to_string())?
161    ///     ]
162    /// );
163    /// # Ok(())
164    /// # }
165    /// ```
166    pub fn get_users(&self) -> Vec<UserId> {
167        [
168            vec![self.metrics_user.clone().into()],
169            self.operator_users.clone(),
170        ]
171        .concat()
172    }
173
174    /// Returns all tracked [`UserId`]s and their respective [`UserRole`].
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// use nethsm::{UserId, UserRole};
180    /// use signstar_config::NetHsmMetricsUsers;
181    ///
182    /// # fn main() -> testresult::TestResult {
183    /// let nethsm_metrics_users = NetHsmMetricsUsers::new(
184    ///     "metrics1".parse()?,
185    ///     vec!["user1".parse()?, "user2".parse()?],
186    /// )?;
187    ///
188    /// assert_eq!(
189    ///     nethsm_metrics_users.get_users_and_roles(),
190    ///     vec![
191    ///         (UserId::new("metrics1".to_string())?, UserRole::Metrics),
192    ///         (UserId::new("user1".to_string())?, UserRole::Operator),
193    ///         (UserId::new("user2".to_string())?, UserRole::Operator)
194    ///     ]
195    /// );
196    /// # Ok(())
197    /// # }
198    /// ```
199    pub fn get_users_and_roles(&self) -> Vec<(UserId, UserRole)> {
200        [
201            vec![(self.metrics_user.clone().into(), UserRole::Metrics)],
202            self.operator_users
203                .iter()
204                .map(|user| (user.clone(), UserRole::Operator))
205                .collect(),
206        ]
207        .concat()
208    }
209}
210
211/// Data about a NetHSM user.
212#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
213pub struct NetHsmUserData<'a> {
214    /// The name of the user.
215    pub user: &'a UserId,
216
217    /// The role of the user.
218    pub role: UserRole,
219
220    /// The optional tag assigned to the user.
221    pub tag: Option<&'a str>,
222}
223
224/// A filter for retrieving information about users and keys.
225#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
226pub enum NetHsmUserKeysFilter {
227    /// Consider both system-wide and namespaced users and keys.
228    All,
229
230    /// Only consider users and keys that are in a namespace.
231    Namespaced,
232
233    /// Only consider system-wide users and keys.
234    SystemWide,
235}
236
237/// Data about a NetHSM signing user associated with a key.
238#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
239pub struct NetHsmUserKeyData<'a> {
240    /// The name of the user.
241    pub user: &'a UserId,
242
243    /// The key associated with the user.
244    pub key_id: &'a KeyId,
245
246    /// The setup of the key.
247    pub key_setup: &'a SigningKeySetup,
248
249    /// The tag assigned to the user and the key.
250    pub tag: &'a str,
251}
252
253/// User and data mapping between system users and NetHSM users.
254#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
255#[serde(rename_all = "snake_case")]
256pub enum NetHsmUserMapping {
257    /// A NetHsm user in the Administrator role, without a system user mapped to it.
258    Admin(UserId),
259
260    /// A system user, with SSH access, mapped to a system-wide NetHSM user in the Backup role.
261    Backup {
262        /// The name of the NetHSM user.
263        backend_user: SystemWideUserId,
264        /// The SSH public key used for connecting to the `system_user`.
265        ssh_authorized_key: AuthorizedKeyEntry,
266        /// The name of the system user.
267        system_user: SystemUserId,
268    },
269
270    /// A system user, without SSH access, mapped to a system-wide NetHSM
271    /// user in the Metrics role and one or more NetHsm users in the Operator role with
272    /// read-only access to zero or more keys.
273    HermeticMetrics {
274        /// The NetHSM users in the [`Metrics`][`UserRole::Metrics`] and
275        /// [`operator`][`UserRole::Operator`] role.
276        backend_users: NetHsmMetricsUsers,
277        /// The name of the system user.
278        system_user: SystemUserId,
279    },
280
281    /// A system user, with SSH access, mapped to a system-wide NetHSM user
282    /// in the Metrics role and `n` users in the Operator role with read-only access to zero or
283    /// more keys.
284    Metrics {
285        /// The NetHSM users in the [`Metrics`][`UserRole::Metrics`] and
286        /// [`operator`][`UserRole::Operator`] role.
287        backend_users: NetHsmMetricsUsers,
288        /// The SSH public key used for connecting to the `system_user`.
289        ssh_authorized_key: AuthorizedKeyEntry,
290        /// The name of the system user.
291        system_user: SystemUserId,
292    },
293
294    /// A system user, with SSH access, mapped to a NetHSM user in the
295    /// Operator role with access to a single signing key.
296    ///
297    /// Signing key and NetHSM user are mapped using a tag.
298    Signing {
299        /// The name of the NetHSM user.
300        backend_user: UserId,
301        /// The ID of the NetHSM key.
302        signing_key_id: KeyId,
303        /// The setup of a NetHSM key.
304        key_setup: SigningKeySetup,
305        /// The SSH public key used for connecting to the `system_user`.
306        ssh_authorized_key: AuthorizedKeyEntry,
307        /// The name of the system user.
308        system_user: SystemUserId,
309        /// The tag used for the user and the signing key on the NetHSM.
310        tag: String,
311    },
312}
313
314impl NetHsmUserMapping {
315    /// Returns the list of [`NamespaceId`]s associated with this [`NetHsmUserMapping`].
316    pub fn namespaces(&self) -> Vec<&NamespaceId> {
317        match self {
318            Self::Admin(backend_user) | Self::Signing { backend_user, .. } => {
319                if let Some(namespace) = backend_user.namespace() {
320                    vec![namespace]
321                } else {
322                    Vec::new()
323                }
324            }
325            Self::Backup { .. } => Vec::new(),
326            Self::HermeticMetrics { backend_users, .. } | Self::Metrics { backend_users, .. } => {
327                backend_users
328                    .operator_users
329                    .iter()
330                    .filter_map(|user_id| user_id.namespace())
331                    .collect::<Vec<_>>()
332            }
333        }
334    }
335
336    /// Returns the optional tag used in the [`NetHsmUserMapping`].
337    ///
338    /// # Note
339    ///
340    /// Only [`NetHsmUserMapping::Signing`] can have a tag.
341    pub fn tag(&self, namespace: Option<&NamespaceId>) -> Option<&str> {
342        match self {
343            Self::Signing {
344                backend_user, tag, ..
345            } => {
346                if namespace == backend_user.namespace() {
347                    Some(tag.as_str())
348                } else {
349                    None
350                }
351            }
352            Self::Admin(_)
353            | Self::Backup { .. }
354            | Self::HermeticMetrics { .. }
355            | Self::Metrics { .. } => None,
356        }
357    }
358
359    /// Returns the list of [`UserId`] objects associated with this [`NetHsmUserMapping`].
360    pub fn nethsm_user_ids(&self) -> Vec<UserId> {
361        match self {
362            Self::Admin(user_id) => vec![user_id.clone()],
363            Self::Backup { backend_user, .. } => vec![backend_user.as_ref().clone()],
364            Self::Metrics { backend_users, .. } | Self::HermeticMetrics { backend_users, .. } => {
365                backend_users.get_users()
366            }
367            Self::Signing { backend_user, .. } => vec![backend_user.clone()],
368        }
369    }
370
371    /// Returns the list of [`NetHsmUserData`] objects associated with this [`NetHsmUserMapping`].
372    pub fn nethsm_user_data<'a>(&'a self) -> HashSet<NetHsmUserData<'a>> {
373        match self {
374            Self::Admin(user_id) => HashSet::from_iter([NetHsmUserData {
375                user: user_id,
376                role: UserRole::Administrator,
377                tag: None,
378            }]),
379            Self::Backup { backend_user, .. } => HashSet::from_iter([NetHsmUserData {
380                user: backend_user.as_ref(),
381                role: UserRole::Backup,
382                tag: None,
383            }]),
384            Self::Metrics { backend_users, .. } | Self::HermeticMetrics { backend_users, .. } => {
385                let mut users = backend_users
386                    .operator_users
387                    .iter()
388                    .map(|user_id| NetHsmUserData {
389                        user: user_id,
390                        role: UserRole::Operator,
391                        tag: None,
392                    })
393                    .collect::<Vec<_>>();
394                users.push(NetHsmUserData {
395                    user: backend_users.metrics_user.as_ref(),
396                    role: UserRole::Metrics,
397                    tag: None,
398                });
399                HashSet::from_iter(users)
400            }
401            Self::Signing {
402                backend_user, tag, ..
403            } => HashSet::from_iter([NetHsmUserData {
404                user: backend_user,
405                role: UserRole::Operator,
406                tag: Some(tag.as_ref()),
407            }]),
408        }
409    }
410
411    /// Returns a filtered list of [`NetHsmUserKeyData`] objects from this [`NetHsmUserMapping`].
412    ///
413    /// Based on a [`NetHsmUserKeysFilter`] it is possible to target only namespaced or system-wide,
414    /// or all user mappings that have associated key configs.
415    pub fn nethsm_user_key_data<'a>(
416        &'a self,
417        filter: NetHsmUserKeysFilter,
418    ) -> Option<NetHsmUserKeyData<'a>> {
419        match self {
420            Self::Admin(_)
421            | Self::Backup { .. }
422            | Self::Metrics { .. }
423            | Self::HermeticMetrics { .. } => None,
424            Self::Signing {
425                backend_user,
426                signing_key_id,
427                key_setup,
428                tag,
429                ..
430            } => {
431                if matches!(filter, NetHsmUserKeysFilter::All)
432                    || (matches!(filter, NetHsmUserKeysFilter::Namespaced)
433                        && backend_user.is_namespaced())
434                    || (matches!(filter, NetHsmUserKeysFilter::SystemWide)
435                        && !backend_user.is_namespaced())
436                {
437                    Some(NetHsmUserKeyData {
438                        user: backend_user,
439                        key_id: signing_key_id,
440                        key_setup,
441                        tag,
442                    })
443                } else {
444                    None
445                }
446            }
447        }
448    }
449}
450
451impl MappingSystemUserId for NetHsmUserMapping {
452    fn system_user_id(&self) -> Option<&SystemUserId> {
453        match self {
454            Self::Admin(_) => None,
455            Self::Backup { system_user, .. }
456            | Self::Metrics { system_user, .. }
457            | Self::HermeticMetrics { system_user, .. }
458            | Self::Signing { system_user, .. } => Some(system_user),
459        }
460    }
461}
462
463impl MappingAuthorizedKeyEntry for NetHsmUserMapping {
464    fn authorized_key_entry(&self) -> Option<&AuthorizedKeyEntry> {
465        match self {
466            Self::Admin(_) | Self::HermeticMetrics { .. } => None,
467            Self::Backup {
468                ssh_authorized_key, ..
469            }
470            | Self::Metrics {
471                ssh_authorized_key, ..
472            }
473            | Self::Signing {
474                ssh_authorized_key, ..
475            } => Some(ssh_authorized_key),
476        }
477    }
478}
479
480impl MappingBackendUserIds for NetHsmUserMapping {
481    fn backend_user_ids(&self, filter: BackendUserIdFilter) -> Vec<String> {
482        match self {
483            Self::Admin(user_id) => {
484                if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
485                    .contains(&filter.backend_user_id_kind)
486                {
487                    Some(vec![user_id.to_string()])
488                } else {
489                    None
490                }
491            }
492            Self::Backup { backend_user, .. } => {
493                if [
494                    BackendUserIdKind::Any,
495                    BackendUserIdKind::Backup,
496                    BackendUserIdKind::NonAdmin,
497                ]
498                .contains(&filter.backend_user_id_kind)
499                {
500                    Some(vec![backend_user.to_string()])
501                } else {
502                    None
503                }
504            }
505            Self::Metrics { backend_users, .. } | Self::HermeticMetrics { backend_users, .. } => {
506                match filter.backend_user_id_kind {
507                    BackendUserIdKind::Admin
508                    | BackendUserIdKind::Backup
509                    | BackendUserIdKind::Signing => None,
510                    BackendUserIdKind::Metrics => {
511                        Some(vec![backend_users.metrics_user.to_string()])
512                    }
513                    BackendUserIdKind::NonAdmin | BackendUserIdKind::Any => Some(
514                        backend_users
515                            .get_users()
516                            .iter()
517                            .map(ToString::to_string)
518                            .collect(),
519                    ),
520                    BackendUserIdKind::Observer => Some(
521                        backend_users
522                            .operator_users
523                            .iter()
524                            .map(ToString::to_string)
525                            .collect(),
526                    ),
527                }
528            }
529            Self::Signing { backend_user, .. } => {
530                if [
531                    BackendUserIdKind::Any,
532                    BackendUserIdKind::NonAdmin,
533                    BackendUserIdKind::Signing,
534                ]
535                .contains(&filter.backend_user_id_kind)
536                {
537                    Some(vec![backend_user.to_string()])
538                } else {
539                    None
540                }
541            }
542        }
543        .unwrap_or_default()
544    }
545
546    fn backend_user_with_passphrase(
547        &self,
548        name: &str,
549        passphrase: Passphrase,
550    ) -> Result<Box<dyn UserWithPassphrase>, crate::Error> {
551        for user in self.nethsm_user_ids() {
552            if user.to_string() == name {
553                return Ok(Box::new(FullCredentials::new(user, passphrase)));
554            }
555        }
556
557        Err(Error::UserIdNotFound {
558            user: name.to_string(),
559        }
560        .into())
561    }
562
563    fn backend_users_with_new_passphrase(
564        &self,
565        filter: BackendUserIdFilter,
566    ) -> Vec<Box<dyn UserWithPassphrase>> {
567        if let Some(backend_user_ids) = match self {
568            Self::Admin(user_id) => {
569                if [BackendUserIdKind::Any, BackendUserIdKind::Admin]
570                    .contains(&filter.backend_user_id_kind)
571                {
572                    Some(vec![user_id.clone()])
573                } else {
574                    None
575                }
576            }
577            Self::Backup { backend_user, .. } => {
578                if [
579                    BackendUserIdKind::Any,
580                    BackendUserIdKind::Backup,
581                    BackendUserIdKind::NonAdmin,
582                ]
583                .contains(&filter.backend_user_id_kind)
584                {
585                    Some(vec![UserId::from(backend_user.clone())])
586                } else {
587                    None
588                }
589            }
590            Self::Metrics { backend_users, .. } | Self::HermeticMetrics { backend_users, .. } => {
591                match filter.backend_user_id_kind {
592                    BackendUserIdKind::Admin
593                    | BackendUserIdKind::Backup
594                    | BackendUserIdKind::Signing => None,
595                    BackendUserIdKind::Metrics => {
596                        Some(vec![backend_users.metrics_user.as_ref().clone()])
597                    }
598                    BackendUserIdKind::NonAdmin | BackendUserIdKind::Any => {
599                        Some(backend_users.get_users().to_vec())
600                    }
601                    BackendUserIdKind::Observer => Some(backend_users.operator_users.to_vec()),
602                }
603            }
604            Self::Signing { backend_user, .. } => {
605                if [
606                    BackendUserIdKind::Any,
607                    BackendUserIdKind::NonAdmin,
608                    BackendUserIdKind::Signing,
609                ]
610                .contains(&filter.backend_user_id_kind)
611                {
612                    Some(vec![backend_user.clone()])
613                } else {
614                    None
615                }
616            }
617        } {
618            backend_user_ids
619                .into_iter()
620                .map(|backend_user_id| {
621                    Box::new(FullCredentials::new(
622                        backend_user_id,
623                        Passphrase::generate(None),
624                    )) as Box<dyn UserWithPassphrase>
625                })
626                .collect()
627        } else {
628            Vec::new()
629        }
630    }
631}
632
633/// A filter for filtering sets of key IDs used in a NetHSM.
634#[derive(Clone, Debug)]
635pub struct NetHsmBackendKeyIdFilter<'a> {
636    pub namespace: Option<&'a NamespaceId>,
637}
638
639impl<'a> BackendKeyIdFilter for NetHsmBackendKeyIdFilter<'a> {}
640
641impl<'a> MappingBackendKeyId<NetHsmBackendKeyIdFilter<'a>> for NetHsmUserMapping {
642    fn backend_key_id(&self, filter: &NetHsmBackendKeyIdFilter<'a>) -> Option<String> {
643        match self {
644            Self::Admin(_)
645            | Self::Backup { .. }
646            | Self::HermeticMetrics { .. }
647            | Self::Metrics { .. } => None,
648            Self::Signing {
649                backend_user,
650                signing_key_id,
651                ..
652            } => {
653                if filter.namespace == backend_user.namespace() {
654                    Some(signing_key_id.to_string())
655                } else {
656                    None
657                }
658            }
659        }
660    }
661}
662
663impl MappingBackendUserSecrets for NetHsmUserMapping {}
664
665/// A filter for filtering sets of tags used in a NetHSM.
666#[derive(Clone, Debug)]
667pub struct NetHsmConfigDomainFilter<'a> {
668    /// An optional [`NamespaceId`] that is used to filter a [`NetHsmUserMapping`] by when
669    /// searching for tags.
670    pub namespace: Option<&'a NamespaceId>,
671}
672
673impl<'a> BackendDomainFilter for NetHsmConfigDomainFilter<'a> {}
674
675impl<'a> MappingBackendDomain<NetHsmConfigDomainFilter<'a>> for NetHsmUserMapping {
676    /// Returns the optional tag of the [`NetHsmUserMapping`].
677    ///
678    /// # Note
679    ///
680    /// Delegates to [`NetHsmUserMapping::tag`].
681    fn backend_domain(&self, filter: Option<&NetHsmConfigDomainFilter>) -> Option<String> {
682        let filter = if let Some(filter) = filter {
683            filter.namespace
684        } else {
685            None
686        };
687
688        self.tag(filter).map(ToString::to_string)
689    }
690}
691
692/// Validates a set of [`Connection`] objects.
693///
694/// Ensures that `value` is not empty and does not contain one or more [`Connection`]s with
695/// duplicate URLs.
696///
697/// # Note
698///
699/// [`Connection`] derives [`Eq`]/[`PartialEq`] and [`Ord`]/[`PartialOrd`], which allows several
700/// items with the same URL but differing TLS settings. However, in a configuration file we
701/// generally never want to use the same device with differing TLS settings, as those devices are
702/// used in a round-robin fashion.
703///
704/// # Errors
705///
706/// Returns an error if `value` is empty or contains one or more [`Connection`]s with duplicate
707/// URLs.
708fn validate_nethsm_config_connections(
709    value: &BTreeSet<Connection>,
710    _context: &(),
711) -> garde::Result {
712    if value.is_empty() {
713        return Err(garde::Error::new("contains no connections"));
714    }
715
716    let urls = value
717        .iter()
718        .map(|connection| connection.url())
719        .collect::<Vec<_>>();
720    let duplicates = {
721        let mut duplicates = HashSet::new();
722
723        for url in urls.iter() {
724            if urls.iter().filter(|list_url| url == *list_url).count() > 1 {
725                duplicates.insert(url);
726            }
727        }
728        let mut duplicates = Vec::from_iter(duplicates);
729        duplicates.sort();
730        duplicates
731    };
732
733    if !duplicates.is_empty() {
734        return Err(garde::Error::new(format!(
735            "contains the duplicate URL{} {}",
736            if duplicates.len() > 1 { "s" } else { "" },
737            duplicates
738                .iter()
739                .map(|url| format!("\"{url}\""))
740                .collect::<Vec<_>>()
741                .join(", ")
742        )));
743    }
744
745    Ok(())
746}
747
748/// Validates a set of [`NetHsmUserMapping`] objects.
749///
750/// Ensures that `value` is not empty.
751///
752/// Further ensures that there are no
753///
754/// - duplicate system users
755/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
756/// - missing system-wide administrator backend users
757/// - duplicate backend users
758/// - duplicate system-wide signing key IDs
759/// - duplicate system-wide tags
760/// - duplicate wrapping key IDs
761/// - missing namespaced administrator backend users
762/// - duplicate namespaced signing key IDs
763/// - duplicate namespaced tags
764///
765/// # Errors
766///
767/// Returns an error if there are
768///
769/// - no items in `value`
770/// - duplicate system users
771/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
772/// - missing system-wide administrator backend users
773/// - duplicate backend users
774/// - duplicate system-wide signing key IDs
775/// - duplicate system-wide tags
776/// - duplicate wrapping key IDs
777/// - missing namespaced administrator backend users
778/// - duplicate namespaced signing key IDs
779/// - duplicate namespaced tags
780fn validate_nethsm_config_mappings(
781    value: &BTreeSet<NetHsmUserMapping>,
782    _context: &(),
783) -> garde::Result {
784    if value.is_empty() {
785        return Err(garde::Error::new("contains no user mappings"));
786    }
787
788    // Collect all duplicate system user IDs.
789    let duplicate_system_user_ids = duplicate_system_user_ids(value);
790
791    // Collect all duplicate SSH public keys used as authorized_keys.
792    let duplicate_authorized_keys = duplicate_authorized_keys(value);
793
794    // Check whether there is at least one system-wide backend administrator.
795    let missing_system_wide_admin = {
796        let num_system_admins = value
797            .iter()
798            .filter_map(|mapping| {
799                if let NetHsmUserMapping::Admin(user_id) = mapping
800                    && !user_id.is_namespaced()
801                {
802                    Some(user_id)
803                } else {
804                    None
805                }
806            })
807            .count();
808
809        if num_system_admins == 0 {
810            Some("no system-wide administrator user".to_string())
811        } else {
812            None
813        }
814    };
815
816    // Collect all duplicate backend user IDs.
817    let duplicate_backend_user_ids = duplicate_backend_user_ids(value);
818
819    // Collect all duplicate system-wide key IDs.
820    let duplicate_system_wide_key_ids = duplicate_key_ids(
821        value,
822        &NetHsmBackendKeyIdFilter { namespace: None },
823        Some(" system-wide".to_string()),
824    );
825
826    // Collect all duplicate system-wide tags.
827    let duplicate_system_wide_tags =
828        duplicate_domains(value, None, Some(" system-wide".to_string()), Some("tag"));
829
830    // Collect all namespace IDs.
831    let all_namespaces = {
832        let mut all_namespaces = Vec::from_iter(
833            value
834                .iter()
835                .flat_map(|mapping| mapping.namespaces())
836                .collect::<HashSet<_>>(),
837        );
838        all_namespaces.sort();
839        all_namespaces
840    };
841
842    // Collect all namespace IDs without an admin user.
843    let namespaces_without_admin = {
844        let mut all_namespaces: HashSet<&NamespaceId> = HashSet::from_iter(all_namespaces.clone());
845
846        for mapping in value.iter() {
847            if let NetHsmUserMapping::Admin(user_id) = mapping
848                && let Some(namespace) = user_id.namespace()
849            {
850                all_namespaces.remove(namespace);
851            }
852        }
853
854        if all_namespaces.is_empty() {
855            None
856        } else {
857            let mut namespaces_without_admin = all_namespaces
858                .iter()
859                .map(|namespace| format!("\"{namespace}\""))
860                .collect::<Vec<_>>();
861            namespaces_without_admin.sort();
862            Some(format!(
863                "the namespace{} {} without an administrator user",
864                if namespaces_without_admin.len() > 1 {
865                    "s"
866                } else {
867                    ""
868                },
869                namespaces_without_admin.join(", ")
870            ))
871        }
872    };
873
874    // Collect all duplicate namespaced key IDs.
875    let duplicate_namespaced_key_ids = {
876        let mut all_duplicates = Vec::new();
877
878        for namespace in all_namespaces.iter() {
879            let mut duplicates = duplicate_key_ids(
880                value,
881                &NetHsmBackendKeyIdFilter {
882                    namespace: Some(namespace),
883                },
884                Some(format!(" \"{namespace}\" namespaced")),
885            );
886            if let Some(message) = duplicates.take() {
887                all_duplicates.push(message)
888            }
889        }
890
891        if all_duplicates.is_empty() {
892            None
893        } else {
894            Some(all_duplicates.join("\n"))
895        }
896    };
897
898    // Collect all duplicate namespaced tags.
899    let duplicate_namespaced_tags = {
900        let mut all_duplicates = Vec::new();
901
902        for namespace in all_namespaces.iter() {
903            let mut duplicates = duplicate_domains(
904                value,
905                Some(&NetHsmConfigDomainFilter {
906                    namespace: Some(namespace),
907                }),
908                Some(format!(" \"{namespace}\" namespaced")),
909                Some("tag"),
910            );
911            if let Some(message) = duplicates.take() {
912                all_duplicates.push(message)
913            }
914        }
915
916        if all_duplicates.is_empty() {
917            None
918        } else {
919            Some(all_duplicates.join("\n"))
920        }
921    };
922
923    let messages = [
924        duplicate_system_user_ids,
925        duplicate_authorized_keys,
926        missing_system_wide_admin,
927        duplicate_backend_user_ids,
928        duplicate_system_wide_key_ids,
929        duplicate_system_wide_tags,
930        namespaces_without_admin,
931        duplicate_namespaced_key_ids,
932        duplicate_namespaced_tags,
933    ];
934    let error_messages = {
935        let mut error_messages = Vec::new();
936
937        for message in messages.iter().flatten() {
938            error_messages.push(message.as_str());
939        }
940
941        error_messages
942    };
943
944    match error_messages.len() {
945        0 => Ok(()),
946        1 => Err(garde::Error::new(format!(
947            "contains {}",
948            error_messages.join("\n")
949        ))),
950        _ => Err(garde::Error::new(format!(
951            "contains multiple issues:\n⤷ {}",
952            error_messages.join("\n⤷ ")
953        ))),
954    }
955}
956
957/// The configuration items for a NetHSM.
958///
959/// Tracks a set of connections to a NetHSM backend and user mappings that are present on each of
960/// them.
961#[derive(Clone, Debug, Default, Deserialize, Serialize, Validate)]
962#[serde(rename_all = "snake_case")]
963pub struct NetHsmConfig {
964    #[garde(custom(validate_nethsm_config_connections))]
965    connections: BTreeSet<Connection>,
966
967    #[garde(custom(validate_nethsm_config_mappings))]
968    mappings: BTreeSet<NetHsmUserMapping>,
969}
970
971impl NetHsmConfig {
972    /// Creates a new [`NetHsmConfig`] from a set of [`Connection`] and a set of
973    /// [`NetHsmUserMapping`] items.
974    pub fn new(
975        connections: BTreeSet<Connection>,
976        mappings: BTreeSet<NetHsmUserMapping>,
977    ) -> Result<Self, crate::Error> {
978        let config = Self {
979            connections,
980            mappings,
981        };
982
983        config
984            .validate()
985            .map_err(|source| crate::Error::Validation {
986                context: "validating a NetHSM specific configuration item".to_string(),
987                source,
988            })?;
989
990        Ok(config)
991    }
992
993    /// Returns a reference to the set of [`Connection`] objects.
994    pub fn connections(&self) -> &BTreeSet<Connection> {
995        &self.connections
996    }
997
998    /// Returns a reference to the set of [`NetHsmUserMapping`] objects.
999    pub fn mappings(&self) -> &BTreeSet<NetHsmUserMapping> {
1000        &self.mappings
1001    }
1002}
1003
1004impl ConfigAuthorizedKeyEntries for NetHsmConfig {
1005    fn authorized_key_entries(&self) -> HashSet<&AuthorizedKeyEntry> {
1006        self.mappings
1007            .iter()
1008            .filter_map(|mapping| mapping.authorized_key_entry())
1009            .collect()
1010    }
1011}
1012
1013impl ConfigSystemUserIds for NetHsmConfig {
1014    fn system_user_ids(&self) -> HashSet<&SystemUserId> {
1015        self.mappings
1016            .iter()
1017            .filter_map(|mapping| mapping.system_user_id())
1018            .collect()
1019    }
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024    use std::thread::current;
1025
1026    use insta::{assert_snapshot, with_settings};
1027    use rstest::{fixture, rstest};
1028    use signstar_crypto::{
1029        key::{CryptographicKeyContext, KeyMechanism, KeyType, SignatureType, SigningKeySetup},
1030        openpgp::OpenPgpUserIdList,
1031    };
1032    use testresult::TestResult;
1033
1034    use super::*;
1035
1036    const SNAPSHOT_PATH: &str = "fixtures/nethsm_config/";
1037
1038    #[test]
1039    fn nethsm_metrics_users_succeeds() -> TestResult {
1040        NetHsmMetricsUsers::new(
1041            SystemWideUserId::new("metrics".to_string())?,
1042            vec![
1043                UserId::new("operator".to_string())?,
1044                UserId::new("ns1~operator".to_string())?,
1045            ],
1046        )?;
1047        Ok(())
1048    }
1049
1050    #[test]
1051    fn nethsm_metrics_users_fails() -> TestResult {
1052        if let Ok(user) = NetHsmMetricsUsers::new(
1053            SystemWideUserId::new("metrics".to_string())?,
1054            vec![
1055                UserId::new("metrics".to_string())?,
1056                UserId::new("ns1~operator".to_string())?,
1057            ],
1058        ) {
1059            panic!("Succeeded creating a NetHsmMetricsUsers, but should have failed:\n{user:?}")
1060        }
1061        Ok(())
1062    }
1063
1064    #[rstest]
1065    #[case::admin(NetHsmUserMapping::Admin("admin".parse()?), vec!["admin".parse()?])]
1066    #[case::backup(
1067        NetHsmUserMapping::Backup{
1068            backend_user: "backup".parse()?,
1069            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1070            system_user: "backup-user".parse()?,
1071        },
1072        vec!["backup".parse()?],
1073    )]
1074    #[case::backup(
1075        NetHsmUserMapping::Metrics{
1076            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1077            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1078            system_user: "metrics-user".parse()?,
1079        },
1080        vec!["metrics".parse()?, "keymetrics".parse()?],
1081    )]
1082    #[case::backup(
1083        NetHsmUserMapping::Signing {
1084            backend_user: "signing".parse()?,
1085            signing_key_id: "signing1".parse()?,
1086            key_setup: SigningKeySetup::new(
1087                KeyType::Curve25519,
1088                vec![KeyMechanism::EdDsaSignature],
1089                None,
1090                SignatureType::EdDsa,
1091                CryptographicKeyContext::OpenPgp {
1092                    user_ids: OpenPgpUserIdList::new(vec![
1093                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1094                    ])?,
1095                    version: "v4".parse()?,
1096                },
1097            )?,
1098            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1099            system_user: "signing-user".parse()?,
1100            tag: "signing1".to_string(),
1101        },
1102        vec!["signing".parse()?],
1103    )]
1104    fn nethsm_user_mapping_nethsm_user_ids(
1105        #[case] mapping: NetHsmUserMapping,
1106        #[case] expected: Vec<UserId>,
1107    ) -> TestResult {
1108        assert_eq!(mapping.nethsm_user_ids(), expected,);
1109
1110        Ok(())
1111    }
1112
1113    #[rstest]
1114    #[case::admin(
1115        NetHsmUserMapping::Admin("admin".parse()?),
1116        vec![("admin".parse()?, UserRole::Administrator, None)],
1117    )]
1118    #[case::backup(
1119        NetHsmUserMapping::Backup{
1120            backend_user: "backup".parse()?,
1121            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1122            system_user: "backup-user".parse()?,
1123        },
1124        vec![("backup".parse()?, UserRole::Backup, None)],
1125    )]
1126    #[case::metrics(
1127        NetHsmUserMapping::Metrics{
1128            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1129            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1130            system_user: "metrics-user".parse()?,
1131        },
1132        vec![
1133            ("keymetrics".parse()?, UserRole::Operator, None),
1134            ("metrics".parse()?, UserRole::Metrics, None),
1135        ],
1136    )]
1137    #[case::signing(
1138        NetHsmUserMapping::Signing {
1139            backend_user: "signing".parse()?,
1140            signing_key_id: "signing1".parse()?,
1141            key_setup: SigningKeySetup::new(
1142                KeyType::Curve25519,
1143                vec![KeyMechanism::EdDsaSignature],
1144                None,
1145                SignatureType::EdDsa,
1146                CryptographicKeyContext::OpenPgp {
1147                    user_ids: OpenPgpUserIdList::new(vec![
1148                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1149                    ])?,
1150                    version: "v4".parse()?,
1151                },
1152            )?,
1153            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1154            system_user: "signing-user".parse()?,
1155            tag: "signing1".to_string(),
1156        },
1157        vec![("signing".parse()?, UserRole::Operator, Some("signing1"))],
1158    )]
1159    fn nethsm_user_mapping_nethsm_user_data(
1160        #[case] mapping: NetHsmUserMapping,
1161        #[case] expected: Vec<(UserId, UserRole, Option<&str>)>,
1162    ) -> TestResult {
1163        let expected = expected
1164            .iter()
1165            .map(|(user, role, tag)| NetHsmUserData {
1166                user,
1167                role: *role,
1168                tag: *tag,
1169            })
1170            .collect::<HashSet<_>>();
1171        assert_eq!(mapping.nethsm_user_data(), expected);
1172
1173        Ok(())
1174    }
1175
1176    #[rstest]
1177    #[case::admin_filter_all(
1178        NetHsmUserMapping::Admin("admin".parse()?),
1179        NetHsmUserKeysFilter::All,
1180        None,
1181    )]
1182    #[case::admin_filter_namespace(
1183        NetHsmUserMapping::Admin("admin".parse()?),
1184        NetHsmUserKeysFilter::Namespaced,
1185        None,
1186    )]
1187    #[case::admin_filter_system_wide(
1188        NetHsmUserMapping::Admin("admin".parse()?),
1189        NetHsmUserKeysFilter::SystemWide,
1190        None,
1191    )]
1192    #[case::backup_filter_all(
1193        NetHsmUserMapping::Backup{
1194            backend_user: "backup".parse()?,
1195            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1196            system_user: "backup-user".parse()?,
1197        },
1198        NetHsmUserKeysFilter::All,
1199        None,
1200    )]
1201    #[case::backup_filter_namespaced(
1202        NetHsmUserMapping::Backup{
1203            backend_user: "backup".parse()?,
1204            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1205            system_user: "backup-user".parse()?,
1206        },
1207        NetHsmUserKeysFilter::Namespaced,
1208        None,
1209    )]
1210    #[case::backup_filter_system_wide(
1211        NetHsmUserMapping::Backup{
1212            backend_user: "backup".parse()?,
1213            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1214            system_user: "backup-user".parse()?,
1215        },
1216        NetHsmUserKeysFilter::SystemWide,
1217        None,
1218    )]
1219    #[case::metrics_filter_all(
1220        NetHsmUserMapping::Metrics{
1221            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1222            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1223            system_user: "metrics-user".parse()?,
1224        },
1225        NetHsmUserKeysFilter::All,
1226        None,
1227    )]
1228    #[case::metrics_filter_namespaced(
1229        NetHsmUserMapping::Metrics{
1230            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1231            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1232            system_user: "metrics-user".parse()?,
1233        },
1234        NetHsmUserKeysFilter::Namespaced,
1235        None,
1236    )]
1237    #[case::metrics_filter_system_wide(
1238        NetHsmUserMapping::Metrics{
1239            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1240            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1241            system_user: "metrics-user".parse()?,
1242        },
1243        NetHsmUserKeysFilter::SystemWide,
1244        None,
1245    )]
1246    #[case::signing_system_wide_filter_all(
1247        NetHsmUserMapping::Signing {
1248            backend_user: "signing".parse()?,
1249            signing_key_id: "signing1".parse()?,
1250            key_setup: SigningKeySetup::new(
1251                KeyType::Curve25519,
1252                vec![KeyMechanism::EdDsaSignature],
1253                None,
1254                SignatureType::EdDsa,
1255                CryptographicKeyContext::OpenPgp {
1256                    user_ids: OpenPgpUserIdList::new(vec![
1257                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1258                    ])?,
1259                    version: "v4".parse()?,
1260                },
1261            )?,
1262            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1263            system_user: "signing-user".parse()?,
1264            tag: "signing1".to_string(),
1265        },
1266        NetHsmUserKeysFilter::All,
1267        Some((
1268            "signing".parse()?,
1269            "signing1".parse()?,
1270            SigningKeySetup::new(
1271                KeyType::Curve25519,
1272                vec![KeyMechanism::EdDsaSignature],
1273                None,
1274                SignatureType::EdDsa,
1275                CryptographicKeyContext::OpenPgp {
1276                    user_ids: OpenPgpUserIdList::new(vec![
1277                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1278                    ])?,
1279                    version: "v4".parse()?,
1280                },
1281            )?,
1282            "signing1",
1283        )),
1284    )]
1285    #[case::signing_system_wide_filter_namespaced(
1286        NetHsmUserMapping::Signing {
1287            backend_user: "signing".parse()?,
1288            signing_key_id: "signing1".parse()?,
1289            key_setup: SigningKeySetup::new(
1290                KeyType::Curve25519,
1291                vec![KeyMechanism::EdDsaSignature],
1292                None,
1293                SignatureType::EdDsa,
1294                CryptographicKeyContext::OpenPgp {
1295                    user_ids: OpenPgpUserIdList::new(vec![
1296                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1297                    ])?,
1298                    version: "v4".parse()?,
1299                },
1300            )?,
1301            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1302            system_user: "signing-user".parse()?,
1303            tag: "signing1".to_string(),
1304        },
1305        NetHsmUserKeysFilter::Namespaced,
1306        None,
1307    )]
1308    #[case::signing_system_wide_filter_system_wide(
1309        NetHsmUserMapping::Signing {
1310            backend_user: "signing".parse()?,
1311            signing_key_id: "signing1".parse()?,
1312            key_setup: SigningKeySetup::new(
1313                KeyType::Curve25519,
1314                vec![KeyMechanism::EdDsaSignature],
1315                None,
1316                SignatureType::EdDsa,
1317                CryptographicKeyContext::OpenPgp {
1318                    user_ids: OpenPgpUserIdList::new(vec![
1319                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1320                    ])?,
1321                    version: "v4".parse()?,
1322                },
1323            )?,
1324            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1325            system_user: "signing-user".parse()?,
1326            tag: "signing1".to_string(),
1327        },
1328        NetHsmUserKeysFilter::SystemWide,
1329        Some((
1330            "signing".parse()?,
1331            "signing1".parse()?,
1332            SigningKeySetup::new(
1333                KeyType::Curve25519,
1334                vec![KeyMechanism::EdDsaSignature],
1335                None,
1336                SignatureType::EdDsa,
1337                CryptographicKeyContext::OpenPgp {
1338                    user_ids: OpenPgpUserIdList::new(vec![
1339                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1340                    ])?,
1341                    version: "v4".parse()?,
1342                },
1343            )?,
1344            "signing1",
1345        )),
1346    )]
1347    #[case::signing_namespaced_filter_all(
1348        NetHsmUserMapping::Signing {
1349            backend_user: "ns1~signing".parse()?,
1350            signing_key_id: "signing1".parse()?,
1351            key_setup: SigningKeySetup::new(
1352                KeyType::Curve25519,
1353                vec![KeyMechanism::EdDsaSignature],
1354                None,
1355                SignatureType::EdDsa,
1356                CryptographicKeyContext::OpenPgp {
1357                    user_ids: OpenPgpUserIdList::new(vec![
1358                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1359                    ])?,
1360                    version: "v4".parse()?,
1361                },
1362            )?,
1363            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1364            system_user: "signing-user".parse()?,
1365            tag: "signing1".to_string(),
1366        },
1367        NetHsmUserKeysFilter::All,
1368        Some((
1369            "ns1~signing".parse()?,
1370            "signing1".parse()?,
1371            SigningKeySetup::new(
1372                KeyType::Curve25519,
1373                vec![KeyMechanism::EdDsaSignature],
1374                None,
1375                SignatureType::EdDsa,
1376                CryptographicKeyContext::OpenPgp {
1377                    user_ids: OpenPgpUserIdList::new(vec![
1378                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1379                    ])?,
1380                    version: "v4".parse()?,
1381                },
1382            )?,
1383            "signing1",
1384        )),
1385    )]
1386    #[case::signing_system_wide_filter_namespaced(
1387        NetHsmUserMapping::Signing {
1388            backend_user: "ns1~signing".parse()?,
1389            signing_key_id: "signing1".parse()?,
1390            key_setup: SigningKeySetup::new(
1391                KeyType::Curve25519,
1392                vec![KeyMechanism::EdDsaSignature],
1393                None,
1394                SignatureType::EdDsa,
1395                CryptographicKeyContext::OpenPgp {
1396                    user_ids: OpenPgpUserIdList::new(vec![
1397                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1398                    ])?,
1399                    version: "v4".parse()?,
1400                },
1401            )?,
1402            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1403            system_user: "signing-user".parse()?,
1404            tag: "signing1".to_string(),
1405        },
1406        NetHsmUserKeysFilter::Namespaced,
1407        Some((
1408            "ns1~signing".parse()?,
1409            "signing1".parse()?,
1410            SigningKeySetup::new(
1411                KeyType::Curve25519,
1412                vec![KeyMechanism::EdDsaSignature],
1413                None,
1414                SignatureType::EdDsa,
1415                CryptographicKeyContext::OpenPgp {
1416                    user_ids: OpenPgpUserIdList::new(vec![
1417                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1418                    ])?,
1419                    version: "v4".parse()?,
1420                },
1421            )?,
1422            "signing1",
1423        )),
1424    )]
1425    #[case::signing_system_wide_filter_system_wide(
1426        NetHsmUserMapping::Signing {
1427            backend_user: "ns1~signing".parse()?,
1428            signing_key_id: "signing1".parse()?,
1429            key_setup: SigningKeySetup::new(
1430                KeyType::Curve25519,
1431                vec![KeyMechanism::EdDsaSignature],
1432                None,
1433                SignatureType::EdDsa,
1434                CryptographicKeyContext::OpenPgp {
1435                    user_ids: OpenPgpUserIdList::new(vec![
1436                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1437                    ])?,
1438                    version: "v4".parse()?,
1439                },
1440            )?,
1441            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1442            system_user: "signing-user".parse()?,
1443            tag: "signing1".to_string(),
1444        },
1445        NetHsmUserKeysFilter::SystemWide,
1446        None,
1447    )]
1448    fn nethsm_user_mapping_nethsm_user_key_data(
1449        #[case] mapping: NetHsmUserMapping,
1450        #[case] filter: NetHsmUserKeysFilter,
1451        #[case] expected: Option<(UserId, KeyId, SigningKeySetup, &str)>,
1452    ) -> TestResult {
1453        let expected = expected
1454            .as_ref()
1455            .map(|(user, key_id, key_setup, tag)| NetHsmUserKeyData {
1456                user,
1457                key_id,
1458                key_setup,
1459                tag,
1460            });
1461        assert_eq!(mapping.nethsm_user_key_data(filter), expected);
1462
1463        Ok(())
1464    }
1465
1466    #[rstest]
1467    #[case::admin_filter_admin(
1468        NetHsmUserMapping::Admin("admin".parse()?),
1469        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1470        vec!["admin"]
1471    )]
1472    #[case::admin_filter_any(
1473        NetHsmUserMapping::Admin("admin".parse()?),
1474        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1475        vec!["admin"]
1476    )]
1477    #[case::backup_filter_any(
1478        NetHsmUserMapping::Backup{
1479            backend_user: "backup".parse()?,
1480            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1481            system_user: "backup-user".parse()?,
1482        },
1483        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1484        vec!["backup"]
1485    )]
1486    #[case::backup_filter_backup(
1487        NetHsmUserMapping::Backup{
1488            backend_user: "backup".parse()?,
1489            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1490            system_user: "backup-user".parse()?,
1491        },
1492        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1493        vec!["backup"]
1494    )]
1495    #[case::backup_filter_non_admin(
1496        NetHsmUserMapping::Backup{
1497            backend_user: "backup".parse()?,
1498            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1499            system_user: "backup-user".parse()?,
1500        },
1501        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1502        vec!["backup"]
1503    )]
1504    #[case::hermetic_metrics_filter_any(
1505        NetHsmUserMapping::HermeticMetrics {
1506            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1507            system_user: "metrics-user".parse()?,
1508        },
1509        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1510        vec!["metrics", "keymetrics"]
1511    )]
1512    #[case::hermetic_metrics_filter_metrics(
1513        NetHsmUserMapping::HermeticMetrics {
1514            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1515            system_user: "metrics-user".parse()?,
1516        },
1517        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1518        vec!["metrics"]
1519    )]
1520    #[case::hermetic_metrics_filter_non_admin(
1521        NetHsmUserMapping::HermeticMetrics {
1522            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1523            system_user: "metrics-user".parse()?,
1524        },
1525        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1526        vec!["metrics", "keymetrics"]
1527    )]
1528    #[case::hermetic_metrics_filter_observer(
1529        NetHsmUserMapping::HermeticMetrics {
1530            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1531            system_user: "metrics-user".parse()?,
1532        },
1533        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1534        vec!["keymetrics"]
1535    )]
1536    #[case::signing_filter_any(
1537        NetHsmUserMapping::Signing {
1538            backend_user: "signing".parse()?,
1539            signing_key_id: "signing1".parse()?,
1540            key_setup: SigningKeySetup::new(
1541                KeyType::Curve25519,
1542                vec![KeyMechanism::EdDsaSignature],
1543                None,
1544                SignatureType::EdDsa,
1545                CryptographicKeyContext::OpenPgp {
1546                    user_ids: OpenPgpUserIdList::new(vec![
1547                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1548                    ])?,
1549                    version: "v4".parse()?,
1550                },
1551            )?,
1552            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1553            system_user: "signing-user".parse()?,
1554            tag: "signing1".to_string(),
1555        },
1556        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1557        vec!["signing"]
1558    )]
1559    #[case::signing_filter_non_admin(
1560        NetHsmUserMapping::Signing {
1561            backend_user: "signing".parse()?,
1562            signing_key_id: "signing1".parse()?,
1563            key_setup: SigningKeySetup::new(
1564                KeyType::Curve25519,
1565                vec![KeyMechanism::EdDsaSignature],
1566                None,
1567                SignatureType::EdDsa,
1568                CryptographicKeyContext::OpenPgp {
1569                    user_ids: OpenPgpUserIdList::new(vec![
1570                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1571                    ])?,
1572                    version: "v4".parse()?,
1573                },
1574            )?,
1575            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1576            system_user: "signing-user".parse()?,
1577            tag: "signing1".to_string(),
1578        },
1579        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1580        vec!["signing"]
1581    )]
1582    #[case::signing_filter_signing(
1583        NetHsmUserMapping::Signing {
1584            backend_user: "signing".parse()?,
1585            signing_key_id: "signing1".parse()?,
1586            key_setup: SigningKeySetup::new(
1587                KeyType::Curve25519,
1588                vec![KeyMechanism::EdDsaSignature],
1589                None,
1590                SignatureType::EdDsa,
1591                CryptographicKeyContext::OpenPgp {
1592                    user_ids: OpenPgpUserIdList::new(vec![
1593                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1594                    ])?,
1595                    version: "v4".parse()?,
1596                },
1597            )?,
1598            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1599            system_user: "signing-user".parse()?,
1600            tag: "signing1".to_string(),
1601        },
1602        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1603        vec!["signing"]
1604    )]
1605    fn nethsm_user_mapping_backend_user_ids_filter_matches(
1606        #[case] mapping: NetHsmUserMapping,
1607        #[case] filter: BackendUserIdFilter,
1608        #[case] expected: Vec<&str>,
1609    ) -> TestResult {
1610        assert_eq!(mapping.backend_user_ids(filter), expected);
1611
1612        Ok(())
1613    }
1614
1615    #[rstest]
1616    #[case::admin_filter_metrics(
1617        NetHsmUserMapping::Admin("admin".parse()?),
1618        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1619    )]
1620    #[case::admin_filter_non_admin(
1621        NetHsmUserMapping::Admin("admin".parse()?),
1622        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1623    )]
1624    #[case::admin_filter_observer(
1625        NetHsmUserMapping::Admin("admin".parse()?),
1626        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1627    )]
1628    #[case::admin_filter_signing(
1629        NetHsmUserMapping::Admin("admin".parse()?),
1630        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1631    )]
1632    #[case::backup_filter_admin(
1633        NetHsmUserMapping::Backup{
1634            backend_user: "backup".parse()?,
1635            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1636            system_user: "backup-user".parse()?,
1637        },
1638        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1639    )]
1640    #[case::backup_filter_metrics(
1641        NetHsmUserMapping::Backup{
1642            backend_user: "backup".parse()?,
1643            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1644            system_user: "backup-user".parse()?,
1645        },
1646        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1647    )]
1648    #[case::backup_filter_observer(
1649        NetHsmUserMapping::Backup{
1650            backend_user: "backup".parse()?,
1651            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1652            system_user: "backup-user".parse()?,
1653        },
1654        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1655    )]
1656    #[case::backup_filter_signing(
1657        NetHsmUserMapping::Backup{
1658            backend_user: "backup".parse()?,
1659            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1660            system_user: "backup-user".parse()?,
1661        },
1662        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1663    )]
1664    #[case::hermetic_metrics_filter_admin(
1665        NetHsmUserMapping::HermeticMetrics {
1666            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1667            system_user: "metrics-user".parse()?,
1668        },
1669        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1670    )]
1671    #[case::hermetic_metrics_filter_backup(
1672        NetHsmUserMapping::HermeticMetrics {
1673            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1674            system_user: "metrics-user".parse()?,
1675        },
1676        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1677    )]
1678    #[case::hermetic_metrics_filter_signing(
1679        NetHsmUserMapping::HermeticMetrics {
1680            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1681            system_user: "metrics-user".parse()?,
1682        },
1683        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1684    )]
1685    #[case::metrics_filter_admin(
1686        NetHsmUserMapping::Metrics {
1687            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1688            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1689            system_user: "hermetic-metrics-user".parse()?,
1690        },
1691        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1692    )]
1693    #[case::metrics_filter_backup(
1694        NetHsmUserMapping::Metrics {
1695            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1696            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1697            system_user: "hermetic-metrics-user".parse()?,
1698        },
1699        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1700    )]
1701    #[case::metrics_filter_signing(
1702        NetHsmUserMapping::Metrics {
1703            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1704            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1705            system_user: "hermetic-metrics-user".parse()?,
1706        },
1707        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1708    )]
1709    #[case::signing_filter_admin(
1710        NetHsmUserMapping::Signing {
1711            backend_user: "signing".parse()?,
1712            signing_key_id: "signing1".parse()?,
1713            key_setup: SigningKeySetup::new(
1714                KeyType::Curve25519,
1715                vec![KeyMechanism::EdDsaSignature],
1716                None,
1717                SignatureType::EdDsa,
1718                CryptographicKeyContext::OpenPgp {
1719                    user_ids: OpenPgpUserIdList::new(vec![
1720                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1721                    ])?,
1722                    version: "v4".parse()?,
1723                },
1724            )?,
1725            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1726            system_user: "signing-user".parse()?,
1727            tag: "signing1".to_string(),
1728        },
1729        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1730    )]
1731    #[case::signing_filter_backup(
1732        NetHsmUserMapping::Signing {
1733            backend_user: "signing".parse()?,
1734            signing_key_id: "signing1".parse()?,
1735            key_setup: SigningKeySetup::new(
1736                KeyType::Curve25519,
1737                vec![KeyMechanism::EdDsaSignature],
1738                None,
1739                SignatureType::EdDsa,
1740                CryptographicKeyContext::OpenPgp {
1741                    user_ids: OpenPgpUserIdList::new(vec![
1742                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1743                    ])?,
1744                    version: "v4".parse()?,
1745                },
1746            )?,
1747            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1748            system_user: "signing-user".parse()?,
1749            tag: "signing1".to_string(),
1750        },
1751        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1752    )]
1753    #[case::signing_filter_metrics(
1754        NetHsmUserMapping::Signing {
1755            backend_user: "signing".parse()?,
1756            signing_key_id: "signing1".parse()?,
1757            key_setup: SigningKeySetup::new(
1758                KeyType::Curve25519,
1759                vec![KeyMechanism::EdDsaSignature],
1760                None,
1761                SignatureType::EdDsa,
1762                CryptographicKeyContext::OpenPgp {
1763                    user_ids: OpenPgpUserIdList::new(vec![
1764                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1765                    ])?,
1766                    version: "v4".parse()?,
1767                },
1768            )?,
1769            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1770            system_user: "signing-user".parse()?,
1771            tag: "signing1".to_string(),
1772        },
1773        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1774    )]
1775    #[case::signing_filter_observer(
1776        NetHsmUserMapping::Signing {
1777            backend_user: "signing".parse()?,
1778            signing_key_id: "signing1".parse()?,
1779            key_setup: SigningKeySetup::new(
1780                KeyType::Curve25519,
1781                vec![KeyMechanism::EdDsaSignature],
1782                None,
1783                SignatureType::EdDsa,
1784                CryptographicKeyContext::OpenPgp {
1785                    user_ids: OpenPgpUserIdList::new(vec![
1786                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1787                    ])?,
1788                    version: "v4".parse()?,
1789                },
1790            )?,
1791            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1792            system_user: "signing-user".parse()?,
1793            tag: "signing1".to_string(),
1794        },
1795        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1796    )]
1797    fn nethsm_user_mapping_backend_user_ids_filter_mismatches(
1798        #[case] mapping: NetHsmUserMapping,
1799        #[case] filter: BackendUserIdFilter,
1800    ) -> TestResult {
1801        assert!(mapping.backend_user_ids(filter).is_empty());
1802
1803        Ok(())
1804    }
1805
1806    #[test]
1807    fn nethsm_user_mapping_backend_user_with_passphrase_succeeds() -> TestResult {
1808        let mapping = NetHsmUserMapping::Admin("admin".parse()?);
1809        let passphrase = Passphrase::generate(None);
1810        let creds = mapping.backend_user_with_passphrase("admin", passphrase.clone())?;
1811
1812        assert_eq!(creds.user(), "admin");
1813        assert_eq!(
1814            creds.passphrase().expose_borrowed(),
1815            passphrase.expose_borrowed()
1816        );
1817
1818        Ok(())
1819    }
1820
1821    #[rstest]
1822    #[case::admin_filter_admin(
1823        NetHsmUserMapping::Admin("admin".parse()?),
1824        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1825        vec!["admin"]
1826    )]
1827    #[case::admin_filter_any(
1828        NetHsmUserMapping::Admin("admin".parse()?),
1829        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1830        vec!["admin"]
1831    )]
1832    #[case::backup_filter_any(
1833        NetHsmUserMapping::Backup{
1834            backend_user: "backup".parse()?,
1835            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1836            system_user: "backup-user".parse()?,
1837        },
1838        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1839        vec!["backup"]
1840    )]
1841    #[case::backup_filter_backup(
1842        NetHsmUserMapping::Backup{
1843            backend_user: "backup".parse()?,
1844            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1845            system_user: "backup-user".parse()?,
1846        },
1847        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1848        vec!["backup"]
1849    )]
1850    #[case::backup_filter_non_admin(
1851        NetHsmUserMapping::Backup{
1852            backend_user: "backup".parse()?,
1853            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1854            system_user: "backup-user".parse()?,
1855        },
1856        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1857        vec!["backup"]
1858    )]
1859    #[case::hermetic_metrics_filter_any(
1860        NetHsmUserMapping::HermeticMetrics {
1861            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1862            system_user: "metrics-user".parse()?,
1863        },
1864        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1865        vec!["metrics", "keymetrics"]
1866    )]
1867    #[case::hermetic_metrics_filter_metrics(
1868        NetHsmUserMapping::HermeticMetrics {
1869            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1870            system_user: "metrics-user".parse()?,
1871        },
1872        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1873        vec!["metrics"]
1874    )]
1875    #[case::hermetic_metrics_filter_non_admin(
1876        NetHsmUserMapping::HermeticMetrics {
1877            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1878            system_user: "metrics-user".parse()?,
1879        },
1880        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1881        vec!["metrics", "keymetrics"]
1882    )]
1883    #[case::hermetic_metrics_filter_observer(
1884        NetHsmUserMapping::HermeticMetrics {
1885            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1886            system_user: "metrics-user".parse()?,
1887        },
1888        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1889        vec!["keymetrics"]
1890    )]
1891    #[case::signing_filter_any(
1892        NetHsmUserMapping::Signing {
1893            backend_user: "signing".parse()?,
1894            signing_key_id: "signing1".parse()?,
1895            key_setup: SigningKeySetup::new(
1896                KeyType::Curve25519,
1897                vec![KeyMechanism::EdDsaSignature],
1898                None,
1899                SignatureType::EdDsa,
1900                CryptographicKeyContext::OpenPgp {
1901                    user_ids: OpenPgpUserIdList::new(vec![
1902                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1903                    ])?,
1904                    version: "v4".parse()?,
1905                },
1906            )?,
1907            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1908            system_user: "signing-user".parse()?,
1909            tag: "signing1".to_string(),
1910        },
1911        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1912        vec!["signing"]
1913    )]
1914    #[case::signing_filter_non_admin(
1915        NetHsmUserMapping::Signing {
1916            backend_user: "signing".parse()?,
1917            signing_key_id: "signing1".parse()?,
1918            key_setup: SigningKeySetup::new(
1919                KeyType::Curve25519,
1920                vec![KeyMechanism::EdDsaSignature],
1921                None,
1922                SignatureType::EdDsa,
1923                CryptographicKeyContext::OpenPgp {
1924                    user_ids: OpenPgpUserIdList::new(vec![
1925                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1926                    ])?,
1927                    version: "v4".parse()?,
1928                },
1929            )?,
1930            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1931            system_user: "signing-user".parse()?,
1932            tag: "signing1".to_string(),
1933        },
1934        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1935        vec!["signing"]
1936    )]
1937    #[case::signing_filter_signing(
1938        NetHsmUserMapping::Signing {
1939            backend_user: "signing".parse()?,
1940            signing_key_id: "signing1".parse()?,
1941            key_setup: SigningKeySetup::new(
1942                KeyType::Curve25519,
1943                vec![KeyMechanism::EdDsaSignature],
1944                None,
1945                SignatureType::EdDsa,
1946                CryptographicKeyContext::OpenPgp {
1947                    user_ids: OpenPgpUserIdList::new(vec![
1948                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1949                    ])?,
1950                    version: "v4".parse()?,
1951                },
1952            )?,
1953            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1954            system_user: "signing-user".parse()?,
1955            tag: "signing1".to_string(),
1956        },
1957        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1958        vec!["signing"]
1959    )]
1960
1961    fn nethsm_user_mapping_backend_users_with_new_passphrase_filter_matches(
1962        #[case] mapping: NetHsmUserMapping,
1963        #[case] filter: BackendUserIdFilter,
1964        #[case] expected: Vec<&str>,
1965    ) -> TestResult {
1966        let creds = mapping.backend_users_with_new_passphrase(filter);
1967        let users = creds
1968            .iter()
1969            .map(|creds| creds.user())
1970            .collect::<HashSet<_>>();
1971        let expected = expected
1972            .iter()
1973            .map(ToString::to_string)
1974            .collect::<HashSet<_>>();
1975        assert_eq!(users, expected);
1976
1977        Ok(())
1978    }
1979
1980    #[rstest]
1981    #[case::admin_filter_metrics(
1982        NetHsmUserMapping::Admin("admin".parse()?),
1983        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1984    )]
1985    #[case::admin_filter_non_admin(
1986        NetHsmUserMapping::Admin("admin".parse()?),
1987        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1988    )]
1989    #[case::admin_filter_observer(
1990        NetHsmUserMapping::Admin("admin".parse()?),
1991        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1992    )]
1993    #[case::admin_filter_signing(
1994        NetHsmUserMapping::Admin("admin".parse()?),
1995        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1996    )]
1997    #[case::backup_filter_admin(
1998        NetHsmUserMapping::Backup{
1999            backend_user: "backup".parse()?,
2000            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2001            system_user: "backup-user".parse()?,
2002        },
2003        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2004    )]
2005    #[case::backup_filter_metrics(
2006        NetHsmUserMapping::Backup{
2007            backend_user: "backup".parse()?,
2008            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2009            system_user: "backup-user".parse()?,
2010        },
2011        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
2012    )]
2013    #[case::backup_filter_observer(
2014        NetHsmUserMapping::Backup{
2015            backend_user: "backup".parse()?,
2016            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2017            system_user: "backup-user".parse()?,
2018        },
2019        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
2020    )]
2021    #[case::backup_filter_signing(
2022        NetHsmUserMapping::Backup{
2023            backend_user: "backup".parse()?,
2024            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2025            system_user: "backup-user".parse()?,
2026        },
2027        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
2028    )]
2029    #[case::hermetic_metrics_filter_admin(
2030        NetHsmUserMapping::HermeticMetrics {
2031            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2032            system_user: "metrics-user".parse()?,
2033        },
2034        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2035    )]
2036    #[case::hermetic_metrics_filter_backup(
2037        NetHsmUserMapping::HermeticMetrics {
2038            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2039            system_user: "metrics-user".parse()?,
2040        },
2041        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
2042    )]
2043    #[case::hermetic_metrics_filter_signing(
2044        NetHsmUserMapping::HermeticMetrics {
2045            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2046            system_user: "metrics-user".parse()?,
2047        },
2048        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
2049    )]
2050    #[case::metrics_filter_admin(
2051        NetHsmUserMapping::Metrics {
2052            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2053            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2054            system_user: "hermetic-metrics-user".parse()?,
2055        },
2056        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2057    )]
2058    #[case::metrics_filter_backup(
2059        NetHsmUserMapping::Metrics {
2060            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2061            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2062            system_user: "hermetic-metrics-user".parse()?,
2063        },
2064        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
2065    )]
2066    #[case::metrics_filter_signing(
2067        NetHsmUserMapping::Metrics {
2068            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2069            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2070            system_user: "hermetic-metrics-user".parse()?,
2071        },
2072        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
2073    )]
2074    #[case::signing_filter_admin(
2075        NetHsmUserMapping::Signing {
2076            backend_user: "signing".parse()?,
2077            signing_key_id: "signing1".parse()?,
2078            key_setup: SigningKeySetup::new(
2079                KeyType::Curve25519,
2080                vec![KeyMechanism::EdDsaSignature],
2081                None,
2082                SignatureType::EdDsa,
2083                CryptographicKeyContext::OpenPgp {
2084                    user_ids: OpenPgpUserIdList::new(vec![
2085                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2086                    ])?,
2087                    version: "v4".parse()?,
2088                },
2089            )?,
2090            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2091            system_user: "signing-user".parse()?,
2092            tag: "signing1".to_string(),
2093        },
2094        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2095    )]
2096    #[case::signing_filter_backup(
2097        NetHsmUserMapping::Signing {
2098            backend_user: "signing".parse()?,
2099            signing_key_id: "signing1".parse()?,
2100            key_setup: SigningKeySetup::new(
2101                KeyType::Curve25519,
2102                vec![KeyMechanism::EdDsaSignature],
2103                None,
2104                SignatureType::EdDsa,
2105                CryptographicKeyContext::OpenPgp {
2106                    user_ids: OpenPgpUserIdList::new(vec![
2107                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2108                    ])?,
2109                    version: "v4".parse()?,
2110                },
2111            )?,
2112            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2113            system_user: "signing-user".parse()?,
2114            tag: "signing1".to_string(),
2115        },
2116        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
2117    )]
2118    #[case::signing_filter_metrics(
2119        NetHsmUserMapping::Signing {
2120            backend_user: "signing".parse()?,
2121            signing_key_id: "signing1".parse()?,
2122            key_setup: SigningKeySetup::new(
2123                KeyType::Curve25519,
2124                vec![KeyMechanism::EdDsaSignature],
2125                None,
2126                SignatureType::EdDsa,
2127                CryptographicKeyContext::OpenPgp {
2128                    user_ids: OpenPgpUserIdList::new(vec![
2129                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2130                    ])?,
2131                    version: "v4".parse()?,
2132                },
2133            )?,
2134            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2135            system_user: "signing-user".parse()?,
2136            tag: "signing1".to_string(),
2137        },
2138        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
2139    )]
2140    #[case::signing_filter_observer(
2141        NetHsmUserMapping::Signing {
2142            backend_user: "signing".parse()?,
2143            signing_key_id: "signing1".parse()?,
2144            key_setup: SigningKeySetup::new(
2145                KeyType::Curve25519,
2146                vec![KeyMechanism::EdDsaSignature],
2147                None,
2148                SignatureType::EdDsa,
2149                CryptographicKeyContext::OpenPgp {
2150                    user_ids: OpenPgpUserIdList::new(vec![
2151                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2152                    ])?,
2153                    version: "v4".parse()?,
2154                },
2155            )?,
2156            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2157            system_user: "signing-user".parse()?,
2158            tag: "signing1".to_string(),
2159        },
2160        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
2161    )]
2162    fn nethsm_user_mapping_backend_users_with_new_passphrase_filter_mismatches(
2163        #[case] mapping: NetHsmUserMapping,
2164        #[case] filter: BackendUserIdFilter,
2165    ) -> TestResult {
2166        assert!(mapping.backend_users_with_new_passphrase(filter).is_empty());
2167
2168        Ok(())
2169    }
2170
2171    #[test]
2172    fn nethsm_user_mapping_backend_user_with_passphrase_fails() -> TestResult {
2173        let mapping = NetHsmUserMapping::Admin("admin".parse()?);
2174        assert!(
2175            mapping
2176                .backend_user_with_passphrase("2", Passphrase::generate(None))
2177                .is_err()
2178        );
2179
2180        Ok(())
2181    }
2182
2183    #[fixture]
2184    fn nethsm_config_connections() -> TestResult<BTreeSet<Connection>> {
2185        Ok(BTreeSet::from_iter([
2186            Connection::new(
2187                "https://nethsm1.example.org/".parse()?,
2188                nethsm::ConnectionSecurity::Unsafe,
2189            ),
2190            Connection::new(
2191                "https://nethsm2.example.org/".parse()?,
2192                nethsm::ConnectionSecurity::Unsafe,
2193            ),
2194        ]))
2195    }
2196
2197    #[fixture]
2198    fn nethsm_config_mappings() -> TestResult<BTreeSet<NetHsmUserMapping>> {
2199        Ok(BTreeSet::from_iter([
2200            NetHsmUserMapping::Admin("admin".parse()?),
2201            NetHsmUserMapping::Backup{
2202                backend_user: "backup".parse()?,
2203                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2204                system_user: "backup-user".parse()?,
2205            },
2206            NetHsmUserMapping::HermeticMetrics {
2207                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2208                system_user: "metrics-user".parse()?,
2209            },
2210            NetHsmUserMapping::Metrics {
2211                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2212                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2213                system_user: "hermetic-metrics-user".parse()?,
2214            },
2215            NetHsmUserMapping::Signing {
2216                backend_user: "signing".parse()?,
2217                signing_key_id: "signing1".parse()?,
2218                key_setup: SigningKeySetup::new(
2219                    KeyType::Curve25519,
2220                    vec![KeyMechanism::EdDsaSignature],
2221                    None,
2222                    SignatureType::EdDsa,
2223                    CryptographicKeyContext::OpenPgp {
2224                        user_ids: OpenPgpUserIdList::new(vec![
2225                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2226                        ])?,
2227                        version: "v4".parse()?,
2228                    },
2229                )?,
2230                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2231                system_user: "signing-user".parse()?,
2232                tag: "signing1".to_string(),
2233            }
2234        ]))
2235    }
2236
2237    #[fixture]
2238    fn nethsm_config(
2239        nethsm_config_connections: TestResult<BTreeSet<Connection>>,
2240        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
2241    ) -> TestResult<NetHsmConfig> {
2242        let nethsm_config_connections = nethsm_config_connections?;
2243        let nethsm_config_mappings = nethsm_config_mappings?;
2244        Ok(NetHsmConfig::new(
2245            nethsm_config_connections,
2246            nethsm_config_mappings,
2247        )?)
2248    }
2249
2250    #[rstest]
2251    fn nethsm_config_connections_matches(
2252        nethsm_config: TestResult<NetHsmConfig>,
2253        nethsm_config_connections: TestResult<BTreeSet<Connection>>,
2254    ) -> TestResult {
2255        let nethsm_config = nethsm_config?;
2256        let nethsm_config_connections = nethsm_config_connections?;
2257        assert_eq!(nethsm_config.connections(), &nethsm_config_connections);
2258
2259        Ok(())
2260    }
2261
2262    #[rstest]
2263    fn nethsm_config_mappings_matches(
2264        nethsm_config: TestResult<NetHsmConfig>,
2265        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
2266    ) -> TestResult {
2267        let nethsm_config = nethsm_config?;
2268        let nethsm_config_mappings = nethsm_config_mappings?;
2269        assert_eq!(nethsm_config.mappings(), &nethsm_config_mappings);
2270
2271        Ok(())
2272    }
2273
2274    #[rstest]
2275    fn nethsm_config_authorized_key_entries(
2276        nethsm_config: TestResult<NetHsmConfig>,
2277        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
2278    ) -> TestResult {
2279        let nethsm_config = nethsm_config?;
2280        let nethsm_config_mappings = nethsm_config_mappings?;
2281        let expected = nethsm_config_mappings
2282            .iter()
2283            .filter_map(|mapping| mapping.authorized_key_entry())
2284            .collect::<HashSet<_>>();
2285        assert_eq!(nethsm_config.authorized_key_entries(), expected);
2286
2287        Ok(())
2288    }
2289
2290    #[rstest]
2291    fn nethsm_config_system_user_ids(
2292        nethsm_config: TestResult<NetHsmConfig>,
2293        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
2294    ) -> TestResult {
2295        let nethsm_config = nethsm_config?;
2296        let nethsm_config_mappings = nethsm_config_mappings?;
2297        let expected = nethsm_config_mappings
2298            .iter()
2299            .filter_map(|mapping| mapping.system_user_id())
2300            .collect::<HashSet<_>>();
2301        assert_eq!(nethsm_config.system_user_ids(), expected);
2302
2303        Ok(())
2304    }
2305
2306    #[rstest]
2307    #[case::no_connection(
2308        "Error message for NetHsmConfig::new with no backend connection",
2309        BTreeSet::new(),
2310        BTreeSet::from_iter([
2311            NetHsmUserMapping::Admin("admin".parse()?),
2312            NetHsmUserMapping::Backup{
2313                backend_user: "backup".parse()?,
2314                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2315                system_user: "backup-user".parse()?,
2316            },
2317            NetHsmUserMapping::HermeticMetrics {
2318                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2319                system_user: "hermetic-metrics-user".parse()?,
2320            },
2321            NetHsmUserMapping::Metrics {
2322                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2323                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2324                system_user: "metrics-user".parse()?,
2325            },
2326            NetHsmUserMapping::Signing {
2327                backend_user: "signing".parse()?,
2328                signing_key_id: "signing1".parse()?,
2329                key_setup: SigningKeySetup::new(
2330                    KeyType::Curve25519,
2331                    vec![KeyMechanism::EdDsaSignature],
2332                    None,
2333                    SignatureType::EdDsa,
2334                    CryptographicKeyContext::OpenPgp {
2335                        user_ids: OpenPgpUserIdList::new(vec![
2336                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2337                        ])?,
2338                        version: "v4".parse()?,
2339                    },
2340                )?,
2341                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2342                system_user: "signing-user".parse()?,
2343                tag: "signing1".to_string(),
2344            }
2345        ]),
2346    )]
2347    #[case::duplicate_connection_url(
2348        "Error message for NetHsmConfig::new with two duplicate connection URLs",
2349        BTreeSet::from_iter([
2350            Connection::new("https://nethsm1.example.org/".parse()?, nethsm::ConnectionSecurity::Unsafe),
2351            Connection::new("https://nethsm1.example.org/".parse()?, nethsm::ConnectionSecurity::Native),
2352        ]),
2353        BTreeSet::from_iter([
2354            NetHsmUserMapping::Admin("admin".parse()?),
2355            NetHsmUserMapping::Backup{
2356                backend_user: "backup".parse()?,
2357                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2358                system_user: "backup-user".parse()?,
2359            },
2360            NetHsmUserMapping::HermeticMetrics {
2361                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2362                system_user: "hermetic-metrics-user".parse()?,
2363            },
2364            NetHsmUserMapping::Metrics {
2365                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2366                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2367                system_user: "metrics-user".parse()?,
2368            },
2369            NetHsmUserMapping::Signing {
2370                backend_user: "signing".parse()?,
2371                signing_key_id: "signing1".parse()?,
2372                key_setup: SigningKeySetup::new(
2373                    KeyType::Curve25519,
2374                    vec![KeyMechanism::EdDsaSignature],
2375                    None,
2376                    SignatureType::EdDsa,
2377                    CryptographicKeyContext::OpenPgp {
2378                        user_ids: OpenPgpUserIdList::new(vec![
2379                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2380                        ])?,
2381                        version: "v4".parse()?,
2382                    },
2383                )?,
2384                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2385                system_user: "signing-user".parse()?,
2386                tag: "signing1".to_string(),
2387            }
2388        ]),
2389    )]
2390    #[case::no_mappings(
2391        "Error message for NetHsmConfig::new with no user mappings",
2392        BTreeSet::from_iter([
2393            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2394            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2395        ]),
2396        BTreeSet::new(),
2397    )]
2398    #[case::duplicate_system_user_ids(
2399        "Error message for NetHsmConfig::new with two duplicate system user IDs",
2400        BTreeSet::from_iter([
2401            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2402            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2403        ]),
2404        BTreeSet::from_iter([
2405            NetHsmUserMapping::Admin("admin".parse()?),
2406            NetHsmUserMapping::Backup{
2407                backend_user: "backup".parse()?,
2408                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2409                system_user: "backup-user".parse()?,
2410            },
2411            NetHsmUserMapping::HermeticMetrics {
2412                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2413                system_user: "hermetic-metrics-user".parse()?,
2414            },
2415            NetHsmUserMapping::Metrics {
2416                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2417                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2418                system_user: "backup-user".parse()?,
2419            },
2420            NetHsmUserMapping::Signing {
2421                backend_user: "signing".parse()?,
2422                signing_key_id: "signing1".parse()?,
2423                key_setup: SigningKeySetup::new(
2424                    KeyType::Curve25519,
2425                    vec![KeyMechanism::EdDsaSignature],
2426                    None,
2427                    SignatureType::EdDsa,
2428                    CryptographicKeyContext::OpenPgp {
2429                        user_ids: OpenPgpUserIdList::new(vec![
2430                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2431                        ])?,
2432                        version: "v4".parse()?,
2433                    },
2434                )?,
2435                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2436                system_user: "signing-user".parse()?,
2437                tag: "signing1".to_string(),
2438            }
2439        ]),
2440    )]
2441    #[case::duplicate_ssh_public_keys(
2442        "Error message for NetHsmConfig::new with two duplicate SSH public keys as authorized keys",
2443        BTreeSet::from_iter([
2444            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2445            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2446        ]),
2447        BTreeSet::from_iter([
2448            NetHsmUserMapping::Admin("admin".parse()?),
2449            NetHsmUserMapping::Backup{
2450                backend_user: "backup".parse()?,
2451                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2452                system_user: "backup-user".parse()?,
2453            },
2454            NetHsmUserMapping::HermeticMetrics {
2455                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2456                system_user: "hermetic-metrics-user".parse()?,
2457            },
2458            NetHsmUserMapping::Metrics {
2459                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2460                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user2@other-host".parse()?,
2461                system_user: "metrics-user".parse()?,
2462            },
2463            NetHsmUserMapping::Signing {
2464                backend_user: "signing".parse()?,
2465                signing_key_id: "signing1".parse()?,
2466                key_setup: SigningKeySetup::new(
2467                    KeyType::Curve25519,
2468                    vec![KeyMechanism::EdDsaSignature],
2469                    None,
2470                    SignatureType::EdDsa,
2471                    CryptographicKeyContext::OpenPgp {
2472                        user_ids: OpenPgpUserIdList::new(vec![
2473                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2474                        ])?,
2475                        version: "v4".parse()?,
2476                    },
2477                )?,
2478                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2479                system_user: "signing-user".parse()?,
2480                tag: "signing1".to_string(),
2481            }
2482        ]),
2483    )]
2484    #[case::missing_system_wide_administrator(
2485        "Error message for NetHsmConfig::new with a system-wide administrator missing",
2486        BTreeSet::from_iter([
2487            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2488            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2489        ]),
2490        BTreeSet::from_iter([
2491            NetHsmUserMapping::Backup{
2492                backend_user: "backup".parse()?,
2493                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2494                system_user: "backup-user".parse()?,
2495            },
2496            NetHsmUserMapping::HermeticMetrics {
2497                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2498                system_user: "hermetic-metrics-user".parse()?,
2499            },
2500            NetHsmUserMapping::Metrics {
2501                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2502                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2503                system_user: "metrics-user".parse()?,
2504            },
2505            NetHsmUserMapping::Signing {
2506                backend_user: "signing".parse()?,
2507                signing_key_id: "signing1".parse()?,
2508                key_setup: SigningKeySetup::new(
2509                    KeyType::Curve25519,
2510                    vec![KeyMechanism::EdDsaSignature],
2511                    None,
2512                    SignatureType::EdDsa,
2513                    CryptographicKeyContext::OpenPgp {
2514                        user_ids: OpenPgpUserIdList::new(vec![
2515                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2516                        ])?,
2517                        version: "v4".parse()?,
2518                    },
2519                )?,
2520                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2521                system_user: "signing-user".parse()?,
2522                tag: "signing1".to_string(),
2523            }
2524        ]),
2525    )]
2526    #[case::duplicate_system_wide_backend_user_ids(
2527        "Error message for NetHsmConfig::new with two duplicate system-wide backend user IDs",
2528        BTreeSet::from_iter([
2529            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2530            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2531        ]),
2532        BTreeSet::from_iter([
2533            NetHsmUserMapping::Admin("admin".parse()?),
2534            NetHsmUserMapping::Admin("backup".parse()?),
2535            NetHsmUserMapping::Backup{
2536                backend_user: "backup".parse()?,
2537                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2538                system_user: "backup-user".parse()?,
2539            },
2540            NetHsmUserMapping::HermeticMetrics {
2541                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2542                system_user: "hermetic-metrics-user".parse()?,
2543            },
2544            NetHsmUserMapping::Metrics {
2545                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2546                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2547                system_user: "metrics-user".parse()?,
2548            },
2549            NetHsmUserMapping::Signing {
2550                backend_user: "signing".parse()?,
2551                signing_key_id: "signing1".parse()?,
2552                key_setup: SigningKeySetup::new(
2553                    KeyType::Curve25519,
2554                    vec![KeyMechanism::EdDsaSignature],
2555                    None,
2556                    SignatureType::EdDsa,
2557                    CryptographicKeyContext::OpenPgp {
2558                        user_ids: OpenPgpUserIdList::new(vec![
2559                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2560                        ])?,
2561                        version: "v4".parse()?,
2562                    },
2563                )?,
2564                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2565                system_user: "signing-user".parse()?,
2566                tag: "signing1".to_string(),
2567            }
2568        ]),
2569    )]
2570    #[case::duplicate_namespaced_backend_user_ids(
2571        "Error message for NetHsmConfig::new with two duplicate namespaced backend user IDs",
2572        BTreeSet::from_iter([
2573            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2574            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2575        ]),
2576        BTreeSet::from_iter([
2577            NetHsmUserMapping::Admin("admin".parse()?),
2578            NetHsmUserMapping::Admin("ns1~admin".parse()?),
2579            NetHsmUserMapping::Signing {
2580                backend_user: "ns1~signing1".parse()?,
2581                signing_key_id: "signing1".parse()?,
2582                key_setup: SigningKeySetup::new(
2583                    KeyType::Curve25519,
2584                    vec![KeyMechanism::EdDsaSignature],
2585                    None,
2586                    SignatureType::EdDsa,
2587                    CryptographicKeyContext::OpenPgp {
2588                        user_ids: OpenPgpUserIdList::new(vec![
2589                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2590                        ])?,
2591                        version: "v4".parse()?,
2592                    },
2593                )?,
2594                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2595                system_user: "ns1-signing-user1".parse()?,
2596                tag: "signing1".to_string(),
2597            },
2598            NetHsmUserMapping::Signing {
2599                backend_user: "ns1~signing1".parse()?,
2600                signing_key_id: "signing2".parse()?,
2601                key_setup: SigningKeySetup::new(
2602                    KeyType::Curve25519,
2603                    vec![KeyMechanism::EdDsaSignature],
2604                    None,
2605                    SignatureType::EdDsa,
2606                    CryptographicKeyContext::OpenPgp {
2607                        user_ids: OpenPgpUserIdList::new(vec![
2608                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2609                        ])?,
2610                        version: "v4".parse()?,
2611                    },
2612                )?,
2613                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2614                system_user: "ns1-signing-user2".parse()?,
2615                tag: "signing2".to_string(),
2616            }
2617        ]),
2618    )]
2619    #[case::duplicate_system_wide_key_ids(
2620        "Error message for NetHsmConfig::new with two duplicate system-wide key IDs",
2621        BTreeSet::from_iter([
2622            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2623            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2624        ]),
2625        BTreeSet::from_iter([
2626            NetHsmUserMapping::Admin("admin".parse()?),
2627            NetHsmUserMapping::Backup{
2628                backend_user: "backup".parse()?,
2629                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2630                system_user: "backup-user".parse()?,
2631            },
2632            NetHsmUserMapping::HermeticMetrics {
2633                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2634                system_user: "hermetic-metrics-user".parse()?,
2635            },
2636            NetHsmUserMapping::Metrics {
2637                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2638                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2639                system_user: "metrics-user".parse()?,
2640            },
2641            NetHsmUserMapping::Signing {
2642                backend_user: "signing1".parse()?,
2643                signing_key_id: "signing1".parse()?,
2644                key_setup: SigningKeySetup::new(
2645                    KeyType::Curve25519,
2646                    vec![KeyMechanism::EdDsaSignature],
2647                    None,
2648                    SignatureType::EdDsa,
2649                    CryptographicKeyContext::OpenPgp {
2650                        user_ids: OpenPgpUserIdList::new(vec![
2651                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2652                        ])?,
2653                        version: "v4".parse()?,
2654                    },
2655                )?,
2656                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2657                system_user: "signing-user".parse()?,
2658                tag: "signing1".to_string(),
2659            },
2660            NetHsmUserMapping::Signing {
2661                backend_user: "signing2".parse()?,
2662                signing_key_id: "signing1".parse()?,
2663                key_setup: SigningKeySetup::new(
2664                    KeyType::Curve25519,
2665                    vec![KeyMechanism::EdDsaSignature],
2666                    None,
2667                    SignatureType::EdDsa,
2668                    CryptographicKeyContext::OpenPgp {
2669                        user_ids: OpenPgpUserIdList::new(vec![
2670                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2671                        ])?,
2672                        version: "v4".parse()?,
2673                    },
2674                )?,
2675                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2676                system_user: "signing-user2".parse()?,
2677                tag: "signing2".to_string(),
2678            }
2679        ]),
2680    )]
2681    #[case::duplicate_system_wide_tags(
2682        "Error message for NetHsmConfig::new with two duplicate system-wide tags",
2683        BTreeSet::from_iter([
2684            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2685            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2686        ]),
2687        BTreeSet::from_iter([
2688            NetHsmUserMapping::Admin("admin".parse()?),
2689            NetHsmUserMapping::Backup{
2690                backend_user: "backup".parse()?,
2691                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2692                system_user: "backup-user".parse()?,
2693            },
2694            NetHsmUserMapping::HermeticMetrics {
2695                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2696                system_user: "hermetic-metrics-user".parse()?,
2697            },
2698            NetHsmUserMapping::Metrics {
2699                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2700                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2701                system_user: "metrics-user".parse()?,
2702            },
2703            NetHsmUserMapping::Signing {
2704                backend_user: "signing1".parse()?,
2705                signing_key_id: "signing1".parse()?,
2706                key_setup: SigningKeySetup::new(
2707                    KeyType::Curve25519,
2708                    vec![KeyMechanism::EdDsaSignature],
2709                    None,
2710                    SignatureType::EdDsa,
2711                    CryptographicKeyContext::OpenPgp {
2712                        user_ids: OpenPgpUserIdList::new(vec![
2713                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2714                        ])?,
2715                        version: "v4".parse()?,
2716                    },
2717                )?,
2718                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2719                system_user: "signing-user".parse()?,
2720                tag: "signing1".to_string(),
2721            },
2722            NetHsmUserMapping::Signing {
2723                backend_user: "signing2".parse()?,
2724                signing_key_id: "signing2".parse()?,
2725                key_setup: SigningKeySetup::new(
2726                    KeyType::Curve25519,
2727                    vec![KeyMechanism::EdDsaSignature],
2728                    None,
2729                    SignatureType::EdDsa,
2730                    CryptographicKeyContext::OpenPgp {
2731                        user_ids: OpenPgpUserIdList::new(vec![
2732                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2733                        ])?,
2734                        version: "v4".parse()?,
2735                    },
2736                )?,
2737                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2738                system_user: "signing-user2".parse()?,
2739                tag: "signing1".to_string(),
2740            }
2741        ]),
2742    )]
2743    #[case::missing_namespace_administrator(
2744        "Error message for NetHsmConfig::new with a missing namespace administrator",
2745        BTreeSet::from_iter([
2746            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2747            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2748        ]),
2749        BTreeSet::from_iter([
2750            NetHsmUserMapping::Admin("admin".parse()?),
2751            NetHsmUserMapping::Signing {
2752                backend_user: "ns1~signing1".parse()?,
2753                signing_key_id: "signing1".parse()?,
2754                key_setup: SigningKeySetup::new(
2755                    KeyType::Curve25519,
2756                    vec![KeyMechanism::EdDsaSignature],
2757                    None,
2758                    SignatureType::EdDsa,
2759                    CryptographicKeyContext::OpenPgp {
2760                        user_ids: OpenPgpUserIdList::new(vec![
2761                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2762                        ])?,
2763                        version: "v4".parse()?,
2764                    },
2765                )?,
2766                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2767                system_user: "ns1-signing-user".parse()?,
2768                tag: "signing1".to_string(),
2769            },
2770        ]),
2771    )]
2772    #[case::duplicate_namespace_key_ids(
2773        "Error message for NetHsmConfig::new with two duplicate namespaced key IDs",
2774        BTreeSet::from_iter([
2775            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2776            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2777        ]),
2778        BTreeSet::from_iter([
2779            NetHsmUserMapping::Admin("admin".parse()?),
2780            NetHsmUserMapping::Admin("ns1~admin".parse()?),
2781            NetHsmUserMapping::Signing {
2782                backend_user: "ns1~signing1".parse()?,
2783                signing_key_id: "signing1".parse()?,
2784                key_setup: SigningKeySetup::new(
2785                    KeyType::Curve25519,
2786                    vec![KeyMechanism::EdDsaSignature],
2787                    None,
2788                    SignatureType::EdDsa,
2789                    CryptographicKeyContext::OpenPgp {
2790                        user_ids: OpenPgpUserIdList::new(vec![
2791                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2792                        ])?,
2793                        version: "v4".parse()?,
2794                    },
2795                )?,
2796                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2797                system_user: "ns1-signing-user".parse()?,
2798                tag: "signing1".to_string(),
2799            },
2800            NetHsmUserMapping::Signing {
2801                backend_user: "ns1~signing2".parse()?,
2802                signing_key_id: "signing1".parse()?,
2803                key_setup: SigningKeySetup::new(
2804                    KeyType::Curve25519,
2805                    vec![KeyMechanism::EdDsaSignature],
2806                    None,
2807                    SignatureType::EdDsa,
2808                    CryptographicKeyContext::OpenPgp {
2809                        user_ids: OpenPgpUserIdList::new(vec![
2810                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2811                        ])?,
2812                        version: "v4".parse()?,
2813                    },
2814                )?,
2815                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2816                system_user: "ns1-signing-user2".parse()?,
2817                tag: "signing2".to_string(),
2818            }
2819        ]),
2820    )]
2821    #[case::duplicate_namespace_tags(
2822        "Error message for NetHsmConfig::new with two duplicate namespaced tags",
2823        BTreeSet::from_iter([
2824            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2825            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2826        ]),
2827        BTreeSet::from_iter([
2828            NetHsmUserMapping::Admin("admin".parse()?),
2829            NetHsmUserMapping::Admin("ns1~admin".parse()?),
2830            NetHsmUserMapping::Signing {
2831                backend_user: "ns1~signing1".parse()?,
2832                signing_key_id: "signing1".parse()?,
2833                key_setup: SigningKeySetup::new(
2834                    KeyType::Curve25519,
2835                    vec![KeyMechanism::EdDsaSignature],
2836                    None,
2837                    SignatureType::EdDsa,
2838                    CryptographicKeyContext::OpenPgp {
2839                        user_ids: OpenPgpUserIdList::new(vec![
2840                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2841                        ])?,
2842                        version: "v4".parse()?,
2843                    },
2844                )?,
2845                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2846                system_user: "ns1-signing-user".parse()?,
2847                tag: "signing1".to_string(),
2848            },
2849            NetHsmUserMapping::Signing {
2850                backend_user: "ns1~signing2".parse()?,
2851                signing_key_id: "signing2".parse()?,
2852                key_setup: SigningKeySetup::new(
2853                    KeyType::Curve25519,
2854                    vec![KeyMechanism::EdDsaSignature],
2855                    None,
2856                    SignatureType::EdDsa,
2857                    CryptographicKeyContext::OpenPgp {
2858                        user_ids: OpenPgpUserIdList::new(vec![
2859                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2860                        ])?,
2861                        version: "v4".parse()?,
2862                    },
2863                )?,
2864                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2865                system_user: "ns1-signing-user2".parse()?,
2866                tag: "signing1".to_string(),
2867            }
2868        ]),
2869    )]
2870    #[case::all_the_issues(
2871        "Error message for NetHsmConfig::new with multiple validation issues (connections and mappings)",
2872        BTreeSet::from_iter([
2873            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2874            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Native),
2875        ]),
2876        BTreeSet::from_iter([
2877            NetHsmUserMapping::Signing {
2878                backend_user: "signing1".parse()?,
2879                signing_key_id: "signing1".parse()?,
2880                key_setup: SigningKeySetup::new(
2881                    KeyType::Curve25519,
2882                    vec![KeyMechanism::EdDsaSignature],
2883                    None,
2884                    SignatureType::EdDsa,
2885                    CryptographicKeyContext::OpenPgp {
2886                        user_ids: OpenPgpUserIdList::new(vec![
2887                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2888                        ])?,
2889                        version: "v4".parse()?,
2890                    },
2891                )?,
2892                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
2893                system_user: "ns1-signing-user1".parse()?,
2894                tag: "signing1".to_string(),
2895            },
2896            NetHsmUserMapping::Signing {
2897                backend_user: "signing1".parse()?,
2898                signing_key_id: "signing1".parse()?,
2899                key_setup: SigningKeySetup::new(
2900                    KeyType::Curve25519,
2901                    vec![KeyMechanism::EdDsaSignature],
2902                    None,
2903                    SignatureType::EdDsa,
2904                    CryptographicKeyContext::OpenPgp {
2905                        user_ids: OpenPgpUserIdList::new(vec![
2906                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2907                        ])?,
2908                        version: "v4".parse()?,
2909                    },
2910                )?,
2911                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2912                system_user: "ns1-signing-user1".parse()?,
2913                tag: "signing1".to_string(),
2914            },
2915            NetHsmUserMapping::Signing {
2916                backend_user: "ns1~signing1".parse()?,
2917                signing_key_id: "signing1".parse()?,
2918                key_setup: SigningKeySetup::new(
2919                    KeyType::Curve25519,
2920                    vec![KeyMechanism::EdDsaSignature],
2921                    None,
2922                    SignatureType::EdDsa,
2923                    CryptographicKeyContext::OpenPgp {
2924                        user_ids: OpenPgpUserIdList::new(vec![
2925                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2926                        ])?,
2927                        version: "v4".parse()?,
2928                    },
2929                )?,
2930                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2931                system_user: "ns1-signing-user1".parse()?,
2932                tag: "signing1".to_string(),
2933            },
2934            NetHsmUserMapping::Signing {
2935                backend_user: "ns1~signing1".parse()?,
2936                signing_key_id: "signing2".parse()?,
2937                key_setup: SigningKeySetup::new(
2938                    KeyType::Curve25519,
2939                    vec![KeyMechanism::EdDsaSignature],
2940                    None,
2941                    SignatureType::EdDsa,
2942                    CryptographicKeyContext::OpenPgp {
2943                        user_ids: OpenPgpUserIdList::new(vec![
2944                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2945                        ])?,
2946                        version: "v4".parse()?,
2947                    },
2948                )?,
2949                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2950                system_user: "ns1-signing-user2".parse()?,
2951                tag: "signing1".to_string(),
2952            },
2953            NetHsmUserMapping::Signing {
2954                backend_user: "ns1~signing2".parse()?,
2955                signing_key_id: "signing2".parse()?,
2956                key_setup: SigningKeySetup::new(
2957                    KeyType::Curve25519,
2958                    vec![KeyMechanism::EdDsaSignature],
2959                    None,
2960                    SignatureType::EdDsa,
2961                    CryptographicKeyContext::OpenPgp {
2962                        user_ids: OpenPgpUserIdList::new(vec![
2963                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2964                        ])?,
2965                        version: "v4".parse()?,
2966                    },
2967                )?,
2968                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2969                system_user: "ns1-signing-user1".parse()?,
2970                tag: "signing1".to_string(),
2971            },
2972        ]),
2973    )]
2974    fn nethsm_config_new_fails_validation(
2975        #[case] description: &str,
2976        #[case] connections: BTreeSet<Connection>,
2977        #[case] mappings: BTreeSet<NetHsmUserMapping>,
2978    ) -> TestResult {
2979        let error_msg = match NetHsmConfig::new(connections, mappings) {
2980            Err(crate::Error::Validation { source, .. }) => source.to_string(),
2981            Ok(config) => {
2982                panic!("Expected to fail with Error::Validation, but succeeded instead: {config:?}")
2983            }
2984            Err(error) => panic!(
2985                "Expected to fail with Error::Validation, but failed with a different error instead: {error}"
2986            ),
2987        };
2988
2989        with_settings!({
2990            description => description,
2991            snapshot_path => SNAPSHOT_PATH,
2992            prepend_module_to_snapshot => false,
2993        }, {
2994            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_msg);
2995        });
2996        Ok(())
2997    }
2998}