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