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 =
953        duplicate_domains(value, None, Some(" system-wide".to_string()), Some("tag"));
954
955    // Collect all namespace IDs.
956    let all_namespaces = {
957        let mut all_namespaces = Vec::from_iter(
958            value
959                .iter()
960                .flat_map(|mapping| mapping.namespaces())
961                .collect::<HashSet<_>>(),
962        );
963        all_namespaces.sort();
964        all_namespaces
965    };
966
967    // Collect all namespace IDs without an admin user.
968    let namespaces_without_admin = {
969        let mut all_namespaces: HashSet<&NamespaceId> = HashSet::from_iter(all_namespaces.clone());
970
971        for mapping in value.iter() {
972            if let NetHsmUserMapping::Admin(user_id) = mapping
973                && let Some(namespace) = user_id.namespace()
974            {
975                all_namespaces.remove(namespace);
976            }
977        }
978
979        if all_namespaces.is_empty() {
980            None
981        } else {
982            let mut namespaces_without_admin = all_namespaces
983                .iter()
984                .map(|namespace| format!("\"{namespace}\""))
985                .collect::<Vec<_>>();
986            namespaces_without_admin.sort();
987            Some(format!(
988                "the namespace{} {} without an administrator user",
989                if namespaces_without_admin.len() > 1 {
990                    "s"
991                } else {
992                    ""
993                },
994                namespaces_without_admin.join(", ")
995            ))
996        }
997    };
998
999    // Collect all duplicate namespaced key IDs.
1000    let duplicate_namespaced_key_ids = {
1001        let mut all_duplicates = Vec::new();
1002
1003        for namespace in all_namespaces.iter() {
1004            let mut duplicates = duplicate_key_ids(
1005                value,
1006                &NetHsmBackendKeyIdFilter {
1007                    namespace: Some(namespace),
1008                },
1009                Some(format!(" \"{namespace}\" namespaced")),
1010            );
1011            if let Some(message) = duplicates.take() {
1012                all_duplicates.push(message)
1013            }
1014        }
1015
1016        if all_duplicates.is_empty() {
1017            None
1018        } else {
1019            Some(all_duplicates.join("\n"))
1020        }
1021    };
1022
1023    // Collect all duplicate namespaced tags.
1024    let duplicate_namespaced_tags = {
1025        let mut all_duplicates = Vec::new();
1026
1027        for namespace in all_namespaces.iter() {
1028            let mut duplicates = duplicate_domains(
1029                value,
1030                Some(&NetHsmConfigDomainFilter {
1031                    namespace: Some(namespace),
1032                }),
1033                Some(format!(" \"{namespace}\" namespaced")),
1034                Some("tag"),
1035            );
1036            if let Some(message) = duplicates.take() {
1037                all_duplicates.push(message)
1038            }
1039        }
1040
1041        if all_duplicates.is_empty() {
1042            None
1043        } else {
1044            Some(all_duplicates.join("\n"))
1045        }
1046    };
1047
1048    let messages = [
1049        duplicate_system_user_ids,
1050        duplicate_authorized_keys,
1051        missing_system_wide_admin,
1052        duplicate_backend_user_ids,
1053        duplicate_system_wide_key_ids,
1054        duplicate_system_wide_tags,
1055        namespaces_without_admin,
1056        duplicate_namespaced_key_ids,
1057        duplicate_namespaced_tags,
1058    ];
1059    let error_messages = {
1060        let mut error_messages = Vec::new();
1061
1062        for message in messages.iter().flatten() {
1063            error_messages.push(message.as_str());
1064        }
1065
1066        error_messages
1067    };
1068
1069    match error_messages.len() {
1070        0 => Ok(()),
1071        1 => Err(garde::Error::new(format!(
1072            "contains {}",
1073            error_messages.join("\n")
1074        ))),
1075        _ => Err(garde::Error::new(format!(
1076            "contains multiple issues:\n⤷ {}",
1077            error_messages.join("\n⤷ ")
1078        ))),
1079    }
1080}
1081
1082/// The configuration items for a NetHSM.
1083///
1084/// Tracks a set of connections to a NetHSM backend and user mappings that are present on each of
1085/// them.
1086#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Validate)]
1087#[serde(rename_all = "snake_case")]
1088pub struct NetHsmConfig {
1089    #[garde(custom(validate_nethsm_config_connections))]
1090    connections: BTreeSet<Connection>,
1091
1092    #[garde(custom(validate_nethsm_config_mappings))]
1093    mappings: BTreeSet<NetHsmUserMapping>,
1094}
1095
1096impl NetHsmConfig {
1097    /// Creates a new [`NetHsmConfig`] from a set of [`Connection`] and a set of
1098    /// [`NetHsmUserMapping`] items.
1099    pub fn new(
1100        connections: BTreeSet<Connection>,
1101        mappings: BTreeSet<NetHsmUserMapping>,
1102    ) -> Result<Self, crate::Error> {
1103        let config = Self {
1104            connections,
1105            mappings,
1106        };
1107
1108        config
1109            .validate()
1110            .map_err(|source| crate::Error::Validation {
1111                context: "validating a NetHSM specific configuration item".to_string(),
1112                source,
1113            })?;
1114
1115        Ok(config)
1116    }
1117
1118    /// Returns a reference to the set of [`Connection`] objects.
1119    pub fn connections(&self) -> &BTreeSet<Connection> {
1120        &self.connections
1121    }
1122
1123    /// Returns a reference to the set of [`NetHsmUserMapping`] objects.
1124    pub fn mappings(&self) -> &BTreeSet<NetHsmUserMapping> {
1125        &self.mappings
1126    }
1127}
1128
1129impl ConfigAuthorizedKeyEntries for NetHsmConfig {
1130    fn authorized_key_entries(&self) -> HashSet<&AuthorizedKeyEntry> {
1131        self.mappings
1132            .iter()
1133            .filter_map(|mapping| mapping.authorized_key_entry())
1134            .collect()
1135    }
1136}
1137
1138impl ConfigSystemUserIds for NetHsmConfig {
1139    fn system_user_ids(&self) -> HashSet<&SystemUserId> {
1140        self.mappings
1141            .iter()
1142            .filter_map(|mapping| mapping.system_user_id())
1143            .collect()
1144    }
1145}
1146
1147/// The state of a NetHSM configuration.
1148///
1149/// Tracks the available backend users, their roles and assigned tags, as well as the key setups
1150/// associated with users.
1151#[derive(Debug, Eq, PartialEq)]
1152pub struct NetHsmConfigState<'a> {
1153    /// The user states.
1154    pub(crate) user_data: Vec<NetHsmConfigUserData<'a>>,
1155    /// The key states.
1156    pub(crate) key_data: Vec<NetHsmConfigUserKeyData<'a>>,
1157}
1158
1159impl<'a> NetHsmConfigState<'a> {
1160    /// The name of the origin for the state
1161    pub const STATE_NAME: &'static str = "NetHSM config";
1162}
1163
1164impl<'a> From<&'a NetHsmConfig> for NetHsmConfigState<'a> {
1165    fn from(value: &'a NetHsmConfig) -> Self {
1166        let (key_data, user_data) = {
1167            let mut key_data = Vec::new();
1168            let mut user_data = Vec::new();
1169
1170            for mapping in value.mappings() {
1171                if let Some(user_key_data) =
1172                    mapping.nethsm_config_user_key_data(NetHsmUserKeysFilter::All)
1173                {
1174                    key_data.push(user_key_data)
1175                }
1176                user_data.extend(mapping.nethsm_config_user_data());
1177            }
1178
1179            (key_data, user_data)
1180        };
1181
1182        Self {
1183            user_data,
1184            key_data,
1185        }
1186    }
1187}
1188
1189impl<'a> StateOriginInfo for NetHsmConfigState<'a> {
1190    fn state_name(&self) -> &str {
1191        Self::STATE_NAME
1192    }
1193
1194    fn state_origin(&self) -> StateOrigin {
1195        StateOrigin::Config
1196    }
1197}
1198
1199impl<'a> PartialEq<NetHsmBackendState> for NetHsmConfigState<'a> {
1200    fn eq(&self, other: &NetHsmBackendState) -> bool {
1201        if self.user_data.len() != other.user_states.len()
1202            || self.key_data.len() != other.key_states.len()
1203        {
1204            return false;
1205        }
1206
1207        {
1208            let mut remaining_other_user_states: HashSet<&UserState> =
1209                HashSet::from_iter(other.user_states.iter());
1210            'outer: for self_user_data in self.user_data.iter() {
1211                for other_user_state in remaining_other_user_states.iter() {
1212                    if self_user_data == *other_user_state {
1213                        remaining_other_user_states.remove(*other_user_state);
1214                        continue 'outer;
1215                    }
1216                }
1217                return false;
1218            }
1219            if !remaining_other_user_states.is_empty() {
1220                return false;
1221            }
1222        }
1223
1224        {
1225            let mut remaining_other_key_states: HashSet<&KeyState> =
1226                HashSet::from_iter(other.key_states.iter());
1227            'outer: for self_key_data in self.key_data.iter() {
1228                for other_user_state in remaining_other_key_states.iter() {
1229                    if self_key_data == *other_user_state {
1230                        remaining_other_key_states.remove(*other_user_state);
1231                        continue 'outer;
1232                    }
1233                }
1234                return false;
1235            }
1236            if !remaining_other_key_states.is_empty() {
1237                return false;
1238            }
1239        }
1240
1241        true
1242    }
1243}
1244
1245#[cfg(test)]
1246mod tests {
1247    use std::thread::current;
1248
1249    use insta::{assert_snapshot, with_settings};
1250    use log::debug;
1251    use rstest::{fixture, rstest};
1252    use signstar_crypto::{
1253        key::{CryptographicKeyContext, KeyMechanism, KeyType, SignatureType, SigningKeySetup},
1254        openpgp::OpenPgpUserIdList,
1255    };
1256    use testresult::TestResult;
1257
1258    use super::*;
1259
1260    const SNAPSHOT_PATH: &str = "fixtures/nethsm_config/";
1261
1262    #[test]
1263    fn nethsm_metrics_users_succeeds() -> TestResult {
1264        NetHsmMetricsUsers::new(
1265            SystemWideUserId::new("metrics".to_string())?,
1266            vec![
1267                UserId::new("operator".to_string())?,
1268                UserId::new("ns1~operator".to_string())?,
1269            ],
1270        )?;
1271        Ok(())
1272    }
1273
1274    #[test]
1275    fn nethsm_metrics_users_fails() -> TestResult {
1276        if let Ok(user) = NetHsmMetricsUsers::new(
1277            SystemWideUserId::new("metrics".to_string())?,
1278            vec![
1279                UserId::new("metrics".to_string())?,
1280                UserId::new("ns1~operator".to_string())?,
1281            ],
1282        ) {
1283            panic!("Succeeded creating a NetHsmMetricsUsers, but should have failed:\n{user:?}")
1284        }
1285        Ok(())
1286    }
1287
1288    #[rstest]
1289    #[case::admin(NetHsmUserMapping::Admin("admin".parse()?), vec!["admin".parse()?])]
1290    #[case::backup(
1291        NetHsmUserMapping::Backup{
1292            backend_user: "backup".parse()?,
1293            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1294            system_user: "backup-user".parse()?,
1295        },
1296        vec!["backup".parse()?],
1297    )]
1298    #[case::backup(
1299        NetHsmUserMapping::Metrics{
1300            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1301            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1302            system_user: "metrics-user".parse()?,
1303        },
1304        vec!["metrics".parse()?, "keymetrics".parse()?],
1305    )]
1306    #[case::backup(
1307        NetHsmUserMapping::Signing {
1308            backend_user: "signing".parse()?,
1309            signing_key_id: "signing1".parse()?,
1310            key_setup: SigningKeySetup::new(
1311                KeyType::Curve25519,
1312                vec![KeyMechanism::EdDsaSignature],
1313                None,
1314                SignatureType::EdDsa,
1315                CryptographicKeyContext::OpenPgp {
1316                    user_ids: OpenPgpUserIdList::new(vec![
1317                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1318                    ])?,
1319                    version: "v4".parse()?,
1320                },
1321            )?,
1322            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1323            system_user: "signing-user".parse()?,
1324            tag: "signing1".to_string(),
1325        },
1326        vec!["signing".parse()?],
1327    )]
1328    fn nethsm_user_mapping_nethsm_user_ids(
1329        #[case] mapping: NetHsmUserMapping,
1330        #[case] expected: Vec<UserId>,
1331    ) -> TestResult {
1332        assert_eq!(mapping.nethsm_user_ids(), expected,);
1333
1334        Ok(())
1335    }
1336
1337    #[rstest]
1338    #[case::admin(
1339        NetHsmUserMapping::Admin("admin".parse()?),
1340        vec![("admin".parse()?, UserRole::Administrator, None)],
1341    )]
1342    #[case::backup(
1343        NetHsmUserMapping::Backup{
1344            backend_user: "backup".parse()?,
1345            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1346            system_user: "backup-user".parse()?,
1347        },
1348        vec![("backup".parse()?, UserRole::Backup, None)],
1349    )]
1350    #[case::metrics(
1351        NetHsmUserMapping::Metrics{
1352            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1353            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1354            system_user: "metrics-user".parse()?,
1355        },
1356        vec![
1357            ("keymetrics".parse()?, UserRole::Operator, None),
1358            ("metrics".parse()?, UserRole::Metrics, None),
1359        ],
1360    )]
1361    #[case::signing(
1362        NetHsmUserMapping::Signing {
1363            backend_user: "signing".parse()?,
1364            signing_key_id: "signing1".parse()?,
1365            key_setup: SigningKeySetup::new(
1366                KeyType::Curve25519,
1367                vec![KeyMechanism::EdDsaSignature],
1368                None,
1369                SignatureType::EdDsa,
1370                CryptographicKeyContext::OpenPgp {
1371                    user_ids: OpenPgpUserIdList::new(vec![
1372                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1373                    ])?,
1374                    version: "v4".parse()?,
1375                },
1376            )?,
1377            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1378            system_user: "signing-user".parse()?,
1379            tag: "signing1".to_string(),
1380        },
1381        vec![("signing".parse()?, UserRole::Operator, Some("signing1"))],
1382    )]
1383    fn nethsm_user_mapping_nethsm_config_user_data(
1384        #[case] mapping: NetHsmUserMapping,
1385        #[case] expected: Vec<(UserId, UserRole, Option<&str>)>,
1386    ) -> TestResult {
1387        let expected = expected
1388            .iter()
1389            .map(|(user, role, tag)| NetHsmConfigUserData {
1390                user,
1391                role: *role,
1392                tag: *tag,
1393            })
1394            .collect::<HashSet<_>>();
1395        assert_eq!(mapping.nethsm_config_user_data(), expected);
1396
1397        Ok(())
1398    }
1399
1400    #[rstest]
1401    #[case::admin_filter_all(
1402        NetHsmUserMapping::Admin("admin".parse()?),
1403        NetHsmUserKeysFilter::All,
1404        None,
1405    )]
1406    #[case::admin_filter_namespace(
1407        NetHsmUserMapping::Admin("admin".parse()?),
1408        NetHsmUserKeysFilter::Namespaced,
1409        None,
1410    )]
1411    #[case::admin_filter_system_wide(
1412        NetHsmUserMapping::Admin("admin".parse()?),
1413        NetHsmUserKeysFilter::SystemWide,
1414        None,
1415    )]
1416    #[case::backup_filter_all(
1417        NetHsmUserMapping::Backup{
1418            backend_user: "backup".parse()?,
1419            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1420            system_user: "backup-user".parse()?,
1421        },
1422        NetHsmUserKeysFilter::All,
1423        None,
1424    )]
1425    #[case::backup_filter_namespaced(
1426        NetHsmUserMapping::Backup{
1427            backend_user: "backup".parse()?,
1428            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1429            system_user: "backup-user".parse()?,
1430        },
1431        NetHsmUserKeysFilter::Namespaced,
1432        None,
1433    )]
1434    #[case::backup_filter_system_wide(
1435        NetHsmUserMapping::Backup{
1436            backend_user: "backup".parse()?,
1437            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1438            system_user: "backup-user".parse()?,
1439        },
1440        NetHsmUserKeysFilter::SystemWide,
1441        None,
1442    )]
1443    #[case::metrics_filter_all(
1444        NetHsmUserMapping::Metrics{
1445            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1446            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1447            system_user: "metrics-user".parse()?,
1448        },
1449        NetHsmUserKeysFilter::All,
1450        None,
1451    )]
1452    #[case::metrics_filter_namespaced(
1453        NetHsmUserMapping::Metrics{
1454            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1455            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1456            system_user: "metrics-user".parse()?,
1457        },
1458        NetHsmUserKeysFilter::Namespaced,
1459        None,
1460    )]
1461    #[case::metrics_filter_system_wide(
1462        NetHsmUserMapping::Metrics{
1463            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1464            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1465            system_user: "metrics-user".parse()?,
1466        },
1467        NetHsmUserKeysFilter::SystemWide,
1468        None,
1469    )]
1470    #[case::signing_system_wide_filter_all(
1471        NetHsmUserMapping::Signing {
1472            backend_user: "signing".parse()?,
1473            signing_key_id: "signing1".parse()?,
1474            key_setup: SigningKeySetup::new(
1475                KeyType::Curve25519,
1476                vec![KeyMechanism::EdDsaSignature],
1477                None,
1478                SignatureType::EdDsa,
1479                CryptographicKeyContext::OpenPgp {
1480                    user_ids: OpenPgpUserIdList::new(vec![
1481                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1482                    ])?,
1483                    version: "v4".parse()?,
1484                },
1485            )?,
1486            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1487            system_user: "signing-user".parse()?,
1488            tag: "signing1".to_string(),
1489        },
1490        NetHsmUserKeysFilter::All,
1491        Some((
1492            "signing".parse()?,
1493            "signing1".parse()?,
1494            SigningKeySetup::new(
1495                KeyType::Curve25519,
1496                vec![KeyMechanism::EdDsaSignature],
1497                None,
1498                SignatureType::EdDsa,
1499                CryptographicKeyContext::OpenPgp {
1500                    user_ids: OpenPgpUserIdList::new(vec![
1501                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1502                    ])?,
1503                    version: "v4".parse()?,
1504                },
1505            )?,
1506            "signing1",
1507        )),
1508    )]
1509    #[case::signing_system_wide_filter_namespaced(
1510        NetHsmUserMapping::Signing {
1511            backend_user: "signing".parse()?,
1512            signing_key_id: "signing1".parse()?,
1513            key_setup: SigningKeySetup::new(
1514                KeyType::Curve25519,
1515                vec![KeyMechanism::EdDsaSignature],
1516                None,
1517                SignatureType::EdDsa,
1518                CryptographicKeyContext::OpenPgp {
1519                    user_ids: OpenPgpUserIdList::new(vec![
1520                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1521                    ])?,
1522                    version: "v4".parse()?,
1523                },
1524            )?,
1525            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1526            system_user: "signing-user".parse()?,
1527            tag: "signing1".to_string(),
1528        },
1529        NetHsmUserKeysFilter::Namespaced,
1530        None,
1531    )]
1532    #[case::signing_system_wide_filter_system_wide(
1533        NetHsmUserMapping::Signing {
1534            backend_user: "signing".parse()?,
1535            signing_key_id: "signing1".parse()?,
1536            key_setup: SigningKeySetup::new(
1537                KeyType::Curve25519,
1538                vec![KeyMechanism::EdDsaSignature],
1539                None,
1540                SignatureType::EdDsa,
1541                CryptographicKeyContext::OpenPgp {
1542                    user_ids: OpenPgpUserIdList::new(vec![
1543                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1544                    ])?,
1545                    version: "v4".parse()?,
1546                },
1547            )?,
1548            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1549            system_user: "signing-user".parse()?,
1550            tag: "signing1".to_string(),
1551        },
1552        NetHsmUserKeysFilter::SystemWide,
1553        Some((
1554            "signing".parse()?,
1555            "signing1".parse()?,
1556            SigningKeySetup::new(
1557                KeyType::Curve25519,
1558                vec![KeyMechanism::EdDsaSignature],
1559                None,
1560                SignatureType::EdDsa,
1561                CryptographicKeyContext::OpenPgp {
1562                    user_ids: OpenPgpUserIdList::new(vec![
1563                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1564                    ])?,
1565                    version: "v4".parse()?,
1566                },
1567            )?,
1568            "signing1",
1569        )),
1570    )]
1571    #[case::signing_namespaced_filter_all(
1572        NetHsmUserMapping::Signing {
1573            backend_user: "ns1~signing".parse()?,
1574            signing_key_id: "signing1".parse()?,
1575            key_setup: SigningKeySetup::new(
1576                KeyType::Curve25519,
1577                vec![KeyMechanism::EdDsaSignature],
1578                None,
1579                SignatureType::EdDsa,
1580                CryptographicKeyContext::OpenPgp {
1581                    user_ids: OpenPgpUserIdList::new(vec![
1582                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1583                    ])?,
1584                    version: "v4".parse()?,
1585                },
1586            )?,
1587            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1588            system_user: "signing-user".parse()?,
1589            tag: "signing1".to_string(),
1590        },
1591        NetHsmUserKeysFilter::All,
1592        Some((
1593            "ns1~signing".parse()?,
1594            "signing1".parse()?,
1595            SigningKeySetup::new(
1596                KeyType::Curve25519,
1597                vec![KeyMechanism::EdDsaSignature],
1598                None,
1599                SignatureType::EdDsa,
1600                CryptographicKeyContext::OpenPgp {
1601                    user_ids: OpenPgpUserIdList::new(vec![
1602                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1603                    ])?,
1604                    version: "v4".parse()?,
1605                },
1606            )?,
1607            "signing1",
1608        )),
1609    )]
1610    #[case::signing_system_wide_filter_namespaced(
1611        NetHsmUserMapping::Signing {
1612            backend_user: "ns1~signing".parse()?,
1613            signing_key_id: "signing1".parse()?,
1614            key_setup: SigningKeySetup::new(
1615                KeyType::Curve25519,
1616                vec![KeyMechanism::EdDsaSignature],
1617                None,
1618                SignatureType::EdDsa,
1619                CryptographicKeyContext::OpenPgp {
1620                    user_ids: OpenPgpUserIdList::new(vec![
1621                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1622                    ])?,
1623                    version: "v4".parse()?,
1624                },
1625            )?,
1626            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1627            system_user: "signing-user".parse()?,
1628            tag: "signing1".to_string(),
1629        },
1630        NetHsmUserKeysFilter::Namespaced,
1631        Some((
1632            "ns1~signing".parse()?,
1633            "signing1".parse()?,
1634            SigningKeySetup::new(
1635                KeyType::Curve25519,
1636                vec![KeyMechanism::EdDsaSignature],
1637                None,
1638                SignatureType::EdDsa,
1639                CryptographicKeyContext::OpenPgp {
1640                    user_ids: OpenPgpUserIdList::new(vec![
1641                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1642                    ])?,
1643                    version: "v4".parse()?,
1644                },
1645            )?,
1646            "signing1",
1647        )),
1648    )]
1649    #[case::signing_system_wide_filter_system_wide(
1650        NetHsmUserMapping::Signing {
1651            backend_user: "ns1~signing".parse()?,
1652            signing_key_id: "signing1".parse()?,
1653            key_setup: SigningKeySetup::new(
1654                KeyType::Curve25519,
1655                vec![KeyMechanism::EdDsaSignature],
1656                None,
1657                SignatureType::EdDsa,
1658                CryptographicKeyContext::OpenPgp {
1659                    user_ids: OpenPgpUserIdList::new(vec![
1660                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1661                    ])?,
1662                    version: "v4".parse()?,
1663                },
1664            )?,
1665            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1666            system_user: "signing-user".parse()?,
1667            tag: "signing1".to_string(),
1668        },
1669        NetHsmUserKeysFilter::SystemWide,
1670        None,
1671    )]
1672    fn nethsm_user_mapping_nethsm_config_user_key_data(
1673        #[case] mapping: NetHsmUserMapping,
1674        #[case] filter: NetHsmUserKeysFilter,
1675        #[case] expected: Option<(UserId, KeyId, SigningKeySetup, &str)>,
1676    ) -> TestResult {
1677        let expected =
1678            expected
1679                .as_ref()
1680                .map(|(user, key_id, key_setup, tag)| NetHsmConfigUserKeyData {
1681                    user,
1682                    key_id,
1683                    key_setup,
1684                    tag,
1685                });
1686        assert_eq!(mapping.nethsm_config_user_key_data(filter), expected);
1687
1688        Ok(())
1689    }
1690
1691    #[rstest]
1692    #[case::admin_filter_admin(
1693        NetHsmUserMapping::Admin("admin".parse()?),
1694        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1695        vec!["admin"]
1696    )]
1697    #[case::admin_filter_any(
1698        NetHsmUserMapping::Admin("admin".parse()?),
1699        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1700        vec!["admin"]
1701    )]
1702    #[case::backup_filter_any(
1703        NetHsmUserMapping::Backup{
1704            backend_user: "backup".parse()?,
1705            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1706            system_user: "backup-user".parse()?,
1707        },
1708        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1709        vec!["backup"]
1710    )]
1711    #[case::backup_filter_backup(
1712        NetHsmUserMapping::Backup{
1713            backend_user: "backup".parse()?,
1714            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1715            system_user: "backup-user".parse()?,
1716        },
1717        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1718        vec!["backup"]
1719    )]
1720    #[case::backup_filter_non_admin(
1721        NetHsmUserMapping::Backup{
1722            backend_user: "backup".parse()?,
1723            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1724            system_user: "backup-user".parse()?,
1725        },
1726        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1727        vec!["backup"]
1728    )]
1729    #[case::hermetic_metrics_filter_any(
1730        NetHsmUserMapping::HermeticMetrics {
1731            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1732            system_user: "metrics-user".parse()?,
1733        },
1734        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1735        vec!["metrics", "keymetrics"]
1736    )]
1737    #[case::hermetic_metrics_filter_metrics(
1738        NetHsmUserMapping::HermeticMetrics {
1739            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1740            system_user: "metrics-user".parse()?,
1741        },
1742        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1743        vec!["metrics"]
1744    )]
1745    #[case::hermetic_metrics_filter_non_admin(
1746        NetHsmUserMapping::HermeticMetrics {
1747            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1748            system_user: "metrics-user".parse()?,
1749        },
1750        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1751        vec!["metrics", "keymetrics"]
1752    )]
1753    #[case::hermetic_metrics_filter_observer(
1754        NetHsmUserMapping::HermeticMetrics {
1755            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1756            system_user: "metrics-user".parse()?,
1757        },
1758        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1759        vec!["keymetrics"]
1760    )]
1761    #[case::signing_filter_any(
1762        NetHsmUserMapping::Signing {
1763            backend_user: "signing".parse()?,
1764            signing_key_id: "signing1".parse()?,
1765            key_setup: SigningKeySetup::new(
1766                KeyType::Curve25519,
1767                vec![KeyMechanism::EdDsaSignature],
1768                None,
1769                SignatureType::EdDsa,
1770                CryptographicKeyContext::OpenPgp {
1771                    user_ids: OpenPgpUserIdList::new(vec![
1772                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1773                    ])?,
1774                    version: "v4".parse()?,
1775                },
1776            )?,
1777            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1778            system_user: "signing-user".parse()?,
1779            tag: "signing1".to_string(),
1780        },
1781        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1782        vec!["signing"]
1783    )]
1784    #[case::signing_filter_non_admin(
1785        NetHsmUserMapping::Signing {
1786            backend_user: "signing".parse()?,
1787            signing_key_id: "signing1".parse()?,
1788            key_setup: SigningKeySetup::new(
1789                KeyType::Curve25519,
1790                vec![KeyMechanism::EdDsaSignature],
1791                None,
1792                SignatureType::EdDsa,
1793                CryptographicKeyContext::OpenPgp {
1794                    user_ids: OpenPgpUserIdList::new(vec![
1795                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1796                    ])?,
1797                    version: "v4".parse()?,
1798                },
1799            )?,
1800            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1801            system_user: "signing-user".parse()?,
1802            tag: "signing1".to_string(),
1803        },
1804        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1805        vec!["signing"]
1806    )]
1807    #[case::signing_filter_signing(
1808        NetHsmUserMapping::Signing {
1809            backend_user: "signing".parse()?,
1810            signing_key_id: "signing1".parse()?,
1811            key_setup: SigningKeySetup::new(
1812                KeyType::Curve25519,
1813                vec![KeyMechanism::EdDsaSignature],
1814                None,
1815                SignatureType::EdDsa,
1816                CryptographicKeyContext::OpenPgp {
1817                    user_ids: OpenPgpUserIdList::new(vec![
1818                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1819                    ])?,
1820                    version: "v4".parse()?,
1821                },
1822            )?,
1823            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1824            system_user: "signing-user".parse()?,
1825            tag: "signing1".to_string(),
1826        },
1827        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1828        vec!["signing"]
1829    )]
1830    fn nethsm_user_mapping_backend_user_ids_filter_matches(
1831        #[case] mapping: NetHsmUserMapping,
1832        #[case] filter: BackendUserIdFilter,
1833        #[case] expected: Vec<&str>,
1834    ) -> TestResult {
1835        assert_eq!(mapping.backend_user_ids(filter), expected);
1836
1837        Ok(())
1838    }
1839
1840    #[rstest]
1841    #[case::admin_filter_metrics(
1842        NetHsmUserMapping::Admin("admin".parse()?),
1843        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1844    )]
1845    #[case::admin_filter_non_admin(
1846        NetHsmUserMapping::Admin("admin".parse()?),
1847        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1848    )]
1849    #[case::admin_filter_observer(
1850        NetHsmUserMapping::Admin("admin".parse()?),
1851        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1852    )]
1853    #[case::admin_filter_signing(
1854        NetHsmUserMapping::Admin("admin".parse()?),
1855        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1856    )]
1857    #[case::backup_filter_admin(
1858        NetHsmUserMapping::Backup{
1859            backend_user: "backup".parse()?,
1860            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1861            system_user: "backup-user".parse()?,
1862        },
1863        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1864    )]
1865    #[case::backup_filter_metrics(
1866        NetHsmUserMapping::Backup{
1867            backend_user: "backup".parse()?,
1868            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1869            system_user: "backup-user".parse()?,
1870        },
1871        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1872    )]
1873    #[case::backup_filter_observer(
1874        NetHsmUserMapping::Backup{
1875            backend_user: "backup".parse()?,
1876            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1877            system_user: "backup-user".parse()?,
1878        },
1879        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1880    )]
1881    #[case::backup_filter_signing(
1882        NetHsmUserMapping::Backup{
1883            backend_user: "backup".parse()?,
1884            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1885            system_user: "backup-user".parse()?,
1886        },
1887        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1888    )]
1889    #[case::hermetic_metrics_filter_admin(
1890        NetHsmUserMapping::HermeticMetrics {
1891            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1892            system_user: "metrics-user".parse()?,
1893        },
1894        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1895    )]
1896    #[case::hermetic_metrics_filter_backup(
1897        NetHsmUserMapping::HermeticMetrics {
1898            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1899            system_user: "metrics-user".parse()?,
1900        },
1901        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1902    )]
1903    #[case::hermetic_metrics_filter_signing(
1904        NetHsmUserMapping::HermeticMetrics {
1905            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1906            system_user: "metrics-user".parse()?,
1907        },
1908        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1909    )]
1910    #[case::metrics_filter_admin(
1911        NetHsmUserMapping::Metrics {
1912            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1913            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1914            system_user: "hermetic-metrics-user".parse()?,
1915        },
1916        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1917    )]
1918    #[case::metrics_filter_backup(
1919        NetHsmUserMapping::Metrics {
1920            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1921            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1922            system_user: "hermetic-metrics-user".parse()?,
1923        },
1924        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1925    )]
1926    #[case::metrics_filter_signing(
1927        NetHsmUserMapping::Metrics {
1928            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1929            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1930            system_user: "hermetic-metrics-user".parse()?,
1931        },
1932        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1933    )]
1934    #[case::signing_filter_admin(
1935        NetHsmUserMapping::Signing {
1936            backend_user: "signing".parse()?,
1937            signing_key_id: "signing1".parse()?,
1938            key_setup: SigningKeySetup::new(
1939                KeyType::Curve25519,
1940                vec![KeyMechanism::EdDsaSignature],
1941                None,
1942                SignatureType::EdDsa,
1943                CryptographicKeyContext::OpenPgp {
1944                    user_ids: OpenPgpUserIdList::new(vec![
1945                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1946                    ])?,
1947                    version: "v4".parse()?,
1948                },
1949            )?,
1950            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1951            system_user: "signing-user".parse()?,
1952            tag: "signing1".to_string(),
1953        },
1954        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1955    )]
1956    #[case::signing_filter_backup(
1957        NetHsmUserMapping::Signing {
1958            backend_user: "signing".parse()?,
1959            signing_key_id: "signing1".parse()?,
1960            key_setup: SigningKeySetup::new(
1961                KeyType::Curve25519,
1962                vec![KeyMechanism::EdDsaSignature],
1963                None,
1964                SignatureType::EdDsa,
1965                CryptographicKeyContext::OpenPgp {
1966                    user_ids: OpenPgpUserIdList::new(vec![
1967                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1968                    ])?,
1969                    version: "v4".parse()?,
1970                },
1971            )?,
1972            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1973            system_user: "signing-user".parse()?,
1974            tag: "signing1".to_string(),
1975        },
1976        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1977    )]
1978    #[case::signing_filter_metrics(
1979        NetHsmUserMapping::Signing {
1980            backend_user: "signing".parse()?,
1981            signing_key_id: "signing1".parse()?,
1982            key_setup: SigningKeySetup::new(
1983                KeyType::Curve25519,
1984                vec![KeyMechanism::EdDsaSignature],
1985                None,
1986                SignatureType::EdDsa,
1987                CryptographicKeyContext::OpenPgp {
1988                    user_ids: OpenPgpUserIdList::new(vec![
1989                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1990                    ])?,
1991                    version: "v4".parse()?,
1992                },
1993            )?,
1994            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1995            system_user: "signing-user".parse()?,
1996            tag: "signing1".to_string(),
1997        },
1998        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1999    )]
2000    #[case::signing_filter_observer(
2001        NetHsmUserMapping::Signing {
2002            backend_user: "signing".parse()?,
2003            signing_key_id: "signing1".parse()?,
2004            key_setup: SigningKeySetup::new(
2005                KeyType::Curve25519,
2006                vec![KeyMechanism::EdDsaSignature],
2007                None,
2008                SignatureType::EdDsa,
2009                CryptographicKeyContext::OpenPgp {
2010                    user_ids: OpenPgpUserIdList::new(vec![
2011                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2012                    ])?,
2013                    version: "v4".parse()?,
2014                },
2015            )?,
2016            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2017            system_user: "signing-user".parse()?,
2018            tag: "signing1".to_string(),
2019        },
2020        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
2021    )]
2022    fn nethsm_user_mapping_backend_user_ids_filter_mismatches(
2023        #[case] mapping: NetHsmUserMapping,
2024        #[case] filter: BackendUserIdFilter,
2025    ) -> TestResult {
2026        assert!(mapping.backend_user_ids(filter).is_empty());
2027
2028        Ok(())
2029    }
2030
2031    #[test]
2032    fn nethsm_user_mapping_backend_user_with_passphrase_succeeds() -> TestResult {
2033        let mapping = NetHsmUserMapping::Admin("admin".parse()?);
2034        let passphrase = Passphrase::generate(None);
2035        let creds = mapping.backend_user_with_passphrase("admin", passphrase.clone())?;
2036
2037        assert_eq!(creds.user(), "admin");
2038        assert_eq!(
2039            creds.passphrase().expose_borrowed(),
2040            passphrase.expose_borrowed()
2041        );
2042
2043        Ok(())
2044    }
2045
2046    #[rstest]
2047    #[case::admin_filter_admin(
2048        NetHsmUserMapping::Admin("admin".parse()?),
2049        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2050        vec!["admin"]
2051    )]
2052    #[case::admin_filter_any(
2053        NetHsmUserMapping::Admin("admin".parse()?),
2054        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
2055        vec!["admin"]
2056    )]
2057    #[case::backup_filter_any(
2058        NetHsmUserMapping::Backup{
2059            backend_user: "backup".parse()?,
2060            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2061            system_user: "backup-user".parse()?,
2062        },
2063        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
2064        vec!["backup"]
2065    )]
2066    #[case::backup_filter_backup(
2067        NetHsmUserMapping::Backup{
2068            backend_user: "backup".parse()?,
2069            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2070            system_user: "backup-user".parse()?,
2071        },
2072        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
2073        vec!["backup"]
2074    )]
2075    #[case::backup_filter_non_admin(
2076        NetHsmUserMapping::Backup{
2077            backend_user: "backup".parse()?,
2078            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2079            system_user: "backup-user".parse()?,
2080        },
2081        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
2082        vec!["backup"]
2083    )]
2084    #[case::hermetic_metrics_filter_any(
2085        NetHsmUserMapping::HermeticMetrics {
2086            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2087            system_user: "metrics-user".parse()?,
2088        },
2089        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
2090        vec!["metrics", "keymetrics"]
2091    )]
2092    #[case::hermetic_metrics_filter_metrics(
2093        NetHsmUserMapping::HermeticMetrics {
2094            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2095            system_user: "metrics-user".parse()?,
2096        },
2097        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
2098        vec!["metrics"]
2099    )]
2100    #[case::hermetic_metrics_filter_non_admin(
2101        NetHsmUserMapping::HermeticMetrics {
2102            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2103            system_user: "metrics-user".parse()?,
2104        },
2105        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
2106        vec!["metrics", "keymetrics"]
2107    )]
2108    #[case::hermetic_metrics_filter_observer(
2109        NetHsmUserMapping::HermeticMetrics {
2110            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2111            system_user: "metrics-user".parse()?,
2112        },
2113        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
2114        vec!["keymetrics"]
2115    )]
2116    #[case::signing_filter_any(
2117        NetHsmUserMapping::Signing {
2118            backend_user: "signing".parse()?,
2119            signing_key_id: "signing1".parse()?,
2120            key_setup: SigningKeySetup::new(
2121                KeyType::Curve25519,
2122                vec![KeyMechanism::EdDsaSignature],
2123                None,
2124                SignatureType::EdDsa,
2125                CryptographicKeyContext::OpenPgp {
2126                    user_ids: OpenPgpUserIdList::new(vec![
2127                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2128                    ])?,
2129                    version: "v4".parse()?,
2130                },
2131            )?,
2132            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2133            system_user: "signing-user".parse()?,
2134            tag: "signing1".to_string(),
2135        },
2136        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
2137        vec!["signing"]
2138    )]
2139    #[case::signing_filter_non_admin(
2140        NetHsmUserMapping::Signing {
2141            backend_user: "signing".parse()?,
2142            signing_key_id: "signing1".parse()?,
2143            key_setup: SigningKeySetup::new(
2144                KeyType::Curve25519,
2145                vec![KeyMechanism::EdDsaSignature],
2146                None,
2147                SignatureType::EdDsa,
2148                CryptographicKeyContext::OpenPgp {
2149                    user_ids: OpenPgpUserIdList::new(vec![
2150                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2151                    ])?,
2152                    version: "v4".parse()?,
2153                },
2154            )?,
2155            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2156            system_user: "signing-user".parse()?,
2157            tag: "signing1".to_string(),
2158        },
2159        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
2160        vec!["signing"]
2161    )]
2162    #[case::signing_filter_signing(
2163        NetHsmUserMapping::Signing {
2164            backend_user: "signing".parse()?,
2165            signing_key_id: "signing1".parse()?,
2166            key_setup: SigningKeySetup::new(
2167                KeyType::Curve25519,
2168                vec![KeyMechanism::EdDsaSignature],
2169                None,
2170                SignatureType::EdDsa,
2171                CryptographicKeyContext::OpenPgp {
2172                    user_ids: OpenPgpUserIdList::new(vec![
2173                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2174                    ])?,
2175                    version: "v4".parse()?,
2176                },
2177            )?,
2178            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2179            system_user: "signing-user".parse()?,
2180            tag: "signing1".to_string(),
2181        },
2182        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
2183        vec!["signing"]
2184    )]
2185
2186    fn nethsm_user_mapping_backend_users_with_new_passphrase_filter_matches(
2187        #[case] mapping: NetHsmUserMapping,
2188        #[case] filter: BackendUserIdFilter,
2189        #[case] expected: Vec<&str>,
2190    ) -> TestResult {
2191        let creds = mapping.backend_users_with_new_passphrase(filter);
2192        let users = creds
2193            .iter()
2194            .map(|creds| creds.user())
2195            .collect::<HashSet<_>>();
2196        let expected = expected
2197            .iter()
2198            .map(ToString::to_string)
2199            .collect::<HashSet<_>>();
2200        assert_eq!(users, expected);
2201
2202        Ok(())
2203    }
2204
2205    #[rstest]
2206    #[case::admin_filter_metrics(
2207        NetHsmUserMapping::Admin("admin".parse()?),
2208        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
2209    )]
2210    #[case::admin_filter_non_admin(
2211        NetHsmUserMapping::Admin("admin".parse()?),
2212        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
2213    )]
2214    #[case::admin_filter_observer(
2215        NetHsmUserMapping::Admin("admin".parse()?),
2216        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
2217    )]
2218    #[case::admin_filter_signing(
2219        NetHsmUserMapping::Admin("admin".parse()?),
2220        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
2221    )]
2222    #[case::backup_filter_admin(
2223        NetHsmUserMapping::Backup{
2224            backend_user: "backup".parse()?,
2225            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2226            system_user: "backup-user".parse()?,
2227        },
2228        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2229    )]
2230    #[case::backup_filter_metrics(
2231        NetHsmUserMapping::Backup{
2232            backend_user: "backup".parse()?,
2233            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2234            system_user: "backup-user".parse()?,
2235        },
2236        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
2237    )]
2238    #[case::backup_filter_observer(
2239        NetHsmUserMapping::Backup{
2240            backend_user: "backup".parse()?,
2241            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2242            system_user: "backup-user".parse()?,
2243        },
2244        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
2245    )]
2246    #[case::backup_filter_signing(
2247        NetHsmUserMapping::Backup{
2248            backend_user: "backup".parse()?,
2249            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2250            system_user: "backup-user".parse()?,
2251        },
2252        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
2253    )]
2254    #[case::hermetic_metrics_filter_admin(
2255        NetHsmUserMapping::HermeticMetrics {
2256            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2257            system_user: "metrics-user".parse()?,
2258        },
2259        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2260    )]
2261    #[case::hermetic_metrics_filter_backup(
2262        NetHsmUserMapping::HermeticMetrics {
2263            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2264            system_user: "metrics-user".parse()?,
2265        },
2266        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
2267    )]
2268    #[case::hermetic_metrics_filter_signing(
2269        NetHsmUserMapping::HermeticMetrics {
2270            backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2271            system_user: "metrics-user".parse()?,
2272        },
2273        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
2274    )]
2275    #[case::metrics_filter_admin(
2276        NetHsmUserMapping::Metrics {
2277            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2278            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2279            system_user: "hermetic-metrics-user".parse()?,
2280        },
2281        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2282    )]
2283    #[case::metrics_filter_backup(
2284        NetHsmUserMapping::Metrics {
2285            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2286            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2287            system_user: "hermetic-metrics-user".parse()?,
2288        },
2289        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
2290    )]
2291    #[case::metrics_filter_signing(
2292        NetHsmUserMapping::Metrics {
2293            backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2294            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2295            system_user: "hermetic-metrics-user".parse()?,
2296        },
2297        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
2298    )]
2299    #[case::signing_filter_admin(
2300        NetHsmUserMapping::Signing {
2301            backend_user: "signing".parse()?,
2302            signing_key_id: "signing1".parse()?,
2303            key_setup: SigningKeySetup::new(
2304                KeyType::Curve25519,
2305                vec![KeyMechanism::EdDsaSignature],
2306                None,
2307                SignatureType::EdDsa,
2308                CryptographicKeyContext::OpenPgp {
2309                    user_ids: OpenPgpUserIdList::new(vec![
2310                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2311                    ])?,
2312                    version: "v4".parse()?,
2313                },
2314            )?,
2315            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2316            system_user: "signing-user".parse()?,
2317            tag: "signing1".to_string(),
2318        },
2319        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
2320    )]
2321    #[case::signing_filter_backup(
2322        NetHsmUserMapping::Signing {
2323            backend_user: "signing".parse()?,
2324            signing_key_id: "signing1".parse()?,
2325            key_setup: SigningKeySetup::new(
2326                KeyType::Curve25519,
2327                vec![KeyMechanism::EdDsaSignature],
2328                None,
2329                SignatureType::EdDsa,
2330                CryptographicKeyContext::OpenPgp {
2331                    user_ids: OpenPgpUserIdList::new(vec![
2332                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2333                    ])?,
2334                    version: "v4".parse()?,
2335                },
2336            )?,
2337            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2338            system_user: "signing-user".parse()?,
2339            tag: "signing1".to_string(),
2340        },
2341        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
2342    )]
2343    #[case::signing_filter_metrics(
2344        NetHsmUserMapping::Signing {
2345            backend_user: "signing".parse()?,
2346            signing_key_id: "signing1".parse()?,
2347            key_setup: SigningKeySetup::new(
2348                KeyType::Curve25519,
2349                vec![KeyMechanism::EdDsaSignature],
2350                None,
2351                SignatureType::EdDsa,
2352                CryptographicKeyContext::OpenPgp {
2353                    user_ids: OpenPgpUserIdList::new(vec![
2354                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2355                    ])?,
2356                    version: "v4".parse()?,
2357                },
2358            )?,
2359            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2360            system_user: "signing-user".parse()?,
2361            tag: "signing1".to_string(),
2362        },
2363        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
2364    )]
2365    #[case::signing_filter_observer(
2366        NetHsmUserMapping::Signing {
2367            backend_user: "signing".parse()?,
2368            signing_key_id: "signing1".parse()?,
2369            key_setup: SigningKeySetup::new(
2370                KeyType::Curve25519,
2371                vec![KeyMechanism::EdDsaSignature],
2372                None,
2373                SignatureType::EdDsa,
2374                CryptographicKeyContext::OpenPgp {
2375                    user_ids: OpenPgpUserIdList::new(vec![
2376                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2377                    ])?,
2378                    version: "v4".parse()?,
2379                },
2380            )?,
2381            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2382            system_user: "signing-user".parse()?,
2383            tag: "signing1".to_string(),
2384        },
2385        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
2386    )]
2387    fn nethsm_user_mapping_backend_users_with_new_passphrase_filter_mismatches(
2388        #[case] mapping: NetHsmUserMapping,
2389        #[case] filter: BackendUserIdFilter,
2390    ) -> TestResult {
2391        assert!(mapping.backend_users_with_new_passphrase(filter).is_empty());
2392
2393        Ok(())
2394    }
2395
2396    #[test]
2397    fn nethsm_user_mapping_backend_user_with_passphrase_fails() -> TestResult {
2398        let mapping = NetHsmUserMapping::Admin("admin".parse()?);
2399        assert!(
2400            mapping
2401                .backend_user_with_passphrase("2", Passphrase::generate(None))
2402                .is_err()
2403        );
2404
2405        Ok(())
2406    }
2407
2408    #[fixture]
2409    fn nethsm_config_connections() -> TestResult<BTreeSet<Connection>> {
2410        Ok(BTreeSet::from_iter([
2411            Connection::new(
2412                "https://nethsm1.example.org/".parse()?,
2413                nethsm::ConnectionSecurity::Unsafe,
2414            ),
2415            Connection::new(
2416                "https://nethsm2.example.org/".parse()?,
2417                nethsm::ConnectionSecurity::Unsafe,
2418            ),
2419        ]))
2420    }
2421
2422    #[fixture]
2423    fn nethsm_config_mappings() -> TestResult<BTreeSet<NetHsmUserMapping>> {
2424        Ok(BTreeSet::from_iter([
2425            NetHsmUserMapping::Admin("admin".parse()?),
2426            NetHsmUserMapping::Backup{
2427                backend_user: "backup".parse()?,
2428                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2429                system_user: "backup-user".parse()?,
2430            },
2431            NetHsmUserMapping::HermeticMetrics {
2432                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2433                system_user: "metrics-user".parse()?,
2434            },
2435            NetHsmUserMapping::Metrics {
2436                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2437                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2438                system_user: "hermetic-metrics-user".parse()?,
2439            },
2440            NetHsmUserMapping::Signing {
2441                backend_user: "signing".parse()?,
2442                signing_key_id: "signing1".parse()?,
2443                key_setup: SigningKeySetup::new(
2444                    KeyType::Curve25519,
2445                    vec![KeyMechanism::EdDsaSignature],
2446                    None,
2447                    SignatureType::EdDsa,
2448                    CryptographicKeyContext::OpenPgp {
2449                        user_ids: OpenPgpUserIdList::new(vec![
2450                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2451                        ])?,
2452                        version: "v4".parse()?,
2453                    },
2454                )?,
2455                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2456                system_user: "signing-user".parse()?,
2457                tag: "signing1".to_string(),
2458            }
2459        ]))
2460    }
2461
2462    #[fixture]
2463    fn nethsm_config(
2464        nethsm_config_connections: TestResult<BTreeSet<Connection>>,
2465        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
2466    ) -> TestResult<NetHsmConfig> {
2467        let nethsm_config_connections = nethsm_config_connections?;
2468        let nethsm_config_mappings = nethsm_config_mappings?;
2469        Ok(NetHsmConfig::new(
2470            nethsm_config_connections,
2471            nethsm_config_mappings,
2472        )?)
2473    }
2474
2475    #[rstest]
2476    fn nethsm_config_connections_matches(
2477        nethsm_config: TestResult<NetHsmConfig>,
2478        nethsm_config_connections: TestResult<BTreeSet<Connection>>,
2479    ) -> TestResult {
2480        let nethsm_config = nethsm_config?;
2481        let nethsm_config_connections = nethsm_config_connections?;
2482        assert_eq!(nethsm_config.connections(), &nethsm_config_connections);
2483
2484        Ok(())
2485    }
2486
2487    #[rstest]
2488    fn nethsm_config_mappings_matches(
2489        nethsm_config: TestResult<NetHsmConfig>,
2490        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
2491    ) -> TestResult {
2492        let nethsm_config = nethsm_config?;
2493        let nethsm_config_mappings = nethsm_config_mappings?;
2494        assert_eq!(nethsm_config.mappings(), &nethsm_config_mappings);
2495
2496        Ok(())
2497    }
2498
2499    #[rstest]
2500    fn nethsm_config_authorized_key_entries(
2501        nethsm_config: TestResult<NetHsmConfig>,
2502        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
2503    ) -> TestResult {
2504        let nethsm_config = nethsm_config?;
2505        let nethsm_config_mappings = nethsm_config_mappings?;
2506        let expected = nethsm_config_mappings
2507            .iter()
2508            .filter_map(|mapping| mapping.authorized_key_entry())
2509            .collect::<HashSet<_>>();
2510        assert_eq!(nethsm_config.authorized_key_entries(), expected);
2511
2512        Ok(())
2513    }
2514
2515    #[rstest]
2516    fn nethsm_config_system_user_ids(
2517        nethsm_config: TestResult<NetHsmConfig>,
2518        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
2519    ) -> TestResult {
2520        let nethsm_config = nethsm_config?;
2521        let nethsm_config_mappings = nethsm_config_mappings?;
2522        let expected = nethsm_config_mappings
2523            .iter()
2524            .filter_map(|mapping| mapping.system_user_id())
2525            .collect::<HashSet<_>>();
2526        assert_eq!(nethsm_config.system_user_ids(), expected);
2527
2528        Ok(())
2529    }
2530
2531    #[rstest]
2532    #[case::no_connection(
2533        "Error message for NetHsmConfig::new with no backend connection",
2534        BTreeSet::new(),
2535        BTreeSet::from_iter([
2536            NetHsmUserMapping::Admin("admin".parse()?),
2537            NetHsmUserMapping::Backup{
2538                backend_user: "backup".parse()?,
2539                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2540                system_user: "backup-user".parse()?,
2541            },
2542            NetHsmUserMapping::HermeticMetrics {
2543                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2544                system_user: "hermetic-metrics-user".parse()?,
2545            },
2546            NetHsmUserMapping::Metrics {
2547                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2548                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2549                system_user: "metrics-user".parse()?,
2550            },
2551            NetHsmUserMapping::Signing {
2552                backend_user: "signing".parse()?,
2553                signing_key_id: "signing1".parse()?,
2554                key_setup: SigningKeySetup::new(
2555                    KeyType::Curve25519,
2556                    vec![KeyMechanism::EdDsaSignature],
2557                    None,
2558                    SignatureType::EdDsa,
2559                    CryptographicKeyContext::OpenPgp {
2560                        user_ids: OpenPgpUserIdList::new(vec![
2561                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2562                        ])?,
2563                        version: "v4".parse()?,
2564                    },
2565                )?,
2566                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2567                system_user: "signing-user".parse()?,
2568                tag: "signing1".to_string(),
2569            }
2570        ]),
2571    )]
2572    #[case::duplicate_connection_url(
2573        "Error message for NetHsmConfig::new with two duplicate connection URLs",
2574        BTreeSet::from_iter([
2575            Connection::new("https://nethsm1.example.org/".parse()?, nethsm::ConnectionSecurity::Unsafe),
2576            Connection::new("https://nethsm1.example.org/".parse()?, nethsm::ConnectionSecurity::Native),
2577        ]),
2578        BTreeSet::from_iter([
2579            NetHsmUserMapping::Admin("admin".parse()?),
2580            NetHsmUserMapping::Backup{
2581                backend_user: "backup".parse()?,
2582                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2583                system_user: "backup-user".parse()?,
2584            },
2585            NetHsmUserMapping::HermeticMetrics {
2586                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2587                system_user: "hermetic-metrics-user".parse()?,
2588            },
2589            NetHsmUserMapping::Metrics {
2590                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2591                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2592                system_user: "metrics-user".parse()?,
2593            },
2594            NetHsmUserMapping::Signing {
2595                backend_user: "signing".parse()?,
2596                signing_key_id: "signing1".parse()?,
2597                key_setup: SigningKeySetup::new(
2598                    KeyType::Curve25519,
2599                    vec![KeyMechanism::EdDsaSignature],
2600                    None,
2601                    SignatureType::EdDsa,
2602                    CryptographicKeyContext::OpenPgp {
2603                        user_ids: OpenPgpUserIdList::new(vec![
2604                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2605                        ])?,
2606                        version: "v4".parse()?,
2607                    },
2608                )?,
2609                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2610                system_user: "signing-user".parse()?,
2611                tag: "signing1".to_string(),
2612            }
2613        ]),
2614    )]
2615    #[case::no_mappings(
2616        "Error message for NetHsmConfig::new with no user mappings",
2617        BTreeSet::from_iter([
2618            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2619            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2620        ]),
2621        BTreeSet::new(),
2622    )]
2623    #[case::duplicate_system_user_ids(
2624        "Error message for NetHsmConfig::new with two duplicate system user IDs",
2625        BTreeSet::from_iter([
2626            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2627            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2628        ]),
2629        BTreeSet::from_iter([
2630            NetHsmUserMapping::Admin("admin".parse()?),
2631            NetHsmUserMapping::Backup{
2632                backend_user: "backup".parse()?,
2633                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2634                system_user: "backup-user".parse()?,
2635            },
2636            NetHsmUserMapping::HermeticMetrics {
2637                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2638                system_user: "hermetic-metrics-user".parse()?,
2639            },
2640            NetHsmUserMapping::Metrics {
2641                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2642                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2643                system_user: "backup-user".parse()?,
2644            },
2645            NetHsmUserMapping::Signing {
2646                backend_user: "signing".parse()?,
2647                signing_key_id: "signing1".parse()?,
2648                key_setup: SigningKeySetup::new(
2649                    KeyType::Curve25519,
2650                    vec![KeyMechanism::EdDsaSignature],
2651                    None,
2652                    SignatureType::EdDsa,
2653                    CryptographicKeyContext::OpenPgp {
2654                        user_ids: OpenPgpUserIdList::new(vec![
2655                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2656                        ])?,
2657                        version: "v4".parse()?,
2658                    },
2659                )?,
2660                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2661                system_user: "signing-user".parse()?,
2662                tag: "signing1".to_string(),
2663            }
2664        ]),
2665    )]
2666    #[case::duplicate_ssh_public_keys(
2667        "Error message for NetHsmConfig::new with two duplicate SSH public keys as authorized keys",
2668        BTreeSet::from_iter([
2669            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2670            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2671        ]),
2672        BTreeSet::from_iter([
2673            NetHsmUserMapping::Admin("admin".parse()?),
2674            NetHsmUserMapping::Backup{
2675                backend_user: "backup".parse()?,
2676                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2677                system_user: "backup-user".parse()?,
2678            },
2679            NetHsmUserMapping::HermeticMetrics {
2680                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2681                system_user: "hermetic-metrics-user".parse()?,
2682            },
2683            NetHsmUserMapping::Metrics {
2684                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2685                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user2@other-host".parse()?,
2686                system_user: "metrics-user".parse()?,
2687            },
2688            NetHsmUserMapping::Signing {
2689                backend_user: "signing".parse()?,
2690                signing_key_id: "signing1".parse()?,
2691                key_setup: SigningKeySetup::new(
2692                    KeyType::Curve25519,
2693                    vec![KeyMechanism::EdDsaSignature],
2694                    None,
2695                    SignatureType::EdDsa,
2696                    CryptographicKeyContext::OpenPgp {
2697                        user_ids: OpenPgpUserIdList::new(vec![
2698                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2699                        ])?,
2700                        version: "v4".parse()?,
2701                    },
2702                )?,
2703                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2704                system_user: "signing-user".parse()?,
2705                tag: "signing1".to_string(),
2706            }
2707        ]),
2708    )]
2709    #[case::missing_system_wide_administrator(
2710        "Error message for NetHsmConfig::new with a system-wide administrator missing",
2711        BTreeSet::from_iter([
2712            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2713            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2714        ]),
2715        BTreeSet::from_iter([
2716            NetHsmUserMapping::Backup{
2717                backend_user: "backup".parse()?,
2718                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2719                system_user: "backup-user".parse()?,
2720            },
2721            NetHsmUserMapping::HermeticMetrics {
2722                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2723                system_user: "hermetic-metrics-user".parse()?,
2724            },
2725            NetHsmUserMapping::Metrics {
2726                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2727                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2728                system_user: "metrics-user".parse()?,
2729            },
2730            NetHsmUserMapping::Signing {
2731                backend_user: "signing".parse()?,
2732                signing_key_id: "signing1".parse()?,
2733                key_setup: SigningKeySetup::new(
2734                    KeyType::Curve25519,
2735                    vec![KeyMechanism::EdDsaSignature],
2736                    None,
2737                    SignatureType::EdDsa,
2738                    CryptographicKeyContext::OpenPgp {
2739                        user_ids: OpenPgpUserIdList::new(vec![
2740                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2741                        ])?,
2742                        version: "v4".parse()?,
2743                    },
2744                )?,
2745                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2746                system_user: "signing-user".parse()?,
2747                tag: "signing1".to_string(),
2748            }
2749        ]),
2750    )]
2751    #[case::duplicate_system_wide_backend_user_ids(
2752        "Error message for NetHsmConfig::new with two duplicate system-wide backend user IDs",
2753        BTreeSet::from_iter([
2754            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2755            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2756        ]),
2757        BTreeSet::from_iter([
2758            NetHsmUserMapping::Admin("admin".parse()?),
2759            NetHsmUserMapping::Admin("backup".parse()?),
2760            NetHsmUserMapping::Backup{
2761                backend_user: "backup".parse()?,
2762                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2763                system_user: "backup-user".parse()?,
2764            },
2765            NetHsmUserMapping::HermeticMetrics {
2766                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2767                system_user: "hermetic-metrics-user".parse()?,
2768            },
2769            NetHsmUserMapping::Metrics {
2770                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2771                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2772                system_user: "metrics-user".parse()?,
2773            },
2774            NetHsmUserMapping::Signing {
2775                backend_user: "signing".parse()?,
2776                signing_key_id: "signing1".parse()?,
2777                key_setup: SigningKeySetup::new(
2778                    KeyType::Curve25519,
2779                    vec![KeyMechanism::EdDsaSignature],
2780                    None,
2781                    SignatureType::EdDsa,
2782                    CryptographicKeyContext::OpenPgp {
2783                        user_ids: OpenPgpUserIdList::new(vec![
2784                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2785                        ])?,
2786                        version: "v4".parse()?,
2787                    },
2788                )?,
2789                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2790                system_user: "signing-user".parse()?,
2791                tag: "signing1".to_string(),
2792            }
2793        ]),
2794    )]
2795    #[case::duplicate_namespaced_backend_user_ids(
2796        "Error message for NetHsmConfig::new with two duplicate namespaced backend user IDs",
2797        BTreeSet::from_iter([
2798            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2799            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2800        ]),
2801        BTreeSet::from_iter([
2802            NetHsmUserMapping::Admin("admin".parse()?),
2803            NetHsmUserMapping::Admin("ns1~admin".parse()?),
2804            NetHsmUserMapping::Signing {
2805                backend_user: "ns1~signing1".parse()?,
2806                signing_key_id: "signing1".parse()?,
2807                key_setup: SigningKeySetup::new(
2808                    KeyType::Curve25519,
2809                    vec![KeyMechanism::EdDsaSignature],
2810                    None,
2811                    SignatureType::EdDsa,
2812                    CryptographicKeyContext::OpenPgp {
2813                        user_ids: OpenPgpUserIdList::new(vec![
2814                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2815                        ])?,
2816                        version: "v4".parse()?,
2817                    },
2818                )?,
2819                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2820                system_user: "ns1-signing-user1".parse()?,
2821                tag: "signing1".to_string(),
2822            },
2823            NetHsmUserMapping::Signing {
2824                backend_user: "ns1~signing1".parse()?,
2825                signing_key_id: "signing2".parse()?,
2826                key_setup: SigningKeySetup::new(
2827                    KeyType::Curve25519,
2828                    vec![KeyMechanism::EdDsaSignature],
2829                    None,
2830                    SignatureType::EdDsa,
2831                    CryptographicKeyContext::OpenPgp {
2832                        user_ids: OpenPgpUserIdList::new(vec![
2833                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2834                        ])?,
2835                        version: "v4".parse()?,
2836                    },
2837                )?,
2838                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2839                system_user: "ns1-signing-user2".parse()?,
2840                tag: "signing2".to_string(),
2841            }
2842        ]),
2843    )]
2844    #[case::duplicate_system_wide_key_ids(
2845        "Error message for NetHsmConfig::new with two duplicate system-wide key IDs",
2846        BTreeSet::from_iter([
2847            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2848            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2849        ]),
2850        BTreeSet::from_iter([
2851            NetHsmUserMapping::Admin("admin".parse()?),
2852            NetHsmUserMapping::Backup{
2853                backend_user: "backup".parse()?,
2854                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2855                system_user: "backup-user".parse()?,
2856            },
2857            NetHsmUserMapping::HermeticMetrics {
2858                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2859                system_user: "hermetic-metrics-user".parse()?,
2860            },
2861            NetHsmUserMapping::Metrics {
2862                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2863                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2864                system_user: "metrics-user".parse()?,
2865            },
2866            NetHsmUserMapping::Signing {
2867                backend_user: "signing1".parse()?,
2868                signing_key_id: "signing1".parse()?,
2869                key_setup: SigningKeySetup::new(
2870                    KeyType::Curve25519,
2871                    vec![KeyMechanism::EdDsaSignature],
2872                    None,
2873                    SignatureType::EdDsa,
2874                    CryptographicKeyContext::OpenPgp {
2875                        user_ids: OpenPgpUserIdList::new(vec![
2876                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2877                        ])?,
2878                        version: "v4".parse()?,
2879                    },
2880                )?,
2881                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2882                system_user: "signing-user".parse()?,
2883                tag: "signing1".to_string(),
2884            },
2885            NetHsmUserMapping::Signing {
2886                backend_user: "signing2".parse()?,
2887                signing_key_id: "signing1".parse()?,
2888                key_setup: SigningKeySetup::new(
2889                    KeyType::Curve25519,
2890                    vec![KeyMechanism::EdDsaSignature],
2891                    None,
2892                    SignatureType::EdDsa,
2893                    CryptographicKeyContext::OpenPgp {
2894                        user_ids: OpenPgpUserIdList::new(vec![
2895                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2896                        ])?,
2897                        version: "v4".parse()?,
2898                    },
2899                )?,
2900                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2901                system_user: "signing-user2".parse()?,
2902                tag: "signing2".to_string(),
2903            }
2904        ]),
2905    )]
2906    #[case::duplicate_system_wide_tags(
2907        "Error message for NetHsmConfig::new with two duplicate system-wide tags",
2908        BTreeSet::from_iter([
2909            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2910            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2911        ]),
2912        BTreeSet::from_iter([
2913            NetHsmUserMapping::Admin("admin".parse()?),
2914            NetHsmUserMapping::Backup{
2915                backend_user: "backup".parse()?,
2916                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2917                system_user: "backup-user".parse()?,
2918            },
2919            NetHsmUserMapping::HermeticMetrics {
2920                backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2921                system_user: "hermetic-metrics-user".parse()?,
2922            },
2923            NetHsmUserMapping::Metrics {
2924                backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2925                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2926                system_user: "metrics-user".parse()?,
2927            },
2928            NetHsmUserMapping::Signing {
2929                backend_user: "signing1".parse()?,
2930                signing_key_id: "signing1".parse()?,
2931                key_setup: SigningKeySetup::new(
2932                    KeyType::Curve25519,
2933                    vec![KeyMechanism::EdDsaSignature],
2934                    None,
2935                    SignatureType::EdDsa,
2936                    CryptographicKeyContext::OpenPgp {
2937                        user_ids: OpenPgpUserIdList::new(vec![
2938                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2939                        ])?,
2940                        version: "v4".parse()?,
2941                    },
2942                )?,
2943                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2944                system_user: "signing-user".parse()?,
2945                tag: "signing1".to_string(),
2946            },
2947            NetHsmUserMapping::Signing {
2948                backend_user: "signing2".parse()?,
2949                signing_key_id: "signing2".parse()?,
2950                key_setup: SigningKeySetup::new(
2951                    KeyType::Curve25519,
2952                    vec![KeyMechanism::EdDsaSignature],
2953                    None,
2954                    SignatureType::EdDsa,
2955                    CryptographicKeyContext::OpenPgp {
2956                        user_ids: OpenPgpUserIdList::new(vec![
2957                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2958                        ])?,
2959                        version: "v4".parse()?,
2960                    },
2961                )?,
2962                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2963                system_user: "signing-user2".parse()?,
2964                tag: "signing1".to_string(),
2965            }
2966        ]),
2967    )]
2968    #[case::missing_namespace_administrator(
2969        "Error message for NetHsmConfig::new with a missing namespace administrator",
2970        BTreeSet::from_iter([
2971            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2972            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
2973        ]),
2974        BTreeSet::from_iter([
2975            NetHsmUserMapping::Admin("admin".parse()?),
2976            NetHsmUserMapping::Signing {
2977                backend_user: "ns1~signing1".parse()?,
2978                signing_key_id: "signing1".parse()?,
2979                key_setup: SigningKeySetup::new(
2980                    KeyType::Curve25519,
2981                    vec![KeyMechanism::EdDsaSignature],
2982                    None,
2983                    SignatureType::EdDsa,
2984                    CryptographicKeyContext::OpenPgp {
2985                        user_ids: OpenPgpUserIdList::new(vec![
2986                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2987                        ])?,
2988                        version: "v4".parse()?,
2989                    },
2990                )?,
2991                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2992                system_user: "ns1-signing-user".parse()?,
2993                tag: "signing1".to_string(),
2994            },
2995        ]),
2996    )]
2997    #[case::duplicate_namespace_key_ids(
2998        "Error message for NetHsmConfig::new with two duplicate namespaced key IDs",
2999        BTreeSet::from_iter([
3000            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
3001            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
3002        ]),
3003        BTreeSet::from_iter([
3004            NetHsmUserMapping::Admin("admin".parse()?),
3005            NetHsmUserMapping::Admin("ns1~admin".parse()?),
3006            NetHsmUserMapping::Signing {
3007                backend_user: "ns1~signing1".parse()?,
3008                signing_key_id: "signing1".parse()?,
3009                key_setup: SigningKeySetup::new(
3010                    KeyType::Curve25519,
3011                    vec![KeyMechanism::EdDsaSignature],
3012                    None,
3013                    SignatureType::EdDsa,
3014                    CryptographicKeyContext::OpenPgp {
3015                        user_ids: OpenPgpUserIdList::new(vec![
3016                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3017                        ])?,
3018                        version: "v4".parse()?,
3019                    },
3020                )?,
3021                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3022                system_user: "ns1-signing-user".parse()?,
3023                tag: "signing1".to_string(),
3024            },
3025            NetHsmUserMapping::Signing {
3026                backend_user: "ns1~signing2".parse()?,
3027                signing_key_id: "signing1".parse()?,
3028                key_setup: SigningKeySetup::new(
3029                    KeyType::Curve25519,
3030                    vec![KeyMechanism::EdDsaSignature],
3031                    None,
3032                    SignatureType::EdDsa,
3033                    CryptographicKeyContext::OpenPgp {
3034                        user_ids: OpenPgpUserIdList::new(vec![
3035                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3036                        ])?,
3037                        version: "v4".parse()?,
3038                    },
3039                )?,
3040                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3041                system_user: "ns1-signing-user2".parse()?,
3042                tag: "signing2".to_string(),
3043            }
3044        ]),
3045    )]
3046    #[case::duplicate_namespace_tags(
3047        "Error message for NetHsmConfig::new with two duplicate namespaced tags",
3048        BTreeSet::from_iter([
3049            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
3050            Connection::new("https://nethsm2.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
3051        ]),
3052        BTreeSet::from_iter([
3053            NetHsmUserMapping::Admin("admin".parse()?),
3054            NetHsmUserMapping::Admin("ns1~admin".parse()?),
3055            NetHsmUserMapping::Signing {
3056                backend_user: "ns1~signing1".parse()?,
3057                signing_key_id: "signing1".parse()?,
3058                key_setup: SigningKeySetup::new(
3059                    KeyType::Curve25519,
3060                    vec![KeyMechanism::EdDsaSignature],
3061                    None,
3062                    SignatureType::EdDsa,
3063                    CryptographicKeyContext::OpenPgp {
3064                        user_ids: OpenPgpUserIdList::new(vec![
3065                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3066                        ])?,
3067                        version: "v4".parse()?,
3068                    },
3069                )?,
3070                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3071                system_user: "ns1-signing-user".parse()?,
3072                tag: "signing1".to_string(),
3073            },
3074            NetHsmUserMapping::Signing {
3075                backend_user: "ns1~signing2".parse()?,
3076                signing_key_id: "signing2".parse()?,
3077                key_setup: SigningKeySetup::new(
3078                    KeyType::Curve25519,
3079                    vec![KeyMechanism::EdDsaSignature],
3080                    None,
3081                    SignatureType::EdDsa,
3082                    CryptographicKeyContext::OpenPgp {
3083                        user_ids: OpenPgpUserIdList::new(vec![
3084                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3085                        ])?,
3086                        version: "v4".parse()?,
3087                    },
3088                )?,
3089                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3090                system_user: "ns1-signing-user2".parse()?,
3091                tag: "signing1".to_string(),
3092            }
3093        ]),
3094    )]
3095    #[case::all_the_issues(
3096        "Error message for NetHsmConfig::new with multiple validation issues (connections and mappings)",
3097        BTreeSet::from_iter([
3098            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Unsafe),
3099            Connection::new("https://nethsm1.example.org/".parse()?,nethsm::ConnectionSecurity::Native),
3100        ]),
3101        BTreeSet::from_iter([
3102            NetHsmUserMapping::Signing {
3103                backend_user: "signing1".parse()?,
3104                signing_key_id: "signing1".parse()?,
3105                key_setup: SigningKeySetup::new(
3106                    KeyType::Curve25519,
3107                    vec![KeyMechanism::EdDsaSignature],
3108                    None,
3109                    SignatureType::EdDsa,
3110                    CryptographicKeyContext::OpenPgp {
3111                        user_ids: OpenPgpUserIdList::new(vec![
3112                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3113                        ])?,
3114                        version: "v4".parse()?,
3115                    },
3116                )?,
3117                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
3118                system_user: "ns1-signing-user1".parse()?,
3119                tag: "signing1".to_string(),
3120            },
3121            NetHsmUserMapping::Signing {
3122                backend_user: "signing1".parse()?,
3123                signing_key_id: "signing1".parse()?,
3124                key_setup: SigningKeySetup::new(
3125                    KeyType::Curve25519,
3126                    vec![KeyMechanism::EdDsaSignature],
3127                    None,
3128                    SignatureType::EdDsa,
3129                    CryptographicKeyContext::OpenPgp {
3130                        user_ids: OpenPgpUserIdList::new(vec![
3131                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3132                        ])?,
3133                        version: "v4".parse()?,
3134                    },
3135                )?,
3136                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3137                system_user: "ns1-signing-user1".parse()?,
3138                tag: "signing1".to_string(),
3139            },
3140            NetHsmUserMapping::Signing {
3141                backend_user: "ns1~signing1".parse()?,
3142                signing_key_id: "signing1".parse()?,
3143                key_setup: SigningKeySetup::new(
3144                    KeyType::Curve25519,
3145                    vec![KeyMechanism::EdDsaSignature],
3146                    None,
3147                    SignatureType::EdDsa,
3148                    CryptographicKeyContext::OpenPgp {
3149                        user_ids: OpenPgpUserIdList::new(vec![
3150                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3151                        ])?,
3152                        version: "v4".parse()?,
3153                    },
3154                )?,
3155                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3156                system_user: "ns1-signing-user1".parse()?,
3157                tag: "signing1".to_string(),
3158            },
3159            NetHsmUserMapping::Signing {
3160                backend_user: "ns1~signing1".parse()?,
3161                signing_key_id: "signing2".parse()?,
3162                key_setup: SigningKeySetup::new(
3163                    KeyType::Curve25519,
3164                    vec![KeyMechanism::EdDsaSignature],
3165                    None,
3166                    SignatureType::EdDsa,
3167                    CryptographicKeyContext::OpenPgp {
3168                        user_ids: OpenPgpUserIdList::new(vec![
3169                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3170                        ])?,
3171                        version: "v4".parse()?,
3172                    },
3173                )?,
3174                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3175                system_user: "ns1-signing-user2".parse()?,
3176                tag: "signing1".to_string(),
3177            },
3178            NetHsmUserMapping::Signing {
3179                backend_user: "ns1~signing2".parse()?,
3180                signing_key_id: "signing2".parse()?,
3181                key_setup: SigningKeySetup::new(
3182                    KeyType::Curve25519,
3183                    vec![KeyMechanism::EdDsaSignature],
3184                    None,
3185                    SignatureType::EdDsa,
3186                    CryptographicKeyContext::OpenPgp {
3187                        user_ids: OpenPgpUserIdList::new(vec![
3188                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3189                        ])?,
3190                        version: "v4".parse()?,
3191                    },
3192                )?,
3193                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3194                system_user: "ns1-signing-user1".parse()?,
3195                tag: "signing1".to_string(),
3196            },
3197        ]),
3198    )]
3199    fn nethsm_config_new_fails_validation(
3200        #[case] description: &str,
3201        #[case] connections: BTreeSet<Connection>,
3202        #[case] mappings: BTreeSet<NetHsmUserMapping>,
3203    ) -> TestResult {
3204        let error_msg = match NetHsmConfig::new(connections, mappings) {
3205            Err(crate::Error::Validation { source, .. }) => source.to_string(),
3206            Ok(config) => {
3207                panic!("Expected to fail with Error::Validation, but succeeded instead: {config:?}")
3208            }
3209            Err(error) => panic!(
3210                "Expected to fail with Error::Validation, but failed with a different error instead: {error}"
3211            ),
3212        };
3213
3214        with_settings!({
3215            description => description,
3216            snapshot_path => SNAPSHOT_PATH,
3217            prepend_module_to_snapshot => false,
3218        }, {
3219            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_msg);
3220        });
3221        Ok(())
3222    }
3223
3224    /// Ensures that [`NetHsmConfigState`] can be created from [`NetHsmConfig`].
3225    #[rstest]
3226    fn nethsm_config_state_from_config(
3227        nethsm_config: TestResult<NetHsmConfig>,
3228        nethsm_config_mappings: TestResult<BTreeSet<NetHsmUserMapping>>,
3229    ) -> TestResult {
3230        let nethsm_config = nethsm_config?;
3231        let nethsm_config_mappings = nethsm_config_mappings?;
3232        let state = NetHsmConfigState::from(&nethsm_config);
3233
3234        for user_id in nethsm_config_mappings
3235            .iter()
3236            .flat_map(|mapping| mapping.nethsm_user_ids())
3237        {
3238            debug!(
3239                "Ensuring that the NetHSM user ID {user_id} can be found in the NetHSM user state."
3240            );
3241            assert!(
3242                state
3243                    .user_data
3244                    .iter()
3245                    .any(|user_data| user_data.user == &user_id)
3246            );
3247        }
3248
3249        for user_id in nethsm_config_mappings.iter().filter_map(|mapping| {
3250            if let NetHsmUserMapping::Signing { backend_user, .. } = mapping {
3251                Some(backend_user)
3252            } else {
3253                None
3254            }
3255        }) {
3256            debug!(
3257                "Ensuring that the NetHSM user ID {user_id} can be found in the NetHSM key state."
3258            );
3259            assert!(
3260                state
3261                    .key_data
3262                    .iter()
3263                    .any(|user_data| user_data.user == user_id)
3264            );
3265        }
3266
3267        Ok(())
3268    }
3269}