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 secrecy::{ExposeSecret, SecretString};
8use serde::{Deserialize, Serialize};
9
10use crate::UserRole;
11
12/// An error that may occur when operating on users.
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15    /// Unable to convert string slice to Passphrase
16    #[error("Unable to convert string to passphrase")]
17    Passphrase,
18
19    /// The passphrase for a [`UserId`] is missing.
20    #[error("The passphrase for user {user} is missing")]
21    PassphraseMissing {
22        /// The [`UserId`] for which the passphrase is missing.
23        user: UserId,
24    },
25
26    /// Invalid Namespace ID
27    #[error("Invalid Namespace ID: {0}")]
28    InvalidNamespaceId(String),
29
30    /// Invalid User ID
31    #[error("Invalid User ID: {0}")]
32    InvalidUserId(String),
33
34    /// The API call does not support users in namespaces
35    #[error("The calling user {0} is in a namespace, which is not supported in this context.")]
36    NamespaceUnsupported(UserId),
37
38    /// A user in one namespace targets a user in another
39    #[error("User {caller} targets {target} which is in a different namespace")]
40    NamespaceTargetMismatch {
41        /// The [`UserId`] of a user that targets a user in another namespace.
42        caller: UserId,
43
44        /// The [`UserId`] of the targeted user.
45        target: UserId,
46    },
47
48    /// A user in a namespace tries to modify a system-wide user
49    #[error("User {caller} targets {target} a system-wide user")]
50    NamespaceSystemWideTarget {
51        /// The [`UserId`] of a user in a namespace that attempts to modify a system-wide user.
52        caller: UserId,
53
54        /// The [`UserId`] of a system-wide user that `caller` attempts to modify.
55        target: UserId,
56    },
57
58    /// A user in Backup or Metrics role is about to be created in a namespace
59    #[error(
60        "User {caller} attempts to create user {target} in role {role} which is not supported in namespaces"
61    )]
62    NamespaceRoleInvalid {
63        /// The [`UserId`] of the user trying to create `target` in `role`.
64        caller: UserId,
65
66        /// The [`UserId`] of the user in a namespace that is attempted to be created by `caller`.
67        target: UserId,
68
69        /// The [`UserRole`] of `target`.
70        role: UserRole,
71    },
72}
73
74/// Whether a resource has [namespace] support or not
75///
76/// [namespace]: https://docs.nitrokey.com/nethsm/administration#namespaces
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum NamespaceSupport {
79    /// The resource supports namespaces
80    Supported,
81    /// The resource does not support namespaces
82    Unsupported,
83}
84
85/// The ID of a [`NetHsm`][`crate::NetHsm`] [namespace]
86///
87/// [`NamespaceId`]s are used as part of a [`UserId`] or standalone for managing a [namespace] using
88/// [`add_namespace`][`crate::NetHsm::add_namespace`] or
89/// [`delete_namespace`][`crate::NetHsm::delete_namespace`].
90///
91/// [namespace]: https://docs.nitrokey.com/nethsm/administration#namespaces
92#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
93pub struct NamespaceId(String);
94
95impl NamespaceId {
96    /// Creates a new [`NamespaceId`] from owned [`String`]
97    ///
98    /// The provided string must be in the character set `[a-z0-9]`.
99    ///
100    /// # Errors
101    ///
102    /// Returns an [`Error`][`crate::Error`] if
103    /// * the provided string contains an invalid character
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use nethsm::NamespaceId;
109    ///
110    /// # fn main() -> testresult::TestResult {
111    /// // a valid NamespaceId
112    /// assert!(NamespaceId::new("namespace1".to_string()).is_ok());
113    ///
114    /// // an invalid NamespaceId
115    /// assert!(NamespaceId::new("namespace-1".to_string()).is_err());
116    /// # Ok(())
117    /// # }
118    /// ```
119    pub fn new(namespace_id: String) -> Result<Self, Error> {
120        if namespace_id.is_empty()
121            || !namespace_id.chars().all(|char| {
122                char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
123            })
124        {
125            return Err(Error::InvalidNamespaceId(namespace_id));
126        }
127        Ok(Self(namespace_id))
128    }
129}
130
131impl AsRef<str> for NamespaceId {
132    fn as_ref(&self) -> &str {
133        self.0.as_str()
134    }
135}
136
137impl FromStr for NamespaceId {
138    type Err = Error;
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        Self::new(s.to_string())
141    }
142}
143
144impl Display for NamespaceId {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        write!(f, "{}", self.0)
147    }
148}
149
150impl TryFrom<&str> for NamespaceId {
151    type Error = Error;
152
153    fn try_from(value: &str) -> Result<Self, Self::Error> {
154        Self::new(value.to_string())
155    }
156}
157
158/// The ID for a [`NetHsm`][`crate::NetHsm`] user
159///
160/// [`UserId`]s are an essential part of the [user management] for a NetHSM.
161/// They come in two types: system-wide and in a namespace.
162///
163/// [`UserId`]s for system-wide users only consist of characters in the set `[a-z0-9]` (e.g.
164/// `user1`) and must be at least one char long.
165///
166/// The [`UserId`]s of users in a namespace consist of characters in the set `[a-z0-9~]` and
167/// contain the name of the namespace (see [`NamespaceId`]) they are in. These [`UserId`]s must be
168/// at least three chars long. The `~` character serves as delimiter between the namespace part and
169/// the user part (e.g. `namespace1~user1`).
170///
171/// [user management]: https://docs.nitrokey.com/nethsm/administration#user-management
172#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
173#[serde(into = "String", try_from = "String")]
174pub enum UserId {
175    /// A system-wide user
176    SystemWide(String),
177    /// A user in a namespace
178    Namespace(NamespaceId, String),
179}
180
181impl UserId {
182    /// Creates a new [`UserId`] from owned [`String`]
183    ///
184    /// The provided string must be in the character set `[a-z0-9~]` and at least one char long. The
185    /// `~` character can not be used as the first character and can only occur once.
186    ///
187    /// # Errors
188    ///
189    /// Returns an [`Error`][`crate::Error`] if
190    /// * the provided string contains an invalid character
191    /// * the `~` character is used as the first character
192    /// * the `~` character is used more than once
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use nethsm::UserId;
198    ///
199    /// # fn main() -> testresult::TestResult {
200    /// // the UserId of a system-wide user
201    /// assert!(UserId::new("user1".to_string()).is_ok());
202    /// // the UserId of a namespace user
203    /// assert!(UserId::new("namespace1~user1".to_string()).is_ok());
204    ///
205    /// // the input can not contain invalid chars
206    /// assert!(UserId::new("user1X".to_string()).is_err());
207    /// assert!(UserId::new("user;-".to_string()).is_err());
208    ///
209    /// // the '~' character must be surrounded by other characters and only occur once
210    /// assert!(UserId::new("~user1".to_string()).is_err());
211    /// assert!(UserId::new("namespace~user~else".to_string()).is_err());
212    /// # Ok(())
213    /// # }
214    /// ```
215    pub fn new(user_id: String) -> Result<Self, Error> {
216        if let Some((namespace, name)) = user_id.split_once("~") {
217            if namespace.is_empty()
218                || !(namespace.chars().all(|char| {
219                    char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
220                }) && name.chars().all(|char| {
221                    char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
222                }))
223            {
224                return Err(Error::InvalidUserId(user_id));
225            }
226            Ok(Self::Namespace(namespace.parse()?, name.to_string()))
227        } else {
228            if user_id.is_empty()
229                || !user_id.chars().all(|char| {
230                    char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
231                })
232            {
233                return Err(Error::InvalidUserId(user_id));
234            }
235            Ok(Self::SystemWide(user_id))
236        }
237    }
238
239    /// Returns the namespace of the [`UserId`]
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use nethsm::UserId;
245    ///
246    /// # fn main() -> testresult::TestResult {
247    /// // the UserId of a system-wide user
248    /// assert_eq!(UserId::new("user1".to_string())?.namespace(), None);
249    /// // the UserId of a namespace user
250    /// assert_eq!(
251    ///     UserId::new("namespace1~user1".to_string())?.namespace(),
252    ///     Some("namespace1".to_string())
253    /// );
254    /// # Ok(())
255    /// # }
256    /// ```
257    pub fn namespace(&self) -> Option<String> {
258        match self {
259            Self::SystemWide(_) => None,
260            Self::Namespace(namespace, _) => Some(namespace.to_string()),
261        }
262    }
263
264    /// Returns whether the [`UserId`] contains a namespace
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use nethsm::UserId;
270    ///
271    /// # fn main() -> testresult::TestResult {
272    /// // the UserId of a system-wide user
273    /// assert_eq!(UserId::new("user1".to_string())?.is_namespaced(), false);
274    /// // the UserId of a namespace user
275    /// assert_eq!(
276    ///     UserId::new("namespace1~user1".to_string())?.is_namespaced(),
277    ///     true
278    /// );
279    /// # Ok(())
280    /// # }
281    /// ```
282    pub fn is_namespaced(&self) -> bool {
283        match self {
284            Self::SystemWide(_) => false,
285            Self::Namespace(_, _) => true,
286        }
287    }
288
289    /// Validates whether the [`UserId`] can be used in a given context
290    ///
291    /// Ensures that [`UserId`] can be used in its context (e.g. calls to system-wide or
292    /// [namespace] resources) by defining [namespace] `support` of the context.
293    /// Additionally ensures the validity of calls to resources targeting other users (provided by
294    /// `target`), which are themselves system-wide or in a [namespace].
295    /// When `role` is provided, the validity of targeting the [`UserRole`] is evaluated.
296    ///
297    /// # Errors
298    ///
299    /// This call returns an
300    /// * [`Error::NamespaceTargetMismatch`] if a user in one namespace tries to target a user in
301    ///   another namespace
302    /// * [`Error::NamespaceRoleInvalid`], if a user in a namespace targets a user in the
303    ///   [`Backup`][`UserRole::Backup`] or [`Metrics`][`UserRole::Metrics`] [role], or if a user
304    ///   not in a namespace targets a namespaced user in the [`Backup`][`UserRole::Backup`] or
305    ///   [`Metrics`][`UserRole::Metrics`] [role].
306    /// * [`Error::NamespaceSystemWideTarget`], if a user in a [namespace] targets a system-wide
307    ///   user
308    ///
309    /// [namespace]: https://docs.nitrokey.com/nethsm/administration#namespaces
310    /// [role]: https://docs.nitrokey.com/nethsm/administration#roles
311    pub fn validate_namespace_access(
312        &self,
313        support: NamespaceSupport,
314        target: Option<&UserId>,
315        role: Option<&UserRole>,
316    ) -> Result<(), Error> {
317        // the caller is in a namespace
318        if let Some(caller_namespace) = self.namespace() {
319            // the caller context does not support namespaces
320            if support == NamespaceSupport::Unsupported {
321                return Err(Error::NamespaceUnsupported(self.to_owned()));
322            }
323
324            // there is a target user
325            if let Some(target) = target {
326                // the target user is in a namespace
327                if let Some(target_namespace) = target.namespace() {
328                    // the caller's and the target's namespaces are not the same
329                    if caller_namespace != target_namespace {
330                        return Err(Error::NamespaceTargetMismatch {
331                            caller: self.to_owned(),
332                            target: target.to_owned(),
333                        });
334                    }
335
336                    // the action towards the targeted user provides a role
337                    if let Some(role) = role {
338                        // the targeted user's role is not supported
339                        if role == &UserRole::Metrics || role == &UserRole::Backup {
340                            return Err(Error::NamespaceRoleInvalid {
341                                caller: self.to_owned(),
342                                target: target.to_owned(),
343                                role: role.to_owned(),
344                            });
345                        }
346                    }
347                } else {
348                    // the caller is in a namespace and the target user is not
349                    return Err(Error::NamespaceSystemWideTarget {
350                        caller: self.to_owned(),
351                        target: target.to_owned(),
352                    });
353                }
354            }
355        // there is a target user
356        } else if let Some(target) = target {
357            // there is a target role
358            if let Some(role) = role {
359                // the targeted user's role is not supported
360                if (role == &UserRole::Metrics || role == &UserRole::Backup)
361                    && target.is_namespaced()
362                {
363                    return Err(Error::NamespaceRoleInvalid {
364                        caller: self.to_owned(),
365                        target: target.to_owned(),
366                        role: role.to_owned(),
367                    });
368                }
369            }
370        }
371        Ok(())
372    }
373}
374
375impl FromStr for UserId {
376    type Err = Error;
377    fn from_str(s: &str) -> Result<Self, Self::Err> {
378        Self::new(s.to_string())
379    }
380}
381
382impl Display for UserId {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        match self {
385            UserId::SystemWide(user_id) => write!(f, "{user_id}"),
386            UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
387        }
388    }
389}
390
391impl From<UserId> for String {
392    fn from(value: UserId) -> Self {
393        value.to_string()
394    }
395}
396
397impl TryFrom<&str> for UserId {
398    type Error = Error;
399
400    fn try_from(value: &str) -> Result<Self, Self::Error> {
401        Self::new(value.to_string())
402    }
403}
404
405impl TryFrom<&String> for UserId {
406    type Error = Error;
407
408    fn try_from(value: &String) -> Result<Self, Self::Error> {
409        Self::new(value.to_string())
410    }
411}
412
413impl TryFrom<String> for UserId {
414    type Error = Error;
415
416    fn try_from(value: String) -> Result<Self, Self::Error> {
417        Self::new(value)
418    }
419}
420
421/// Credentials for a [`NetHsm`][`crate::NetHsm`].
422///
423/// Tracks a [`UserId`] and an accompanying [`Passphrase`].
424/// Different from [`Credentials`], this type _requires_ a [`Passphrase`].
425#[derive(Clone, Debug, Deserialize, Serialize)]
426pub struct FullCredentials {
427    /// The user name.
428    pub name: UserId,
429
430    /// The passphrase for `name`.
431    pub passphrase: Passphrase,
432}
433
434impl FullCredentials {
435    /// Creates a new [`FullCredentials`].
436    ///
437    /// # Examples
438    ///
439    /// ```
440    /// use nethsm::FullCredentials;
441    ///
442    /// # fn main() -> testresult::TestResult {
443    /// let creds = FullCredentials::new("operator".parse()?, "passphrase".parse()?);
444    /// # eprintln!("{creds:?}");
445    /// # Ok(())
446    /// # }
447    /// ```
448    pub fn new(name: UserId, passphrase: Passphrase) -> Self {
449        Self { name, passphrase }
450    }
451}
452
453impl From<FullCredentials> for BasicAuth {
454    fn from(value: FullCredentials) -> Self {
455        Self::from(&value)
456    }
457}
458
459impl From<&FullCredentials> for BasicAuth {
460    fn from(value: &FullCredentials) -> Self {
461        (
462            value.name.to_string(),
463            Some(value.passphrase.expose_owned()),
464        )
465    }
466}
467
468impl TryFrom<&Credentials> for FullCredentials {
469    type Error = Error;
470
471    fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
472        let creds = value.clone();
473        FullCredentials::try_from(creds)
474    }
475}
476
477impl TryFrom<Credentials> for FullCredentials {
478    type Error = Error;
479
480    fn try_from(value: Credentials) -> Result<Self, Self::Error> {
481        let Some(passphrase) = value.passphrase else {
482            return Err(Error::PassphraseMissing {
483                user: value.user_id,
484            });
485        };
486
487        Ok(FullCredentials {
488            name: value.user_id,
489            passphrase,
490        })
491    }
492}
493
494/// Credentials for a [`NetHsm`][`crate::NetHsm`]
495///
496/// Holds a user ID and an accompanying [`Passphrase`].
497#[derive(Clone, Debug)]
498pub struct Credentials {
499    /// The user ID.
500    pub user_id: UserId,
501
502    /// The optional passphrase for `user_id`.
503    pub passphrase: Option<Passphrase>,
504}
505
506impl Credentials {
507    /// Creates a new [`Credentials`]
508    ///
509    /// # Examples
510    ///
511    /// ```
512    /// use nethsm::{Credentials, Passphrase};
513    ///
514    /// # fn main() -> testresult::TestResult {
515    /// let creds = Credentials::new(
516    ///     "operator".parse()?,
517    ///     Some(Passphrase::new("passphrase".to_string())),
518    /// );
519    /// # Ok(())
520    /// # }
521    /// ```
522    pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
523        Self {
524            user_id,
525            passphrase,
526        }
527    }
528}
529
530impl From<Credentials> for BasicAuth {
531    fn from(value: Credentials) -> Self {
532        (
533            value.user_id.to_string(),
534            value.passphrase.map(|x| x.expose_owned()),
535        )
536    }
537}
538
539impl From<&Credentials> for BasicAuth {
540    fn from(value: &Credentials) -> Self {
541        (
542            value.user_id.to_string(),
543            value.passphrase.as_ref().map(|x| x.expose_owned()),
544        )
545    }
546}
547
548impl From<&FullCredentials> for Credentials {
549    fn from(value: &FullCredentials) -> Self {
550        let creds = value.clone();
551        Self::from(creds)
552    }
553}
554
555impl From<FullCredentials> for Credentials {
556    fn from(value: FullCredentials) -> Self {
557        Credentials::new(value.name, Some(value.passphrase))
558    }
559}
560
561/// A secret passphrase
562///
563/// The passphrase is held by a [`SecretString`], which guarantees zeroing of memory on
564/// destruct.
565#[derive(Clone, Debug, Default, Deserialize)]
566pub struct Passphrase(SecretString);
567
568impl Passphrase {
569    /// Creates a new [`Passphrase`] from owned [`String`]
570    ///
571    /// # Examples
572    /// ```
573    /// use nethsm::Passphrase;
574    ///
575    /// let passphrase = Passphrase::new("passphrase".to_string());
576    /// ```
577    pub fn new(passphrase: String) -> Self {
578        Self(SecretString::new(passphrase.into()))
579    }
580
581    /// Exposes the secret passphrase as owned [`String`]
582    ///
583    /// This is a convenience function, as much of [`nethsm_sdk_rs`] exclusively deals with owned
584    /// strings.
585    pub fn expose_owned(&self) -> String {
586        self.0.expose_secret().to_owned()
587    }
588
589    /// Exposes the secret passphrase as borrowed [`str`]
590    pub fn expose_borrowed(&self) -> &str {
591        self.0.expose_secret()
592    }
593}
594
595impl FromStr for Passphrase {
596    type Err = Error;
597
598    fn from_str(s: &str) -> Result<Self, Self::Err> {
599        Ok(Self(SecretString::from(s.to_string())))
600    }
601}
602
603impl Serialize for Passphrase {
604    /// Serializes a [`Passphrase`].
605    ///
606    /// # Warning
607    ///
608    /// This may be used to write a passphrase to file!
609    /// Take precautions so that passphrases can not leak to the environment.
610    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
611    where
612        S: serde::Serializer,
613    {
614        self.0.expose_secret().serialize(serializer)
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use rstest::rstest;
621    use testresult::TestResult;
622
623    use super::*;
624
625    #[rstest]
626    #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
627    #[case("f", Some(UserId::SystemWide("f".to_string())))]
628    #[case("1", Some(UserId::SystemWide("1".to_string())))]
629    #[case("foo;-", None)]
630    #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
631    #[case("FOO", None)]
632    #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
633    #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
634    #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
635    #[case("~bar", None)]
636    #[case("", None)]
637    #[case("foo;-~bar\\", None)]
638    #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
639    #[case("foo~bar~baz", None)]
640    #[case("FOO~bar", None)]
641    #[case("foo~BAR", None)]
642    fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
643        if let Some(user_id) = user_id {
644            assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
645        } else {
646            assert!(UserId::from_str(input).is_err());
647        }
648
649        Ok(())
650    }
651
652    #[rstest]
653    #[case(UserId::SystemWide("user".to_string()), None)]
654    #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some("namespace".to_string()))]
655    fn user_id_namespace(#[case] input: UserId, #[case] result: Option<String>) -> TestResult {
656        assert_eq!(input.namespace(), result);
657        Ok(())
658    }
659
660    #[rstest]
661    #[case(UserId::SystemWide("user".to_string()), false)]
662    #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
663    fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
664        assert_eq!(input.is_namespaced(), result);
665        Ok(())
666    }
667
668    #[rstest]
669    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
670    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
671    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
672    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
673    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
674    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
675    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
676    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
677    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
678    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
679    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
680    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
681    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
682    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
683    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
684    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
685    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
686    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
687    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
688    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
689    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
690    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
691    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
692    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
693    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
694    fn validate_namespace_access(
695        #[case] caller: UserId,
696        #[case] namespace_support: NamespaceSupport,
697        #[case] target: Option<UserId>,
698        #[case] role: Option<UserRole>,
699        #[case] result: Option<()>,
700    ) -> TestResult {
701        if result.is_some() {
702            assert!(
703                caller
704                    .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
705                    .is_ok()
706            );
707        } else {
708            assert!(
709                caller
710                    .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
711                    .is_err()
712            )
713        }
714        Ok(())
715    }
716}