Skip to main content

signstar_config/nethsm/
config.rs

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