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