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