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