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