Skip to main content

nethsm/
user.rs

1//! Module for credentials, user IDs and passphrases.
2
3use std::fmt::Display;
4use std::str::FromStr;
5
6use nethsm_sdk_rs::apis::configuration::BasicAuth;
7use serde::{Deserialize, Serialize};
8use signstar_crypto::traits::UserWithPassphrase;
9use strum::AsRefStr;
10
11use crate::{Passphrase, UserRole};
12
13/// An error that may occur when operating on users.
14#[derive(Debug, thiserror::Error)]
15pub enum Error {
16    /// The passphrase for a [`UserId`] is missing.
17    #[error("The passphrase for user {user} is missing")]
18    PassphraseMissing {
19        /// The [`UserId`] for which the passphrase is missing.
20        user: UserId,
21    },
22
23    /// One or more [`NamespaceId`]s are invalid.
24    #[error("Invalid Namespace IDs: {}", namespace_ids.join(", "))]
25    InvalidNamespaceIds {
26        /// The list of invalid Namespace IDs.
27        namespace_ids: Vec<String>,
28    },
29
30    /// One or more [`UserId`]s are invalid.
31    #[error("Invalid User IDs: {}", user_ids.join(", "))]
32    InvalidUserIds {
33        /// A list of strings representing invalid [`UserId`]s.
34        user_ids: Vec<String>,
35    },
36
37    /// The API call does not support users in namespaces
38    #[error("The calling user {0} is in a namespace, which is not supported in this context.")]
39    NamespaceUnsupported(UserId),
40
41    /// A user in one namespace targets a user in another
42    #[error("User {caller} targets {target} which is in a different namespace")]
43    NamespaceTargetMismatch {
44        /// The [`UserId`] of a user that targets a user in another namespace.
45        caller: UserId,
46
47        /// The [`UserId`] of the targeted user.
48        target: UserId,
49    },
50
51    /// A user in a namespace tries to modify a system-wide user
52    #[error("User {caller} targets {target} a system-wide user")]
53    NamespaceSystemWideTarget {
54        /// The [`UserId`] of a user in a namespace that attempts to modify a system-wide user.
55        caller: UserId,
56
57        /// The [`UserId`] of a system-wide user that `caller` attempts to modify.
58        target: UserId,
59    },
60
61    /// A user in Backup or Metrics role is about to be created in a namespace
62    #[error(
63        "User {caller} attempts to create user {target} in role {role} which is not supported in namespaces"
64    )]
65    NamespaceRoleInvalid {
66        /// The [`UserId`] of the user trying to create `target` in `role`.
67        caller: UserId,
68
69        /// The [`UserId`] of the user in a namespace that is attempted to be created by `caller`.
70        target: UserId,
71
72        /// The [`UserRole`] of `target`.
73        role: UserRole,
74    },
75
76    /// A namespaced [`UserId`] has no namespace.
77    #[error("The namespaced User ID has no namespace: {0}")]
78    NamespacedUserIdWithoutNamespace(UserId),
79
80    /// A system-wide [`UserId`] has a namespace
81    #[error("The system-wide User ID has a namespace: {0}")]
82    SystemWideUserIdWithNamespace(UserId),
83}
84
85/// Whether a resource has [namespace] support or not
86///
87/// [namespace]: https://docs.nitrokey.com/nethsm/administration#namespaces
88#[derive(AsRefStr, Clone, Debug, strum::Display, Eq, PartialEq)]
89#[strum(serialize_all = "lowercase")]
90pub enum NamespaceSupport {
91    /// The resource supports namespaces
92    Supported,
93    /// The resource does not support namespaces
94    Unsupported,
95}
96
97/// The ID of a [`NetHsm`][`crate::NetHsm`] [namespace]
98///
99/// [`NamespaceId`]s are used as part of a [`UserId`] or standalone for managing a [namespace] using
100/// [`add_namespace`][`crate::NetHsm::add_namespace`] or
101/// [`delete_namespace`][`crate::NetHsm::delete_namespace`].
102///
103/// [namespace]: https://docs.nitrokey.com/nethsm/administration#namespaces
104#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
105pub struct NamespaceId(String);
106
107impl NamespaceId {
108    /// Creates a new [`NamespaceId`] from owned [`String`]
109    ///
110    /// The provided string must be in the character set `[a-z0-9]`.
111    ///
112    /// # Errors
113    ///
114    /// Returns an [`Error`][`crate::Error`] if
115    /// * the provided string contains an invalid character
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use nethsm::NamespaceId;
121    ///
122    /// # fn main() -> testresult::TestResult {
123    /// // a valid NamespaceId
124    /// assert!(NamespaceId::new("namespace1".to_string()).is_ok());
125    ///
126    /// // an invalid NamespaceId
127    /// assert!(NamespaceId::new("namespace-1".to_string()).is_err());
128    /// # Ok(())
129    /// # }
130    /// ```
131    pub fn new(namespace_id: String) -> Result<Self, Error> {
132        if namespace_id.is_empty()
133            || !namespace_id.chars().all(|char| {
134                char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
135            })
136        {
137            return Err(Error::InvalidNamespaceIds {
138                namespace_ids: vec![namespace_id],
139            });
140        }
141        Ok(Self(namespace_id))
142    }
143}
144
145impl AsRef<str> for NamespaceId {
146    fn as_ref(&self) -> &str {
147        self.0.as_str()
148    }
149}
150
151impl FromStr for NamespaceId {
152    type Err = Error;
153    fn from_str(s: &str) -> Result<Self, Self::Err> {
154        Self::new(s.to_string())
155    }
156}
157
158impl Display for NamespaceId {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        write!(f, "{}", self.0)
161    }
162}
163
164impl TryFrom<&str> for NamespaceId {
165    type Error = Error;
166
167    fn try_from(value: &str) -> Result<Self, Self::Error> {
168        Self::new(value.to_string())
169    }
170}
171
172/// The ID for a [`NetHsm`][`crate::NetHsm`] user
173///
174/// [`UserId`]s are an essential part of the [user management] for a NetHSM.
175/// They come in two types: system-wide and in a namespace.
176///
177/// [`UserId`]s for system-wide users only consist of characters in the set `[a-z0-9]` (e.g.
178/// `user1`) and must be at least one char long.
179///
180/// The [`UserId`]s of users in a namespace consist of characters in the set `[a-z0-9~]` and
181/// contain the name of the namespace (see [`NamespaceId`]) they are in. These [`UserId`]s must be
182/// at least three chars long. The `~` character serves as delimiter between the namespace part and
183/// the user part (e.g. `namespace1~user1`).
184///
185/// [user management]: https://docs.nitrokey.com/nethsm/administration#user-management
186#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
187#[serde(into = "String", try_from = "String")]
188pub enum UserId {
189    /// A system-wide user
190    SystemWide(String),
191    /// A user in a namespace
192    Namespace(NamespaceId, String),
193}
194
195impl UserId {
196    /// Creates a new [`UserId`] from owned [`String`]
197    ///
198    /// The provided string must be in the character set `[a-z0-9~]` and at least one char long. The
199    /// `~` character can not be used as the first character and can only occur once.
200    ///
201    /// # Errors
202    ///
203    /// Returns an [`Error`][`crate::Error`] if
204    /// * the provided string contains an invalid character
205    /// * the `~` character is used as the first character
206    /// * the `~` character is used more than once
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// use nethsm::UserId;
212    ///
213    /// # fn main() -> testresult::TestResult {
214    /// // the UserId of a system-wide user
215    /// assert!(UserId::new("user1".to_string()).is_ok());
216    /// // the UserId of a namespace user
217    /// assert!(UserId::new("namespace1~user1".to_string()).is_ok());
218    ///
219    /// // the input can not contain invalid chars
220    /// assert!(UserId::new("user1X".to_string()).is_err());
221    /// assert!(UserId::new("user;-".to_string()).is_err());
222    ///
223    /// // the '~' character must be surrounded by other characters and only occur once
224    /// assert!(UserId::new("~user1".to_string()).is_err());
225    /// assert!(UserId::new("namespace~user~else".to_string()).is_err());
226    /// # Ok(())
227    /// # }
228    /// ```
229    pub fn new(user_id: String) -> Result<Self, Error> {
230        if let Some((namespace, name)) = user_id.split_once("~") {
231            if namespace.is_empty()
232                || !(namespace.chars().all(|char| {
233                    char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
234                }) && name.chars().all(|char| {
235                    char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
236                }))
237            {
238                return Err(Error::InvalidUserIds {
239                    user_ids: vec![user_id],
240                });
241            }
242            Ok(Self::Namespace(namespace.parse()?, name.to_string()))
243        } else {
244            if user_id.is_empty()
245                || !user_id.chars().all(|char| {
246                    char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
247                })
248            {
249                return Err(Error::InvalidUserIds {
250                    user_ids: vec![user_id],
251                });
252            }
253            Ok(Self::SystemWide(user_id))
254        }
255    }
256
257    /// Returns the namespace of the [`UserId`]
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use nethsm::UserId;
263    ///
264    /// # fn main() -> testresult::TestResult {
265    /// // the UserId of a system-wide user
266    /// assert_eq!(UserId::new("user1".to_string())?.namespace(), None);
267    /// // the UserId of a namespace user
268    /// assert_eq!(
269    ///     UserId::new("namespace1~user1".to_string())?.namespace(),
270    ///     Some(&"namespace1".try_into()?)
271    /// );
272    /// # Ok(())
273    /// # }
274    /// ```
275    pub fn namespace(&self) -> Option<&NamespaceId> {
276        match self {
277            Self::SystemWide(_) => None,
278            Self::Namespace(namespace, _) => Some(namespace),
279        }
280    }
281
282    /// Returns whether the [`UserId`] contains a namespace
283    ///
284    /// # Examples
285    ///
286    /// ```
287    /// use nethsm::UserId;
288    ///
289    /// # fn main() -> testresult::TestResult {
290    /// // the UserId of a system-wide user
291    /// assert_eq!(UserId::new("user1".to_string())?.is_namespaced(), false);
292    /// // the UserId of a namespace user
293    /// assert_eq!(
294    ///     UserId::new("namespace1~user1".to_string())?.is_namespaced(),
295    ///     true
296    /// );
297    /// # Ok(())
298    /// # }
299    /// ```
300    pub fn is_namespaced(&self) -> bool {
301        match self {
302            Self::SystemWide(_) => false,
303            Self::Namespace(_, _) => true,
304        }
305    }
306
307    /// Validates whether the [`UserId`] can be used in a given context
308    ///
309    /// Ensures that [`UserId`] can be used in its context (e.g. calls to system-wide or
310    /// [namespace] resources) by defining [namespace] `support` of the context.
311    /// Additionally ensures the validity of calls to resources targeting other users (provided by
312    /// `target`), which are themselves system-wide or in a [namespace].
313    /// When `role` is provided, the validity of targeting the [`UserRole`] is evaluated.
314    ///
315    /// # Errors
316    ///
317    /// This call returns an
318    /// * [`Error::NamespaceTargetMismatch`] if a user in one namespace tries to target a user in
319    ///   another namespace
320    /// * [`Error::NamespaceRoleInvalid`], if a user in a namespace targets a user in the
321    ///   [`Backup`][`UserRole::Backup`] or [`Metrics`][`UserRole::Metrics`] [role], or if a user
322    ///   not in a namespace targets a namespaced user in the [`Backup`][`UserRole::Backup`] or
323    ///   [`Metrics`][`UserRole::Metrics`] [role].
324    /// * [`Error::NamespaceSystemWideTarget`], if a user in a [namespace] targets a system-wide
325    ///   user
326    ///
327    /// [namespace]: https://docs.nitrokey.com/nethsm/administration#namespaces
328    /// [role]: https://docs.nitrokey.com/nethsm/administration#roles
329    pub fn validate_namespace_access(
330        &self,
331        support: NamespaceSupport,
332        target: Option<&UserId>,
333        role: Option<&UserRole>,
334    ) -> Result<(), Error> {
335        // the caller is in a namespace
336        if let Some(caller_namespace) = self.namespace() {
337            // the caller context does not support namespaces
338            if support == NamespaceSupport::Unsupported {
339                return Err(Error::NamespaceUnsupported(self.to_owned()));
340            }
341
342            // there is a target user
343            if let Some(target) = target {
344                // the target user is in a namespace
345                if let Some(target_namespace) = target.namespace() {
346                    // the caller's and the target's namespaces are not the same
347                    if caller_namespace != target_namespace {
348                        return Err(Error::NamespaceTargetMismatch {
349                            caller: self.to_owned(),
350                            target: target.to_owned(),
351                        });
352                    }
353
354                    // the action towards the targeted user provides a role
355                    if let Some(role) = role {
356                        // the targeted user's role is not supported
357                        if role == &UserRole::Metrics || role == &UserRole::Backup {
358                            return Err(Error::NamespaceRoleInvalid {
359                                caller: self.to_owned(),
360                                target: target.to_owned(),
361                                role: role.to_owned(),
362                            });
363                        }
364                    }
365                } else {
366                    // the caller is in a namespace and the target user is not
367                    return Err(Error::NamespaceSystemWideTarget {
368                        caller: self.to_owned(),
369                        target: target.to_owned(),
370                    });
371                }
372            }
373        // there is a target user
374        } else if let Some(target) = target {
375            // there is a target role
376            if let Some(role) = role {
377                // the targeted user's role is not supported
378                if (role == &UserRole::Metrics || role == &UserRole::Backup)
379                    && target.is_namespaced()
380                {
381                    return Err(Error::NamespaceRoleInvalid {
382                        caller: self.to_owned(),
383                        target: target.to_owned(),
384                        role: role.to_owned(),
385                    });
386                }
387            }
388        }
389        Ok(())
390    }
391}
392
393impl FromStr for UserId {
394    type Err = Error;
395    fn from_str(s: &str) -> Result<Self, Self::Err> {
396        Self::new(s.to_string())
397    }
398}
399
400impl Display for UserId {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        match self {
403            UserId::SystemWide(user_id) => write!(f, "{user_id}"),
404            UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
405        }
406    }
407}
408
409impl From<UserId> for String {
410    fn from(value: UserId) -> Self {
411        value.to_string()
412    }
413}
414
415impl TryFrom<&str> for UserId {
416    type Error = Error;
417
418    fn try_from(value: &str) -> Result<Self, Self::Error> {
419        Self::new(value.to_string())
420    }
421}
422
423impl TryFrom<&String> for UserId {
424    type Error = Error;
425
426    fn try_from(value: &String) -> Result<Self, Self::Error> {
427        Self::new(value.to_string())
428    }
429}
430
431impl TryFrom<String> for UserId {
432    type Error = Error;
433
434    fn try_from(value: String) -> Result<Self, Self::Error> {
435        Self::new(value)
436    }
437}
438
439/// A guaranteed to be system-wide [`NetHsm`][`crate::NetHsm`] user.
440#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
441#[serde(into = "String", try_from = "String")]
442pub struct SystemWideUserId(UserId);
443
444impl SystemWideUserId {
445    /// Creates a new [`SystemWideUserId`] from an owned string.
446    ///
447    /// # Errors
448    ///
449    /// Returns an error, if the provided `user_id` is not a valid [`UserId`] or contains a
450    /// namespace.
451    ///
452    /// # Examples
453    ///
454    /// ```
455    /// use nethsm::SystemWideUserId;
456    ///
457    /// # fn main() -> testresult::TestResult {
458    /// SystemWideUserId::new("user1".to_string())?;
459    ///
460    /// // this fails because the User ID contains a namespace
461    /// assert!(SystemWideUserId::new("ns1~user1".to_string()).is_err());
462    /// # Ok(())
463    /// # }
464    /// ```
465    pub fn new(user_id: String) -> Result<Self, Error> {
466        let user_id = UserId::new(user_id)?;
467
468        if user_id.is_namespaced() {
469            return Err(Error::SystemWideUserIdWithNamespace(user_id));
470        }
471
472        Ok(Self(user_id))
473    }
474}
475
476impl AsRef<UserId> for SystemWideUserId {
477    fn as_ref(&self) -> &UserId {
478        &self.0
479    }
480}
481
482impl Display for SystemWideUserId {
483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484        self.0.fmt(f)
485    }
486}
487
488impl FromStr for SystemWideUserId {
489    type Err = Error;
490    fn from_str(s: &str) -> Result<Self, Self::Err> {
491        Self::new(s.to_string())
492    }
493}
494
495impl From<SystemWideUserId> for String {
496    fn from(value: SystemWideUserId) -> Self {
497        value.to_string()
498    }
499}
500
501impl From<SystemWideUserId> for UserId {
502    fn from(value: SystemWideUserId) -> Self {
503        value.0
504    }
505}
506
507impl TryFrom<String> for SystemWideUserId {
508    type Error = Error;
509
510    fn try_from(value: String) -> Result<Self, Self::Error> {
511        Self::new(value)
512    }
513}
514
515/// A guaranteed to be namespaced [`NetHsm`][`crate::NetHsm`] user.
516#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
517#[serde(into = "String", try_from = "String")]
518pub struct NamespacedUserId(UserId);
519
520impl NamespacedUserId {
521    /// Creates a new [`NamespacedUserId`] from an owned string.
522    ///
523    /// # Errors
524    ///
525    /// Returns an error, if the provided `user_id` is not a valid [`UserId`] or does not contain a
526    /// namespace.
527    ///
528    /// # Examples
529    ///
530    /// ```
531    /// use nethsm::NamespacedUserId;
532    ///
533    /// # fn main() -> testresult::TestResult {
534    /// NamespacedUserId::new("ns1~user1".to_string())?;
535    ///
536    /// // this fails because the User ID does not contain a namespace
537    /// assert!(NamespacedUserId::new("user1".to_string()).is_err());
538    /// # Ok(())
539    /// # }
540    /// ```
541    pub fn new(user_id: String) -> Result<Self, Error> {
542        let user_id = UserId::new(user_id)?;
543
544        if !user_id.is_namespaced() {
545            return Err(Error::NamespacedUserIdWithoutNamespace(user_id));
546        }
547
548        Ok(Self(user_id))
549    }
550}
551
552impl AsRef<UserId> for NamespacedUserId {
553    fn as_ref(&self) -> &UserId {
554        &self.0
555    }
556}
557
558impl Display for NamespacedUserId {
559    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560        self.0.fmt(f)
561    }
562}
563
564impl FromStr for NamespacedUserId {
565    type Err = Error;
566    fn from_str(s: &str) -> Result<Self, Self::Err> {
567        Self::new(s.to_string())
568    }
569}
570
571impl From<NamespacedUserId> for String {
572    fn from(value: NamespacedUserId) -> Self {
573        value.to_string()
574    }
575}
576
577impl From<NamespacedUserId> for UserId {
578    fn from(value: NamespacedUserId) -> Self {
579        value.0
580    }
581}
582
583impl TryFrom<String> for NamespacedUserId {
584    type Error = Error;
585
586    fn try_from(value: String) -> Result<Self, Self::Error> {
587        Self::new(value)
588    }
589}
590
591/// Credentials for a [`NetHsm`][`crate::NetHsm`].
592///
593/// Tracks a [`UserId`] and an accompanying [`Passphrase`].
594/// Different from [`Credentials`], this type _requires_ a [`Passphrase`].
595#[derive(Clone, Debug, Deserialize, Serialize)]
596pub struct FullCredentials {
597    /// The user name.
598    pub name: UserId,
599
600    /// The passphrase for `name`.
601    pub passphrase: Passphrase,
602}
603
604impl FullCredentials {
605    /// Creates a new [`FullCredentials`].
606    ///
607    /// # Examples
608    ///
609    /// ```
610    /// use nethsm::FullCredentials;
611    ///
612    /// # fn main() -> testresult::TestResult {
613    /// let creds = FullCredentials::new("operator".parse()?, "passphrase".parse()?);
614    /// # eprintln!("{creds:?}");
615    /// # Ok(())
616    /// # }
617    /// ```
618    pub fn new(name: UserId, passphrase: Passphrase) -> Self {
619        Self { name, passphrase }
620    }
621}
622
623impl UserWithPassphrase for FullCredentials {
624    fn user(&self) -> String {
625        self.name.to_string()
626    }
627
628    fn passphrase(&self) -> &Passphrase {
629        &self.passphrase
630    }
631}
632
633impl From<FullCredentials> for BasicAuth {
634    fn from(value: FullCredentials) -> Self {
635        Self::from(&value)
636    }
637}
638
639impl From<&FullCredentials> for BasicAuth {
640    fn from(value: &FullCredentials) -> Self {
641        (
642            value.name.to_string(),
643            Some(value.passphrase.expose_owned()),
644        )
645    }
646}
647
648impl TryFrom<&Credentials> for FullCredentials {
649    type Error = Error;
650
651    fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
652        let creds = value.clone();
653        FullCredentials::try_from(creds)
654    }
655}
656
657impl TryFrom<Credentials> for FullCredentials {
658    type Error = Error;
659
660    fn try_from(value: Credentials) -> Result<Self, Self::Error> {
661        let Some(passphrase) = value.passphrase else {
662            return Err(Error::PassphraseMissing {
663                user: value.user_id,
664            });
665        };
666
667        Ok(FullCredentials {
668            name: value.user_id,
669            passphrase,
670        })
671    }
672}
673
674/// Credentials for a [`NetHsm`][`crate::NetHsm`]
675///
676/// Holds a user ID and an accompanying [`Passphrase`].
677#[derive(Clone, Debug)]
678pub struct Credentials {
679    /// The user ID.
680    pub user_id: UserId,
681
682    /// The optional passphrase for `user_id`.
683    pub passphrase: Option<Passphrase>,
684}
685
686impl Credentials {
687    /// Creates a new [`Credentials`]
688    ///
689    /// # Examples
690    ///
691    /// ```
692    /// use nethsm::{Credentials, Passphrase};
693    ///
694    /// # fn main() -> testresult::TestResult {
695    /// let creds = Credentials::new(
696    ///     "operator".parse()?,
697    ///     Some(Passphrase::new("passphrase".to_string())),
698    /// );
699    /// # Ok(())
700    /// # }
701    /// ```
702    pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
703        Self {
704            user_id,
705            passphrase,
706        }
707    }
708}
709
710impl Display for Credentials {
711    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
712        write!(f, "{}", self.user_id)?;
713        if let Some(passphrase) = self.passphrase.as_ref() {
714            write!(f, " ({passphrase})")?;
715        }
716        Ok(())
717    }
718}
719
720impl From<Credentials> for BasicAuth {
721    fn from(value: Credentials) -> Self {
722        (
723            value.user_id.to_string(),
724            value.passphrase.map(|x| x.expose_owned()),
725        )
726    }
727}
728
729impl From<&Credentials> for BasicAuth {
730    fn from(value: &Credentials) -> Self {
731        (
732            value.user_id.to_string(),
733            value.passphrase.as_ref().map(|x| x.expose_owned()),
734        )
735    }
736}
737
738impl From<&FullCredentials> for Credentials {
739    fn from(value: &FullCredentials) -> Self {
740        let creds = value.clone();
741        Self::from(creds)
742    }
743}
744
745impl From<FullCredentials> for Credentials {
746    fn from(value: FullCredentials) -> Self {
747        Credentials::new(value.name, Some(value.passphrase))
748    }
749}
750
751impl TryFrom<Box<dyn UserWithPassphrase>> for Credentials {
752    type Error = crate::Error;
753
754    fn try_from(value: Box<dyn UserWithPassphrase>) -> Result<Self, Self::Error> {
755        Ok(Self::new(
756            UserId::try_from(value.user())?,
757            Some(value.passphrase().clone()),
758        ))
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use rstest::rstest;
765    use testresult::TestResult;
766
767    use super::*;
768
769    #[rstest]
770    #[case(Credentials::new(UserId::new("user".to_string())?, Some(Passphrase::new("a-secret-passphrase".to_string()))), "user ([REDACTED])")]
771    #[case(Credentials::new(UserId::new("user".to_string())?, None), "user")]
772    fn credentials_display(#[case] credentials: Credentials, #[case] expected: &str) -> TestResult {
773        assert_eq!(credentials.to_string(), expected);
774        Ok(())
775    }
776
777    #[rstest]
778    #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
779    #[case("f", Some(UserId::SystemWide("f".to_string())))]
780    #[case("1", Some(UserId::SystemWide("1".to_string())))]
781    #[case("foo;-", None)]
782    #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
783    #[case("FOO", None)]
784    #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
785    #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
786    #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
787    #[case("~bar", None)]
788    #[case("", None)]
789    #[case("foo;-~bar\\", None)]
790    #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
791    #[case("foo~bar~baz", None)]
792    #[case("FOO~bar", None)]
793    #[case("foo~BAR", None)]
794    fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
795        if let Some(user_id) = user_id {
796            assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
797        } else {
798            assert!(UserId::from_str(input).is_err());
799        }
800
801        Ok(())
802    }
803
804    #[rstest]
805    #[case(UserId::SystemWide("user".to_string()), None)]
806    #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some(NamespaceId("namespace".to_string())))]
807    fn user_id_namespace(#[case] input: UserId, #[case] result: Option<NamespaceId>) -> TestResult {
808        assert_eq!(input.namespace(), result.as_ref());
809        Ok(())
810    }
811
812    #[rstest]
813    #[case(UserId::SystemWide("user".to_string()), false)]
814    #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
815    fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
816        assert_eq!(input.is_namespaced(), result);
817        Ok(())
818    }
819
820    #[rstest]
821    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
822    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
823    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
824    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
825    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
826    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
827    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
828    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
829    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
830    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
831    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
832    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
833    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
834    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
835    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
836    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
837    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
838    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
839    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
840    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
841    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
842    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
843    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
844    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
845    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
846    fn validate_namespace_access(
847        #[case] caller: UserId,
848        #[case] namespace_support: NamespaceSupport,
849        #[case] target: Option<UserId>,
850        #[case] role: Option<UserRole>,
851        #[case] result: Option<()>,
852    ) -> TestResult {
853        if result.is_some() {
854            assert!(
855                caller
856                    .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
857                    .is_ok()
858            );
859        } else {
860            assert!(
861                caller
862                    .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
863                    .is_err()
864            )
865        }
866        Ok(())
867    }
868
869    /// Ensures that [`SystemWideUserId::new`] fails on User IDs with a namespace.
870    #[test]
871    fn system_wide_user_id_new_fails_on_user_id_with_namespace() -> TestResult {
872        match SystemWideUserId::new("ns1~test".to_string()) {
873            Err(Error::SystemWideUserIdWithNamespace(_)) => Ok(()),
874            Err(error) => panic!(
875                "Expected to fail with a Error::SystemWideUserIdWithNamespace but got a different error instead:\n{error}"
876            ),
877            Ok(user_id) => panic!(
878                "Expected to fail with a Error::SystemWideUserIdWithNamespace but succeeded instead:\n{user_id}"
879            ),
880        }
881    }
882
883    /// Ensures that [`SystemWideUserId::new`] fails on invalid User IDs.
884    #[test]
885    fn system_wide_user_id_new_fails_on_invalid_user_id() -> TestResult {
886        match SystemWideUserId::new("test[]".to_string()) {
887            Err(Error::InvalidUserIds { .. }) => Ok(()),
888            Err(error) => panic!(
889                "Expected to fail with a Error::InvalidUserIds but got a different error instead:\n{error}"
890            ),
891            Ok(user_id) => panic!(
892                "Expected to fail with a Error::InvalidUserIds but succeeded instead:\n{user_id}"
893            ),
894        }
895    }
896
897    /// Ensures that [`SystemWideUserId::from_str`] succeeds on valid User IDs.
898    #[test]
899    fn system_wide_user_id_from_str_succeeds() -> TestResult {
900        assert_eq!(SystemWideUserId::from_str("test")?.to_string(), "test");
901        Ok(())
902    }
903
904    /// Ensures that [`NamespacedUserId::new`] fails on User IDs without a namespace.
905    #[test]
906    fn namespaced_user_id_new_fails_on_user_id_without_namespace() -> TestResult {
907        match NamespacedUserId::new("test".to_string()) {
908            Err(Error::NamespacedUserIdWithoutNamespace(_)) => Ok(()),
909            Err(error) => panic!(
910                "Expected to fail with a Error::NamespacedUserIdWithoutNamespace but got a different error instead:\n{error}"
911            ),
912            Ok(user_id) => panic!(
913                "Expected to fail with a Error::NamespacedUserIdWithoutNamespace but succeeded instead:\n{user_id}"
914            ),
915        }
916    }
917
918    /// Ensures that [`NamespacedUserId::new`] fails on invalid User IDs.
919    #[test]
920    fn namespaced_user_id_new_fails_on_invalid_user_id() -> TestResult {
921        match NamespacedUserId::new("test[]".to_string()) {
922            Err(Error::InvalidUserIds { .. }) => Ok(()),
923            Err(error) => panic!(
924                "Expected to fail with a Error::InvalidUserIds but got a different error instead:\n{error}"
925            ),
926            Ok(user_id) => panic!(
927                "Expected to fail with a Error::InvalidUserIds but succeeded instead:\n{user_id}"
928            ),
929        }
930    }
931
932    /// Ensures that [`NamespacedUserId::from_str`] succeeds on valid User IDs.
933    #[test]
934    fn namespaced_user_id_from_str_succeeds() -> TestResult {
935        assert_eq!(
936            NamespacedUserId::from_str("ns1~test")?.to_string(),
937            "ns1~test"
938        );
939        Ok(())
940    }
941}