signstar_config/config/
traits.rs

1//! Traits for configuration use.
2
3use std::collections::HashSet;
4
5use nix::unistd::User;
6use signstar_crypto::{
7    NonAdministrativeSecretHandling,
8    passphrase::Passphrase,
9    secret_file::{load_passphrase_from_secrets_file, write_passphrase_to_secrets_file},
10    traits::UserWithPassphrase,
11};
12
13use crate::{AuthorizedKeyEntry, SystemUserId, utils::get_current_system_user};
14
15/// An error that may occur when using signstar-config traits.
16#[derive(Debug, thiserror::Error)]
17pub enum Error {
18    /// A backend user ID does not match.
19    #[error("Expected the backend user ID {expected}, but found {actual} instead")]
20    BackendUserIdMismatch {
21        /// The expected backend user ID.
22        expected: String,
23
24        /// The actually found backend user ID.
25        actual: String,
26    },
27}
28
29/// An interface for returning an optional [`SystemUserId`] or a [`User`].
30///
31/// It is implemented by mapping implementations, that track system user data.
32///
33/// # Example
34///
35/// ```
36/// use signstar_config::{SystemUserId, config::MappingSystemUserId};
37/// use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
38///
39/// #[derive(Debug)]
40/// enum ExampleUserMapping {
41///     Admin {
42///         backend_id: u8,
43///     },
44///     Backup {
45///         backend_id: u8,
46///         system_user: SystemUserId,
47///     },
48///     Metrics {
49///         backend_id: u8,
50///         system_user: SystemUserId,
51///     },
52///     Signer {
53///         backend_id: u8,
54///         system_user: SystemUserId,
55///     },
56/// }
57///
58/// impl ExampleUserMapping {
59///     pub fn backend_user_id(&self) -> u8 {
60///         match self {
61///             Self::Admin { backend_id }
62///             | Self::Backup { backend_id, .. }
63///             | Self::Metrics { backend_id, .. }
64///             | Self::Signer { backend_id, .. } => *backend_id,
65///         }
66///     }
67/// }
68///
69/// impl MappingSystemUserId for ExampleUserMapping {
70///     fn system_user_id(&self) -> Option<&SystemUserId> {
71///         match self {
72///             Self::Admin { .. } => None,
73///             Self::Backup { system_user, .. }
74///             | Self::Metrics { system_user, .. }
75///             | Self::Signer { system_user, .. } => Some(system_user),
76///         }
77///     }
78/// }
79/// ```
80pub trait MappingSystemUserId {
81    /// Returns a reference to the [`SystemUserId`].
82    ///
83    /// # Note
84    ///
85    /// Should return [`None`], if the user mapping implementation does not track a system user.
86    fn system_user_id(&self) -> Option<&SystemUserId>;
87
88    /// Returns the tracked system user ID as [`User`] if it exists.
89    ///
90    /// This is a default implementation and should require no specific implementation.
91    ///
92    /// # Note
93    ///
94    /// Returns `Ok(None)`, if [`MappingSystemUserId::system_user_id`] returns [`None`] (the user
95    /// mapping implementation tracks no system user).
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if no Unix user of the mapping's system user name exists.
100    fn system_user_id_as_existing_unix_user(&self) -> Result<Option<User>, crate::Error> {
101        let Some(system_user_id) = self.system_user_id() else {
102            return Ok(None);
103        };
104
105        // NOTE: We ignore the potential `None` return value of `User::from_name` because it would
106        // mean an invalid system user name (which cannot happen due to validation).
107        Ok(User::from_name(system_user_id.as_ref()).map_err(|source| {
108            crate::utils::Error::SystemUserLookup {
109                user: crate::utils::NameOrUid::Name(system_user_id.clone()),
110                source,
111            }
112        })?)
113    }
114
115    /// Returns the tracked system user ID as the current [`User`] if it exists.
116    ///
117    /// This is a default implementation and should require no specific implementation.
118    ///
119    /// # Note
120    ///
121    /// Returns `Ok(None)`, if [`MappingSystemUserId::system_user_id`] returns [`None`] (the user
122    /// mapping implementation tracks no system user).
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if
127    ///
128    /// - retrieving the effective User ID of the current Unix user fails,
129    /// - the currently calling system user does not match the one returned by
130    ///   [`MappingSystemUserId::system_user_id`],
131    fn system_user_id_as_current_unix_user(&self) -> Result<Option<User>, crate::Error> {
132        let Some(system_user_id) = self.system_user_id() else {
133            return Ok(None);
134        };
135        let current_system_user = get_current_system_user()?;
136
137        if current_system_user.name != system_user_id.as_ref() {
138            return Err(crate::utils::Error::SystemUserMismatch {
139                target_user: system_user_id.to_string(),
140                current_user: current_system_user.name,
141            }
142            .into());
143        }
144
145        Ok(Some(current_system_user))
146    }
147}
148
149/// The kind of backend user.
150///
151/// This distinguishes between the different access rights levels (i.e. administrative and
152/// non-administrative) and roles (e.g. backup, metrics, signing) of a backend user.
153#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
154pub enum BackendUserIdKind {
155    /// Any user.
156    #[default]
157    Any,
158
159    /// Administrative user.
160    Admin,
161
162    /// Backup user.
163    Backup,
164
165    /// Metrics user.
166    Metrics,
167
168    /// Any non-administrative user.
169    NonAdmin,
170
171    /// User used to observe keys, without access to them.
172    Observer,
173
174    /// Signing user.
175    Signing,
176}
177
178/// A filter for user mapping variants.
179#[derive(Clone, Debug, Default, Eq, PartialEq)]
180pub struct BackendUserIdFilter {
181    /// The kind of backend user.
182    pub backend_user_id_kind: BackendUserIdKind,
183}
184
185/// An interface for returning a list of backend users based on a filter.
186///
187/// # Example
188///
189/// ```
190/// use signstar_config::{
191///     Error,
192///     SystemUserId,
193///     config::{BackendUserIdFilter, BackendUserIdKind, MappingBackendUserIds},
194/// };
195/// use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
196///
197/// #[derive(Debug)]
198/// struct ExampleCreds {
199///     pub id: u8,
200///     pub passphrase: Passphrase,
201/// }
202///
203/// impl UserWithPassphrase for ExampleCreds {
204///     fn user(&self) -> String {
205///         self.id.to_string()
206///     }
207///
208///     fn passphrase(&self) -> &Passphrase {
209///         &self.passphrase
210///     }
211/// }
212///
213/// #[derive(Debug)]
214/// enum ExampleUserMapping {
215///     Admin {
216///         backend_id: u8,
217///     },
218///     Backup {
219///         backend_id: u8,
220///         system_user: SystemUserId,
221///     },
222///     Metrics {
223///         backend_id: u8,
224///         system_user: SystemUserId,
225///     },
226///     Signer {
227///         backend_id: u8,
228///         system_user: SystemUserId,
229///     },
230/// }
231///
232/// impl ExampleUserMapping {
233///     pub fn backend_user_id(&self) -> u8 {
234///         match self {
235///             Self::Admin { backend_id }
236///             | Self::Backup { backend_id, .. }
237///             | Self::Metrics { backend_id, .. }
238///             | Self::Signer { backend_id, .. } => *backend_id,
239///         }
240///     }
241/// }
242///
243/// impl MappingBackendUserIds for ExampleUserMapping {
244///     fn backend_user_ids(&self, filter: BackendUserIdFilter) -> Vec<String> {
245///         match self {
246///             Self::Admin { backend_id, .. } => {
247///                 if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
248///                     .contains(&filter.backend_user_id_kind)
249///                 {
250///                     return vec![backend_id.to_string()];
251///                 }
252///             }
253///             Self::Backup { backend_id, .. } => {
254///                 if [
255///                     BackendUserIdKind::Any,
256///                     BackendUserIdKind::Backup,
257///                     BackendUserIdKind::NonAdmin,
258///                 ]
259///                 .contains(&filter.backend_user_id_kind)
260///                 {
261///                     return vec![backend_id.to_string()];
262///                 }
263///             }
264///             Self::Metrics { backend_id, .. } => {
265///                 if [
266///                     BackendUserIdKind::Any,
267///                     BackendUserIdKind::Metrics,
268///                     BackendUserIdKind::NonAdmin,
269///                 ]
270///                 .contains(&filter.backend_user_id_kind)
271///                 {
272///                     return vec![backend_id.to_string()];
273///                 }
274///             }
275///             Self::Signer { backend_id, .. } => {
276///                 if [
277///                     BackendUserIdKind::Any,
278///                     BackendUserIdKind::Signing,
279///                     BackendUserIdKind::NonAdmin,
280///                 ]
281///                 .contains(&filter.backend_user_id_kind)
282///                 {
283///                     return vec![backend_id.to_string()];
284///                 }
285///             }
286///         }
287///
288///         Vec::new()
289///     }
290///
291///     fn backend_user_with_passphrase(
292///         &self,
293///         name: &str,
294///         passphrase: Passphrase,
295///     ) -> Result<Box<dyn UserWithPassphrase>, Error> {
296///         let backend_user_id = self.backend_user_id();
297///         if backend_user_id.to_string() != name {
298///             return Err(
299///                 signstar_config::config::TraitsError::BackendUserIdMismatch {
300///                     expected: name.to_string(),
301///                     actual: backend_user_id.to_string(),
302///                 }
303///                 .into(),
304///             );
305///         }
306///
307///         Ok(Box::new(ExampleCreds {
308///             id: backend_user_id,
309///             passphrase,
310///         }))
311///     }
312///
313///     fn backend_users_with_new_passphrase(
314///         &self,
315///         filter: BackendUserIdFilter,
316///     ) -> Vec<Box<dyn UserWithPassphrase>> {
317///         if let Some(backend_id) = match self {
318///             Self::Admin { backend_id, .. } => {
319///                 if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
320///                     .contains(&filter.backend_user_id_kind)
321///                 {
322///                     Some(*backend_id)
323///                 } else {
324///                     None
325///                 }
326///             }
327///             Self::Backup { backend_id, .. } => {
328///                 if [
329///                     BackendUserIdKind::Any,
330///                     BackendUserIdKind::Backup,
331///                     BackendUserIdKind::NonAdmin,
332///                 ]
333///                 .contains(&filter.backend_user_id_kind)
334///                 {
335///                     Some(*backend_id)
336///                 } else {
337///                     None
338///                 }
339///             }
340///             Self::Metrics { backend_id, .. } => {
341///                 if [
342///                     BackendUserIdKind::Any,
343///                     BackendUserIdKind::Metrics,
344///                     BackendUserIdKind::NonAdmin,
345///                 ]
346///                 .contains(&filter.backend_user_id_kind)
347///                 {
348///                     Some(*backend_id)
349///                 } else {
350///                     None
351///                 }
352///             }
353///             Self::Signer { backend_id, .. } => {
354///                 if [
355///                     BackendUserIdKind::Any,
356///                     BackendUserIdKind::Signing,
357///                     BackendUserIdKind::NonAdmin,
358///                 ]
359///                 .contains(&filter.backend_user_id_kind)
360///                 {
361///                     Some(*backend_id)
362///                 } else {
363///                     None
364///                 }
365///             }
366///         } {
367///             vec![Box::new(ExampleCreds {
368///                 id: backend_id,
369///                 passphrase: Passphrase::generate(None),
370///             })]
371///         } else {
372///             Vec::new()
373///         }
374///     }
375/// }
376///
377/// # fn main() -> testresult::TestResult {
378/// let backend_id = 1;
379/// let mapping = ExampleUserMapping::Backup {
380///     backend_id,
381///     system_user: "backup".parse()?,
382/// };
383///
384/// // Find backend user IDs based on the kind of user.
385/// assert!(
386///     mapping
387///         .backend_user_ids(BackendUserIdFilter {
388///             backend_user_id_kind: BackendUserIdKind::Backup
389///         })
390///         .first()
391///         .is_some_and(|id| *id == backend_id.to_string())
392/// );
393/// // This returns an empty list if there are no matches.
394/// assert!(
395///     mapping
396///         .backend_user_ids(BackendUserIdFilter {
397///             backend_user_id_kind: BackendUserIdKind::Admin
398///         })
399///         .is_empty()
400/// );
401///
402/// // Create credentials based on a user and a passphrase.
403/// let passphrase = Passphrase::generate(None);
404/// let creds = mapping.backend_user_with_passphrase("1", passphrase.clone())?;
405/// assert_eq!(creds.user(), backend_id.to_string());
406/// assert_eq!(
407///     creds.passphrase().expose_borrowed(),
408///     passphrase.expose_borrowed()
409/// );
410/// // The user ID has to match when creating credentials!
411/// assert!(
412///     mapping
413///         .backend_user_with_passphrase("foo", passphrase.clone())
414///         .is_err()
415/// );
416///
417/// // Create new credentials with new passphrase based on backend user ID.
418/// assert!(
419///     mapping
420///         .backend_users_with_new_passphrase(BackendUserIdFilter {
421///             backend_user_id_kind: BackendUserIdKind::Backup
422///         })
423///         .first()
424///         .is_some_and(|creds| creds.user() == backend_id.to_string())
425/// );
426/// # Ok(())
427/// # }
428/// ```
429pub trait MappingBackendUserIds {
430    /// Returns a list of [`String`]s representing backend User IDs according to a `filter`.
431    fn backend_user_ids(&self, filter: BackendUserIdFilter) -> Vec<String>;
432
433    /// Returns a specific [`UserWithPassphrase`] implementation for a backend user.
434    ///
435    /// # Errors
436    ///
437    /// Returns an error if `user` matches no backend user of the user mapping.
438    /// Note, that implementations may use [`Error::BackendUserIdMismatch`] for this, if they do not
439    /// wish to create their own error variant for this purpose.
440    fn backend_user_with_passphrase(
441        &self,
442        name: &str,
443        passphrase: Passphrase,
444    ) -> Result<Box<dyn UserWithPassphrase>, crate::Error>;
445
446    /// Returns a list of [`UserWithPassphrase`] implementations according to a `filter`.
447    ///
448    /// For each returned backend user a new [`Passphrase`] is generated using the default settings
449    /// of [`Passphrase::generate`].
450    ///
451    /// With an implementation of [`BackendUserIdFilter`] it is possible to target specific kinds of
452    /// backend users.
453    fn backend_users_with_new_passphrase(
454        &self,
455        filter: BackendUserIdFilter,
456    ) -> Vec<Box<dyn UserWithPassphrase>>;
457}
458
459/// An interface for returning an optional SSH `authorized_keys` entry.
460///
461/// # Example
462///
463/// ```
464/// use signstar_config::{AuthorizedKeyEntry, SystemUserId, config::MappingAuthorizedKeyEntry};
465/// use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
466///
467/// #[derive(Debug)]
468/// enum ExampleUserMapping {
469///     Admin {
470///         backend_id: u8,
471///     },
472///     Backup {
473///         backend_id: u8,
474///         ssh_authorized_key: AuthorizedKeyEntry,
475///         system_user: SystemUserId,
476///     },
477///     Metrics {
478///         backend_id: u8,
479///         ssh_authorized_key: AuthorizedKeyEntry,
480///         system_user: SystemUserId,
481///     },
482///     Signer {
483///         backend_id: u8,
484///         ssh_authorized_key: AuthorizedKeyEntry,
485///         system_user: SystemUserId,
486///     },
487/// }
488///
489/// impl MappingAuthorizedKeyEntry for ExampleUserMapping {
490///     fn authorized_key_entry(&self) -> Option<&AuthorizedKeyEntry> {
491///         match self {
492///             Self::Admin { .. } => None,
493///             Self::Backup {
494///                 ssh_authorized_key, ..
495///             }
496///             | Self::Metrics {
497///                 ssh_authorized_key, ..
498///             }
499///             | Self::Signer {
500///                 ssh_authorized_key, ..
501///             } => Some(ssh_authorized_key),
502///         }
503///     }
504/// }
505///
506/// # fn main() -> testresult::TestResult {
507/// let ssh_authorized_key: AuthorizedKeyEntry = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?;
508/// let mapping = ExampleUserMapping::Backup{
509///     backend_id: 1,
510///     ssh_authorized_key: ssh_authorized_key.clone(),
511///     system_user: "backup".parse()?,
512/// };
513/// assert!(mapping.authorized_key_entry().is_some_and(|key| key == &ssh_authorized_key));
514/// # Ok(())
515/// # }
516/// ```
517pub trait MappingAuthorizedKeyEntry {
518    /// Returns an optional SSH `authorized_keys` entry.
519    ///
520    /// Implementations must return [`None`] if the specific mapping does not provide any
521    /// [`AuthorizedKeyEntry`].
522    fn authorized_key_entry(&self) -> Option<&AuthorizedKeyEntry>;
523}
524
525/// An interface to define a generic filter when evaluating the key IDs of a backend.
526pub trait BackendKeyIdFilter: Clone {}
527
528/// An interface for returning a list of key IDs based on a filter.
529///
530/// The associated filter is an implementation of [`BackendKeyIdFilter`] and should target the
531/// inherent details of a cryptographic key in the backend.
532/// A key ID must only be returned, if the filter fully matches.
533///
534/// # Example
535///
536/// ```
537/// use signstar_config::{
538///     SystemUserId,
539///     config::{BackendKeyIdFilter, MappingBackendKeyId},
540/// };
541/// use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
542///
543/// #[derive(Clone, Debug, PartialEq)]
544/// enum KeyFeature {
545///     V1,
546///     V2,
547/// }
548///
549/// #[derive(Debug)]
550/// struct KeySetup {
551///     pub feature: KeyFeature,
552///     pub id: u8,
553/// }
554///
555/// #[derive(Debug)]
556/// enum ExampleUserMapping {
557///     Admin {
558///         backend_id: u8,
559///     },
560///     Backup {
561///         backend_id: u8,
562///         system_user: SystemUserId,
563///     },
564///     Metrics {
565///         backend_id: u8,
566///         system_user: SystemUserId,
567///     },
568///     Signer {
569///         backend_id: u8,
570///         key_setup: KeySetup,
571///         system_user: SystemUserId,
572///     },
573/// }
574///
575/// #[derive(Clone, Debug)]
576/// struct KeyFilter {
577///     pub feature: KeyFeature,
578/// }
579///
580/// impl BackendKeyIdFilter for KeyFilter {}
581///
582/// impl MappingBackendKeyId<KeyFilter> for ExampleUserMapping {
583///     fn backend_key_id(&self, filter: &KeyFilter) -> Option<String> {
584///         match self {
585///             Self::Admin { .. } | Self::Backup { .. } | Self::Metrics { .. } => None,
586///             Self::Signer { key_setup, .. } => {
587///                 if key_setup.feature == filter.feature {
588///                     Some(key_setup.id.to_string())
589///                 } else {
590///                     None
591///                 }
592///             }
593///         }
594///     }
595/// }
596///
597/// # fn main() -> testresult::TestResult {
598/// let key_id = 1;
599/// let mapping = ExampleUserMapping::Signer {
600///     backend_id: 1,
601///     key_setup: KeySetup {
602///         feature: KeyFeature::V1,
603///         id: key_id,
604///     },
605///     system_user: "backup".parse()?,
606/// };
607///
608/// // A key ID is only returned, if the custom filter matches.
609/// assert!(
610///     mapping
611///         .backend_key_id(&KeyFilter {
612///             feature: KeyFeature::V1
613///         })
614///         .is_some_and(|id| id == key_id.to_string())
615/// );
616/// assert!(
617///     mapping
618///         .backend_key_id(&KeyFilter {
619///             feature: KeyFeature::V2
620///         })
621///         .is_none()
622/// );
623/// # Ok(())
624/// # }
625/// ```
626pub trait MappingBackendKeyId<T>
627where
628    T: BackendKeyIdFilter,
629{
630    /// Returns an optional [`String`] representing a backend key ID according to a `filter`.
631    fn backend_key_id(&self, filter: &T) -> Option<String>;
632}
633
634/// An interface to define a generic filter when evaluating the domains of a backend.
635pub trait BackendDomainFilter {}
636
637/// An interface for returning a backend domain based on an optional filter.
638///
639/// A backend domain usually describes a form of access restriction for a backend.
640/// With them restrictions to the access of cryptographic key material is enforced for backend
641/// users.
642///
643/// If a filter is provided, the domain ID must only be returned if the filter matches.
644///
645/// # Example
646///
647/// ```
648/// use signstar_config::{
649///     SystemUserId,
650///     config::{BackendDomainFilter, MappingBackendDomain},
651/// };
652/// use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
653///
654/// #[derive(Clone, Debug, PartialEq)]
655/// enum KeyFeature {
656///     V1,
657///     V2,
658/// }
659///
660/// #[derive(Debug)]
661/// struct KeySetup {
662///     pub feature: KeyFeature,
663///     pub id: u8,
664/// }
665///
666/// #[derive(Debug)]
667/// struct Domain {
668///     pub id: u8,
669///     pub special: bool,
670/// }
671///
672/// #[derive(Debug)]
673/// enum ExampleUserMapping {
674///     Admin {
675///         backend_id: u8,
676///     },
677///     Backup {
678///         backend_id: u8,
679///         system_user: SystemUserId,
680///     },
681///     Metrics {
682///         backend_id: u8,
683///         system_user: SystemUserId,
684///     },
685///     Signer {
686///         backend_id: u8,
687///         domain: Domain,
688///         key_setup: KeySetup,
689///         system_user: SystemUserId,
690///     },
691/// }
692///
693/// #[derive(Debug)]
694/// struct DomainFilter {
695///     pub special: bool,
696/// }
697///
698/// impl BackendDomainFilter for DomainFilter {}
699///
700/// impl MappingBackendDomain<DomainFilter> for ExampleUserMapping {
701///     fn backend_domain(&self, filter: Option<&DomainFilter>) -> Option<String> {
702///         match self {
703///             Self::Admin { .. } | Self::Backup { .. } | Self::Metrics { .. } => None,
704///             Self::Signer { domain, .. } => {
705///                 if let Some(filter) = filter {
706///                     if domain.special == filter.special {
707///                         Some(domain.id.to_string())
708///                     } else {
709///                         None
710///                     }
711///                 } else {
712///                     Some(domain.id.to_string())
713///                 }
714///             }
715///         }
716///     }
717/// }
718///
719/// # fn main() -> testresult::TestResult {
720/// let domain_id = 1;
721/// let mapping = ExampleUserMapping::Signer {
722///     backend_id: 1,
723///     domain: Domain {
724///         id: domain_id,
725///         special: true,
726///     },
727///     key_setup: KeySetup {
728///         feature: KeyFeature::V1,
729///         id: 1,
730///     },
731///     system_user: "backup".parse()?,
732/// };
733///
734/// // A domain ID is only returned, if the custom filter matches.
735/// assert!(
736///     mapping
737///         .backend_domain(Some(&DomainFilter { special: true }))
738///         .is_some_and(|id| id == domain_id.to_string())
739/// );
740/// // .. or if no filter is provided and a backend user with a domain is found.
741/// assert!(
742///     mapping
743///         .backend_domain(Some(&DomainFilter { special: true }))
744///         .is_some_and(|id| id == domain_id.to_string())
745/// );
746/// assert!(
747///     mapping
748///         .backend_domain(Some(&DomainFilter { special: false }))
749///         .is_none()
750/// );
751/// # Ok(())
752/// # }
753/// ```
754pub trait MappingBackendDomain<T>
755where
756    T: BackendDomainFilter,
757{
758    /// Returns a [`String`] representing a backend domain according to an optional `filter`.
759    fn backend_domain(&self, filter: Option<&T>) -> Option<String>;
760}
761
762/// The kind of non-administrative backend user.
763///
764/// This distinguishes between the different access rights levels (i.e. backup, metrics, observer,
765/// signing) of a non-administrative backend user.
766#[derive(Clone, Copy, Debug, Default)]
767pub enum NonAdminBackendUserIdKind {
768    /// Any non-administrative user.
769    #[default]
770    Any,
771
772    /// Backup user.
773    Backup,
774
775    /// Metrics user.
776    Metrics,
777
778    /// User used to observe keys, without access to them.
779    Observer,
780
781    /// Signing user.
782    Signing,
783}
784
785impl From<NonAdminBackendUserIdKind> for BackendUserIdKind {
786    fn from(value: NonAdminBackendUserIdKind) -> Self {
787        match value {
788            NonAdminBackendUserIdKind::Any => Self::NonAdmin,
789            NonAdminBackendUserIdKind::Backup => Self::Backup,
790            NonAdminBackendUserIdKind::Metrics => Self::Metrics,
791            NonAdminBackendUserIdKind::Observer => Self::Observer,
792            NonAdminBackendUserIdKind::Signing => Self::Signing,
793        }
794    }
795}
796
797/// A filter for non-administrative user mapping variants.
798#[derive(Clone, Debug, Default)]
799pub struct NonAdminBackendUserIdFilter {
800    /// The kind of backend user.
801    pub backend_user_id_kind: NonAdminBackendUserIdKind,
802}
803
804impl From<NonAdminBackendUserIdFilter> for BackendUserIdFilter {
805    fn from(value: NonAdminBackendUserIdFilter) -> Self {
806        Self {
807            backend_user_id_kind: value.backend_user_id_kind.into(),
808        }
809    }
810}
811
812/// An interface to create and load secrets for backend users in user mapping implementations.
813///
814/// Implementations are required to also implement [`MappingBackendUserIds`] and
815/// [`MappingSystemUserId`]
816///
817/// # Example
818///
819/// ```
820/// use signstar_config::{
821///     Error,
822///     SystemUserId,
823///     config::{
824///         BackendUserIdFilter,
825///         BackendUserIdKind,
826///         MappingBackendUserIds,
827///         MappingBackendUserSecrets,
828///         MappingSystemUserId,
829///     },
830/// };
831/// use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
832///
833/// #[derive(Debug)]
834/// struct ExampleCreds {
835///     pub id: u8,
836///     pub passphrase: Passphrase,
837/// }
838///
839/// impl UserWithPassphrase for ExampleCreds {
840///     fn user(&self) -> String {
841///         self.id.to_string()
842///     }
843///
844///     fn passphrase(&self) -> &Passphrase {
845///         &self.passphrase
846///     }
847/// }
848///
849/// #[derive(Debug)]
850/// enum ExampleUserMapping {
851///     Admin {
852///         backend_id: u8,
853///     },
854///     Backup {
855///         backend_id: u8,
856///         system_user: SystemUserId,
857///     },
858///     Metrics {
859///         backend_id: u8,
860///         system_user: SystemUserId,
861///     },
862///     Signer {
863///         backend_id: u8,
864///         system_user: SystemUserId,
865///     },
866/// }
867///
868/// impl ExampleUserMapping {
869///     pub fn backend_user_id(&self) -> u8 {
870///         match self {
871///             Self::Admin { backend_id }
872///             | Self::Backup { backend_id, .. }
873///             | Self::Metrics { backend_id, .. }
874///             | Self::Signer { backend_id, .. } => *backend_id,
875///         }
876///     }
877/// }
878///
879/// impl MappingBackendUserIds for ExampleUserMapping {
880///     fn backend_user_ids(&self, filter: BackendUserIdFilter) -> Vec<String> {
881///         match self {
882///             Self::Admin { backend_id, .. } => {
883///                 if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
884///                     .contains(&filter.backend_user_id_kind)
885///                 {
886///                     return vec![backend_id.to_string()];
887///                 }
888///             }
889///             Self::Backup { backend_id, .. } => {
890///                 if [
891///                     BackendUserIdKind::Any,
892///                     BackendUserIdKind::Backup,
893///                     BackendUserIdKind::NonAdmin,
894///                 ]
895///                 .contains(&filter.backend_user_id_kind)
896///                 {
897///                     return vec![backend_id.to_string()];
898///                 }
899///             }
900///             Self::Metrics { backend_id, .. } => {
901///                 if [
902///                     BackendUserIdKind::Any,
903///                     BackendUserIdKind::Metrics,
904///                     BackendUserIdKind::NonAdmin,
905///                 ]
906///                 .contains(&filter.backend_user_id_kind)
907///                 {
908///                     return vec![backend_id.to_string()];
909///                 }
910///             }
911///             Self::Signer { backend_id, .. } => {
912///                 if [
913///                     BackendUserIdKind::Any,
914///                     BackendUserIdKind::Signing,
915///                     BackendUserIdKind::NonAdmin,
916///                 ]
917///                 .contains(&filter.backend_user_id_kind)
918///                 {
919///                     return vec![backend_id.to_string()];
920///                 }
921///             }
922///         }
923///
924///         Vec::new()
925///     }
926///
927///     fn backend_user_with_passphrase(
928///         &self,
929///         name: &str,
930///         passphrase: Passphrase,
931///     ) -> Result<Box<dyn UserWithPassphrase>, Error> {
932///         let backend_user_id = self.backend_user_id();
933///         if backend_user_id.to_string() != name {
934///             return Err(
935///                 signstar_config::config::TraitsError::BackendUserIdMismatch {
936///                     expected: name.to_string(),
937///                     actual: backend_user_id.to_string(),
938///                 }
939///                 .into(),
940///             );
941///         }
942///
943///         Ok(Box::new(ExampleCreds {
944///             id: backend_user_id,
945///             passphrase,
946///         }))
947///     }
948///
949///     fn backend_users_with_new_passphrase(
950///         &self,
951///         filter: BackendUserIdFilter,
952///     ) -> Vec<Box<dyn UserWithPassphrase>> {
953///         if let Some(backend_id) = match self {
954///             Self::Admin { backend_id, .. } => {
955///                 if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
956///                     .contains(&filter.backend_user_id_kind)
957///                 {
958///                     Some(*backend_id)
959///                 } else {
960///                     None
961///                 }
962///             }
963///             Self::Backup { backend_id, .. } => {
964///                 if [
965///                     BackendUserIdKind::Any,
966///                     BackendUserIdKind::Backup,
967///                     BackendUserIdKind::NonAdmin,
968///                 ]
969///                 .contains(&filter.backend_user_id_kind)
970///                 {
971///                     Some(*backend_id)
972///                 } else {
973///                     None
974///                 }
975///             }
976///             Self::Metrics { backend_id, .. } => {
977///                 if [
978///                     BackendUserIdKind::Any,
979///                     BackendUserIdKind::Metrics,
980///                     BackendUserIdKind::NonAdmin,
981///                 ]
982///                 .contains(&filter.backend_user_id_kind)
983///                 {
984///                     Some(*backend_id)
985///                 } else {
986///                     None
987///                 }
988///             }
989///             Self::Signer { backend_id, .. } => {
990///                 if [
991///                     BackendUserIdKind::Any,
992///                     BackendUserIdKind::Signing,
993///                     BackendUserIdKind::NonAdmin,
994///                 ]
995///                 .contains(&filter.backend_user_id_kind)
996///                 {
997///                     Some(*backend_id)
998///                 } else {
999///                     None
1000///                 }
1001///             }
1002///         } {
1003///             vec![Box::new(ExampleCreds {
1004///                 id: backend_id,
1005///                 passphrase: Passphrase::generate(None),
1006///             })]
1007///         } else {
1008///             Vec::new()
1009///         }
1010///     }
1011/// }
1012///
1013/// impl MappingSystemUserId for ExampleUserMapping {
1014///     fn system_user_id(&self) -> Option<&SystemUserId> {
1015///         match self {
1016///             Self::Admin { .. } => None,
1017///             Self::Backup { system_user, .. }
1018///             | Self::Metrics { system_user, .. }
1019///             | Self::Signer { system_user, .. } => Some(system_user),
1020///         }
1021///     }
1022/// }
1023///
1024/// impl MappingBackendUserSecrets for ExampleUserMapping {}
1025/// ```
1026pub trait MappingBackendUserSecrets: MappingSystemUserId + MappingBackendUserIds {
1027    /// Creates on-disk secrets for non-administrative backend users of the mapping.
1028    ///
1029    /// Returns a list of the created credentials as [`UserWithPassphrase`] implementations.
1030    ///
1031    /// # Note
1032    ///
1033    /// Returns `Ok(None)`, if the mapping implementation tracks no system user.
1034    ///
1035    /// # Errors
1036    ///
1037    /// Returns an error if
1038    ///
1039    /// - the system user in the user mapping does not match an existing Unix user
1040    /// - the user calling this function is not root
1041    /// - [`write_passphrase_to_secrets_file`] fails for one of the newly generated passphrases
1042    fn create_non_admin_backend_user_secrets(
1043        &self,
1044        secret_handling: NonAdministrativeSecretHandling,
1045    ) -> Result<Option<Vec<Box<dyn UserWithPassphrase>>>, crate::Error> {
1046        let Some(user) = self.system_user_id_as_existing_unix_user()? else {
1047            // The mapping implementation does not track a system user.
1048            return Ok(None);
1049        };
1050
1051        // Get credentials for all non-admin backend users (with newly generated passphrases).
1052        let credentials = self.backend_users_with_new_passphrase(BackendUserIdFilter {
1053            backend_user_id_kind: BackendUserIdKind::NonAdmin,
1054        });
1055
1056        // Write the passphrase for each set of credentials to disk.
1057        for creds in credentials.iter() {
1058            write_passphrase_to_secrets_file(
1059                secret_handling,
1060                &user,
1061                &creds.user(),
1062                creds.passphrase(),
1063            )?
1064        }
1065
1066        Ok(Some(credentials))
1067    }
1068
1069    /// Loads secrets from on-disk files for each non-administrative backend user matching a
1070    /// `filter`.
1071    ///
1072    /// Returns a list of the loaded credentials as [`UserWithPassphrase`] implementations.
1073    ///
1074    /// # Notes
1075    ///
1076    /// Returns `Ok(None)`, if the mapping implementation tracks no system user.
1077    ///
1078    /// The system user of the user mapping implementation must match the effective user of the
1079    /// current process.
1080    ///
1081    /// Delegates to [`load_passphrase_from_secrets_file`] for the loading of a single
1082    /// [`Passphrase`] from a secrets file.
1083    ///
1084    /// # Errors
1085    ///
1086    /// Returns an error if
1087    ///
1088    /// - the system user in the user mapping does not match the currently calling Unix user
1089    /// - [`load_passphrase_from_secrets_file`] fails for one of the secret files of the system user
1090    ///   of the mapping
1091    fn load_non_admin_backend_user_secrets(
1092        &self,
1093        secret_handling: NonAdministrativeSecretHandling,
1094        filter: NonAdminBackendUserIdFilter,
1095    ) -> Result<Option<Vec<Box<dyn UserWithPassphrase>>>, crate::Error> {
1096        let Some(system_user) = self.system_user_id_as_current_unix_user()? else {
1097            // The mapping implementation does not track a system user.
1098            return Ok(None);
1099        };
1100
1101        let mut credentials = Vec::new();
1102
1103        for backend_user in self.backend_user_ids(filter.into()) {
1104            credentials.push(self.backend_user_with_passphrase(
1105                &backend_user,
1106                load_passphrase_from_secrets_file(secret_handling, &system_user, &backend_user)?,
1107            )?);
1108        }
1109
1110        Ok(Some(credentials))
1111    }
1112}
1113
1114/// An interface for returning all [`SystemUserId`]s tracked by a configuration implementation.
1115///
1116/// # Example
1117///
1118/// ```
1119/// use std::collections::HashSet;
1120///
1121/// use signstar_config::{
1122///     SystemUserId,
1123///     config::{ConfigSystemUserIds, MappingSystemUserId},
1124/// };
1125/// use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
1126///
1127/// #[derive(Debug, Eq, Hash, PartialEq)]
1128/// enum ExampleUserMapping {
1129///     Admin {
1130///         backend_id: u8,
1131///     },
1132///     Backup {
1133///         backend_id: u8,
1134///         system_user: SystemUserId,
1135///     },
1136///     Metrics {
1137///         backend_id: u8,
1138///         system_user: SystemUserId,
1139///     },
1140///     Signer {
1141///         backend_id: u8,
1142///         system_user: SystemUserId,
1143///     },
1144/// }
1145///
1146/// impl ExampleUserMapping {
1147///     pub fn backend_user_id(&self) -> u8 {
1148///         match self {
1149///             Self::Admin { backend_id }
1150///             | Self::Backup { backend_id, .. }
1151///             | Self::Metrics { backend_id, .. }
1152///             | Self::Signer { backend_id, .. } => *backend_id,
1153///         }
1154///     }
1155/// }
1156///
1157/// impl MappingSystemUserId for ExampleUserMapping {
1158///     fn system_user_id(&self) -> Option<&SystemUserId> {
1159///         match self {
1160///             Self::Admin { .. } => None,
1161///             Self::Backup { system_user, .. }
1162///             | Self::Metrics { system_user, .. }
1163///             | Self::Signer { system_user, .. } => Some(system_user),
1164///         }
1165///     }
1166/// }
1167///
1168/// #[derive(Debug)]
1169/// struct Config {
1170///     pub mappings: HashSet<ExampleUserMapping>,
1171/// }
1172///
1173/// impl ConfigSystemUserIds for Config {
1174///     fn system_user_ids(&self) -> HashSet<&SystemUserId> {
1175///         self.mappings
1176///             .iter()
1177///             .filter_map(|mapping| mapping.system_user_id())
1178///             .collect::<HashSet<_>>()
1179///     }
1180/// }
1181///
1182/// # fn main() -> testresult::TestResult {
1183/// let config = Config {
1184///     mappings: HashSet::from_iter([
1185///         ExampleUserMapping::Admin { backend_id: 1 },
1186///         ExampleUserMapping::Backup {
1187///             backend_id: 2,
1188///             system_user: "backup".parse()?,
1189///         },
1190///         ExampleUserMapping::Metrics {
1191///             backend_id: 3,
1192///             system_user: "metrics".parse()?,
1193///         },
1194///         ExampleUserMapping::Signer {
1195///             backend_id: 3,
1196///             system_user: "signer".parse()?,
1197///         },
1198///     ]),
1199/// };
1200/// let system_users: HashSet<SystemUserId> =
1201///     HashSet::from_iter(["backup".parse()?, "metrics".parse()?, "signer".parse()?]);
1202///
1203/// assert_eq!(
1204///     config.system_user_ids(),
1205///     system_users.iter().collect::<HashSet<_>>()
1206/// );
1207/// # Ok(())
1208/// # }
1209/// ```
1210pub trait ConfigSystemUserIds {
1211    /// Returns the list of all [`SystemUserId`]s.
1212    fn system_user_ids(&self) -> HashSet<&SystemUserId>;
1213}
1214
1215/// An interface for returning all [`AuthorizedKeyEntry`]s tracked by a configuration
1216/// implementation.
1217///
1218/// # Example
1219///
1220/// ```
1221/// use std::collections::HashSet;
1222///
1223/// use signstar_config::{AuthorizedKeyEntry, SystemUserId, config::{ConfigAuthorizedKeyEntries, MappingAuthorizedKeyEntry}};
1224/// use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
1225///
1226/// #[derive(Debug, Eq, Hash, PartialEq)]
1227/// enum ExampleUserMapping {
1228///     Admin {
1229///         backend_id: u8,
1230///     },
1231///     Backup {
1232///         backend_id: u8,
1233///         ssh_authorized_key: AuthorizedKeyEntry,
1234///         system_user: SystemUserId,
1235///     },
1236///     Metrics {
1237///         backend_id: u8,
1238///         ssh_authorized_key: AuthorizedKeyEntry,
1239///         system_user: SystemUserId,
1240///     },
1241///     Signer {
1242///         backend_id: u8,
1243///         ssh_authorized_key: AuthorizedKeyEntry,
1244///         system_user: SystemUserId,
1245///     },
1246/// }
1247///
1248/// impl MappingAuthorizedKeyEntry for ExampleUserMapping {
1249///     fn authorized_key_entry(&self) -> Option<&AuthorizedKeyEntry> {
1250///         match self {
1251///             Self::Admin { .. } => None,
1252///             Self::Backup {
1253///                 ssh_authorized_key, ..
1254///             }
1255///             | Self::Metrics {
1256///                 ssh_authorized_key, ..
1257///             }
1258///             | Self::Signer {
1259///                 ssh_authorized_key, ..
1260///             } => Some(ssh_authorized_key),
1261///         }
1262///     }
1263/// }
1264///
1265/// #[derive(Debug)]
1266/// struct Config {
1267///     pub mappings: HashSet<ExampleUserMapping>,
1268/// }
1269///
1270/// impl ConfigAuthorizedKeyEntries for Config {
1271///     fn authorized_key_entries(&self) -> HashSet<&AuthorizedKeyEntry> {
1272///         self.mappings
1273///             .iter()
1274///             .filter_map(|mapping| mapping.authorized_key_entry())
1275///             .collect::<HashSet<_>>()
1276///     }
1277/// }
1278///
1279/// # fn main() -> testresult::TestResult {
1280/// let config = Config {
1281///     mappings: HashSet::from_iter([
1282///     ExampleUserMapping::Admin {
1283///         backend_id: 1,
1284///     },
1285///     ExampleUserMapping::Backup {
1286///         backend_id: 2,
1287///         ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1288///         system_user: "backup".parse()?,
1289///     },
1290///     ExampleUserMapping::Signer {
1291///         backend_id: 3,
1292///         ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1293///         system_user: "signer".parse()?,
1294///     },
1295///     ])
1296/// };
1297/// let ssh_authorized_keys: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
1298/// "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1299/// "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?
1300/// ]);
1301///
1302/// assert_eq!(config.authorized_key_entries(), ssh_authorized_keys.iter().collect::<HashSet<_>>());
1303/// # Ok(())
1304/// # }
1305/// ```
1306pub trait ConfigAuthorizedKeyEntries {
1307    /// Returns the list of all [`AuthorizedKeyEntry`]s.
1308    fn authorized_key_entries(&self) -> HashSet<&AuthorizedKeyEntry>;
1309}
1310
1311#[cfg(test)]
1312mod tests {
1313    use rstest::rstest;
1314    use testresult::TestResult;
1315
1316    use super::*;
1317
1318    #[derive(Debug)]
1319    struct ExampleCreds {
1320        pub id: u8,
1321        pub passphrase: Passphrase,
1322    }
1323
1324    impl UserWithPassphrase for ExampleCreds {
1325        fn user(&self) -> String {
1326            self.id.to_string()
1327        }
1328
1329        fn passphrase(&self) -> &Passphrase {
1330            &self.passphrase
1331        }
1332    }
1333
1334    #[derive(Debug)]
1335    enum ExampleUserMapping {
1336        Admin { backend_id: u8 },
1337        Backup { backend_id: u8 },
1338        Metrics { backend_id: u8 },
1339        Observer { backend_id: u8 },
1340        Signer { backend_id: u8 },
1341    }
1342
1343    impl ExampleUserMapping {
1344        pub fn backend_user_id(&self) -> u8 {
1345            match self {
1346                Self::Admin { backend_id }
1347                | Self::Backup { backend_id }
1348                | Self::Metrics { backend_id }
1349                | Self::Observer { backend_id }
1350                | Self::Signer { backend_id } => *backend_id,
1351            }
1352        }
1353    }
1354
1355    impl MappingBackendUserIds for ExampleUserMapping {
1356        fn backend_user_ids(&self, filter: BackendUserIdFilter) -> Vec<String> {
1357            match self {
1358                Self::Admin { backend_id } => {
1359                    if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
1360                        .contains(&filter.backend_user_id_kind)
1361                    {
1362                        return vec![backend_id.to_string()];
1363                    }
1364                }
1365                Self::Backup { backend_id } => {
1366                    if [
1367                        BackendUserIdKind::Backup,
1368                        BackendUserIdKind::NonAdmin,
1369                        BackendUserIdKind::Any,
1370                    ]
1371                    .contains(&filter.backend_user_id_kind)
1372                    {
1373                        return vec![backend_id.to_string()];
1374                    }
1375                }
1376                Self::Metrics { backend_id } => {
1377                    if [
1378                        BackendUserIdKind::Metrics,
1379                        BackendUserIdKind::NonAdmin,
1380                        BackendUserIdKind::Any,
1381                    ]
1382                    .contains(&filter.backend_user_id_kind)
1383                    {
1384                        return vec![backend_id.to_string()];
1385                    }
1386                }
1387                Self::Observer { backend_id } => {
1388                    if [
1389                        BackendUserIdKind::Observer,
1390                        BackendUserIdKind::NonAdmin,
1391                        BackendUserIdKind::Any,
1392                    ]
1393                    .contains(&filter.backend_user_id_kind)
1394                    {
1395                        return vec![backend_id.to_string()];
1396                    }
1397                }
1398                Self::Signer { backend_id } => {
1399                    if [
1400                        BackendUserIdKind::Signing,
1401                        BackendUserIdKind::NonAdmin,
1402                        BackendUserIdKind::Any,
1403                    ]
1404                    .contains(&filter.backend_user_id_kind)
1405                    {
1406                        return vec![backend_id.to_string()];
1407                    }
1408                }
1409            }
1410
1411            Vec::new()
1412        }
1413
1414        fn backend_user_with_passphrase(
1415            &self,
1416            name: &str,
1417            passphrase: Passphrase,
1418        ) -> Result<Box<dyn UserWithPassphrase>, crate::Error> {
1419            let backend_user_id = self.backend_user_id();
1420            if backend_user_id.to_string() != name {
1421                return Err(Error::BackendUserIdMismatch {
1422                    expected: name.to_string(),
1423                    actual: backend_user_id.to_string(),
1424                }
1425                .into());
1426            }
1427
1428            Ok(Box::new(ExampleCreds {
1429                id: backend_user_id,
1430                passphrase,
1431            }))
1432        }
1433
1434        fn backend_users_with_new_passphrase(
1435            &self,
1436            filter: BackendUserIdFilter,
1437        ) -> Vec<Box<dyn UserWithPassphrase>> {
1438            if let Some(backend_id) = match self {
1439                Self::Admin { backend_id } => {
1440                    if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
1441                        .contains(&filter.backend_user_id_kind)
1442                    {
1443                        Some(*backend_id)
1444                    } else {
1445                        None
1446                    }
1447                }
1448                Self::Backup { backend_id } => {
1449                    if [
1450                        BackendUserIdKind::Backup,
1451                        BackendUserIdKind::NonAdmin,
1452                        BackendUserIdKind::Any,
1453                    ]
1454                    .contains(&filter.backend_user_id_kind)
1455                    {
1456                        Some(*backend_id)
1457                    } else {
1458                        None
1459                    }
1460                }
1461                Self::Metrics { backend_id } => {
1462                    if [
1463                        BackendUserIdKind::Metrics,
1464                        BackendUserIdKind::NonAdmin,
1465                        BackendUserIdKind::Any,
1466                    ]
1467                    .contains(&filter.backend_user_id_kind)
1468                    {
1469                        Some(*backend_id)
1470                    } else {
1471                        None
1472                    }
1473                }
1474                Self::Observer { backend_id } => {
1475                    if [
1476                        BackendUserIdKind::Observer,
1477                        BackendUserIdKind::NonAdmin,
1478                        BackendUserIdKind::Any,
1479                    ]
1480                    .contains(&filter.backend_user_id_kind)
1481                    {
1482                        Some(*backend_id)
1483                    } else {
1484                        None
1485                    }
1486                }
1487                Self::Signer { backend_id } => {
1488                    if [
1489                        BackendUserIdKind::Signing,
1490                        BackendUserIdKind::NonAdmin,
1491                        BackendUserIdKind::Any,
1492                    ]
1493                    .contains(&filter.backend_user_id_kind)
1494                    {
1495                        Some(*backend_id)
1496                    } else {
1497                        None
1498                    }
1499                }
1500            } {
1501                vec![Box::new(ExampleCreds {
1502                    id: backend_id,
1503                    passphrase: Passphrase::generate(None),
1504                })]
1505            } else {
1506                Vec::new()
1507            }
1508        }
1509    }
1510
1511    // NonAdminBackendUserIdFilter
1512    #[rstest]
1513    #[case(NonAdminBackendUserIdKind::Any, BackendUserIdKind::NonAdmin)]
1514    #[case(NonAdminBackendUserIdKind::Backup, BackendUserIdKind::Backup)]
1515    #[case(NonAdminBackendUserIdKind::Observer, BackendUserIdKind::Observer)]
1516    #[case(NonAdminBackendUserIdKind::Metrics, BackendUserIdKind::Metrics)]
1517    #[case(NonAdminBackendUserIdKind::Signing, BackendUserIdKind::Signing)]
1518    fn non_admin_backend_user_id_kind_to_backend_user_id_kind(
1519        #[case] input: NonAdminBackendUserIdKind,
1520        #[case] output: BackendUserIdKind,
1521    ) {
1522        let transform: BackendUserIdKind = input.into();
1523
1524        assert_eq!(transform, output)
1525    }
1526
1527    #[rstest]
1528    #[case(NonAdminBackendUserIdFilter{ backend_user_id_kind: NonAdminBackendUserIdKind::Any }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1529    #[case(NonAdminBackendUserIdFilter{ backend_user_id_kind: NonAdminBackendUserIdKind::Backup }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup })]
1530    #[case(NonAdminBackendUserIdFilter{ backend_user_id_kind: NonAdminBackendUserIdKind::Observer }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer })]
1531    #[case(NonAdminBackendUserIdFilter{ backend_user_id_kind: NonAdminBackendUserIdKind::Metrics }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics })]
1532    #[case(NonAdminBackendUserIdFilter{ backend_user_id_kind: NonAdminBackendUserIdKind::Signing }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing })]
1533    fn backend_user_id_filter_from_non_admin_backend_user_id_filter(
1534        #[case] input: NonAdminBackendUserIdFilter,
1535        #[case] output: BackendUserIdFilter,
1536    ) {
1537        let transform: BackendUserIdFilter = input.into();
1538
1539        assert_eq!(transform, output)
1540    }
1541
1542    #[rstest]
1543    #[case(ExampleUserMapping::Admin{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1544    #[case(ExampleUserMapping::Backup{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1545    #[case(ExampleUserMapping::Observer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1546    #[case(ExampleUserMapping::Metrics{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1547    #[case(ExampleUserMapping::Signer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1548    #[case(ExampleUserMapping::Admin{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin })]
1549    #[case(ExampleUserMapping::Backup{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup })]
1550    #[case(ExampleUserMapping::Observer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer })]
1551    #[case(ExampleUserMapping::Metrics{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics })]
1552    #[case(ExampleUserMapping::Signer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing })]
1553    #[case(ExampleUserMapping::Backup{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1554    #[case(ExampleUserMapping::Observer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1555    #[case(ExampleUserMapping::Metrics{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1556    #[case(ExampleUserMapping::Signer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1557    fn backend_user_ids_matches(
1558        #[case] mapping: ExampleUserMapping,
1559        #[case] filter: BackendUserIdFilter,
1560    ) {
1561        assert_eq!(mapping.backend_user_ids(filter), ["1"])
1562    }
1563
1564    #[test]
1565    fn backend_user_with_passphrase_succeeds() -> TestResult {
1566        let mapping = ExampleUserMapping::Admin { backend_id: 1 };
1567        let passphrase = Passphrase::generate(None);
1568        let creds = mapping.backend_user_with_passphrase("1", passphrase.clone())?;
1569        assert_eq!(creds.user(), "1");
1570        assert_eq!(
1571            creds.passphrase().expose_borrowed(),
1572            passphrase.expose_borrowed()
1573        );
1574
1575        Ok(())
1576    }
1577
1578    #[test]
1579    fn backend_user_with_passphrase_fails() {
1580        let mapping = ExampleUserMapping::Admin { backend_id: 1 };
1581        assert!(
1582            mapping
1583                .backend_user_with_passphrase("2", Passphrase::generate(None))
1584                .is_err()
1585        );
1586    }
1587
1588    #[rstest]
1589    #[case(ExampleUserMapping::Admin{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1590    #[case(ExampleUserMapping::Backup{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1591    #[case(ExampleUserMapping::Observer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1592    #[case(ExampleUserMapping::Metrics{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1593    #[case(ExampleUserMapping::Signer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any })]
1594    #[case(ExampleUserMapping::Admin{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin })]
1595    #[case(ExampleUserMapping::Backup{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup })]
1596    #[case(ExampleUserMapping::Observer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer })]
1597    #[case(ExampleUserMapping::Metrics{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics })]
1598    #[case(ExampleUserMapping::Signer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing })]
1599    #[case(ExampleUserMapping::Backup{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1600    #[case(ExampleUserMapping::Observer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1601    #[case(ExampleUserMapping::Metrics{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1602    #[case(ExampleUserMapping::Signer{ backend_id: 1 }, BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin })]
1603    fn backend_users_with_new_passphrase_applies(
1604        #[case] mapping: ExampleUserMapping,
1605        #[case] filter: BackendUserIdFilter,
1606    ) {
1607        let creds = mapping.backend_users_with_new_passphrase(filter);
1608        assert!(creds.first().is_some_and(|creds| creds.user() == "1"))
1609    }
1610}