Skip to main content

signstar_config/nethsm/
config.rs

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