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};
9use strum::AsRefStr;
10
11use crate::UserRole;
12
13/// An error that may occur when operating on users.
14#[derive(Debug, thiserror::Error)]
15pub enum Error {
16    /// Unable to convert string slice to Passphrase
17    #[error("Unable to convert string to passphrase")]
18    Passphrase,
19
20    /// The passphrase for a [`UserId`] is missing.
21    #[error("The passphrase for user {user} is missing")]
22    PassphraseMissing {
23        /// The [`UserId`] for which the passphrase is missing.
24        user: UserId,
25    },
26
27    /// One or more [`NamespaceId`]s are invalid.
28    #[error("Invalid Namespace IDs: {}", namespace_ids.join(", "))]
29    InvalidNamespaceIds {
30        /// The list of invalid Namespace IDs.
31        namespace_ids: Vec<String>,
32    },
33
34    /// One or more [`UserId`]s are invalid.
35    #[error("Invalid User IDs: {}", user_ids.join(", "))]
36    InvalidUserIds {
37        /// A list of strings representing invalid [`UserId`]s.
38        user_ids: Vec<String>,
39    },
40
41    /// The API call does not support users in namespaces
42    #[error("The calling user {0} is in a namespace, which is not supported in this context.")]
43    NamespaceUnsupported(UserId),
44
45    /// A user in one namespace targets a user in another
46    #[error("User {caller} targets {target} which is in a different namespace")]
47    NamespaceTargetMismatch {
48        /// The [`UserId`] of a user that targets a user in another namespace.
49        caller: UserId,
50
51        /// The [`UserId`] of the targeted user.
52        target: UserId,
53    },
54
55    /// A user in a namespace tries to modify a system-wide user
56    #[error("User {caller} targets {target} a system-wide user")]
57    NamespaceSystemWideTarget {
58        /// The [`UserId`] of a user in a namespace that attempts to modify a system-wide user.
59        caller: UserId,
60
61        /// The [`UserId`] of a system-wide user that `caller` attempts to modify.
62        target: UserId,
63    },
64
65    /// A user in Backup or Metrics role is about to be created in a namespace
66    #[error(
67        "User {caller} attempts to create user {target} in role {role} which is not supported in namespaces"
68    )]
69    NamespaceRoleInvalid {
70        /// The [`UserId`] of the user trying to create `target` in `role`.
71        caller: UserId,
72
73        /// The [`UserId`] of the user in a namespace that is attempted to be created by `caller`.
74        target: UserId,
75
76        /// The [`UserRole`] of `target`.
77        role: UserRole,
78    },
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/// Credentials for a [`NetHsm`][`crate::NetHsm`].
436///
437/// Tracks a [`UserId`] and an accompanying [`Passphrase`].
438/// Different from [`Credentials`], this type _requires_ a [`Passphrase`].
439#[derive(Clone, Debug, Deserialize, Serialize)]
440pub struct FullCredentials {
441    /// The user name.
442    pub name: UserId,
443
444    /// The passphrase for `name`.
445    pub passphrase: Passphrase,
446}
447
448impl FullCredentials {
449    /// Creates a new [`FullCredentials`].
450    ///
451    /// # Examples
452    ///
453    /// ```
454    /// use nethsm::FullCredentials;
455    ///
456    /// # fn main() -> testresult::TestResult {
457    /// let creds = FullCredentials::new("operator".parse()?, "passphrase".parse()?);
458    /// # eprintln!("{creds:?}");
459    /// # Ok(())
460    /// # }
461    /// ```
462    pub fn new(name: UserId, passphrase: Passphrase) -> Self {
463        Self { name, passphrase }
464    }
465}
466
467impl From<FullCredentials> for BasicAuth {
468    fn from(value: FullCredentials) -> Self {
469        Self::from(&value)
470    }
471}
472
473impl From<&FullCredentials> for BasicAuth {
474    fn from(value: &FullCredentials) -> Self {
475        (
476            value.name.to_string(),
477            Some(value.passphrase.expose_owned()),
478        )
479    }
480}
481
482impl TryFrom<&Credentials> for FullCredentials {
483    type Error = Error;
484
485    fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
486        let creds = value.clone();
487        FullCredentials::try_from(creds)
488    }
489}
490
491impl TryFrom<Credentials> for FullCredentials {
492    type Error = Error;
493
494    fn try_from(value: Credentials) -> Result<Self, Self::Error> {
495        let Some(passphrase) = value.passphrase else {
496            return Err(Error::PassphraseMissing {
497                user: value.user_id,
498            });
499        };
500
501        Ok(FullCredentials {
502            name: value.user_id,
503            passphrase,
504        })
505    }
506}
507
508/// Credentials for a [`NetHsm`][`crate::NetHsm`]
509///
510/// Holds a user ID and an accompanying [`Passphrase`].
511#[derive(Clone, Debug)]
512pub struct Credentials {
513    /// The user ID.
514    pub user_id: UserId,
515
516    /// The optional passphrase for `user_id`.
517    pub passphrase: Option<Passphrase>,
518}
519
520impl Credentials {
521    /// Creates a new [`Credentials`]
522    ///
523    /// # Examples
524    ///
525    /// ```
526    /// use nethsm::{Credentials, Passphrase};
527    ///
528    /// # fn main() -> testresult::TestResult {
529    /// let creds = Credentials::new(
530    ///     "operator".parse()?,
531    ///     Some(Passphrase::new("passphrase".to_string())),
532    /// );
533    /// # Ok(())
534    /// # }
535    /// ```
536    pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
537        Self {
538            user_id,
539            passphrase,
540        }
541    }
542}
543
544impl Display for Credentials {
545    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546        write!(f, "{}", self.user_id)?;
547        if let Some(passphrase) = self.passphrase.as_ref() {
548            write!(f, " ({passphrase})")?;
549        }
550        Ok(())
551    }
552}
553
554impl From<Credentials> for BasicAuth {
555    fn from(value: Credentials) -> Self {
556        (
557            value.user_id.to_string(),
558            value.passphrase.map(|x| x.expose_owned()),
559        )
560    }
561}
562
563impl From<&Credentials> for BasicAuth {
564    fn from(value: &Credentials) -> Self {
565        (
566            value.user_id.to_string(),
567            value.passphrase.as_ref().map(|x| x.expose_owned()),
568        )
569    }
570}
571
572impl From<&FullCredentials> for Credentials {
573    fn from(value: &FullCredentials) -> Self {
574        let creds = value.clone();
575        Self::from(creds)
576    }
577}
578
579impl From<FullCredentials> for Credentials {
580    fn from(value: FullCredentials) -> Self {
581        Credentials::new(value.name, Some(value.passphrase))
582    }
583}
584
585/// A secret passphrase
586///
587/// The passphrase is held by a [`SecretString`], which guarantees zeroing of memory on
588/// destruct.
589#[derive(Clone, Debug, Default, Deserialize)]
590pub struct Passphrase(SecretString);
591
592impl Passphrase {
593    /// Creates a new [`Passphrase`] from owned [`String`]
594    ///
595    /// # Examples
596    /// ```
597    /// use nethsm::Passphrase;
598    ///
599    /// let passphrase = Passphrase::new("passphrase".to_string());
600    /// ```
601    pub fn new(passphrase: String) -> Self {
602        Self(SecretString::new(passphrase.into()))
603    }
604
605    /// Exposes the secret passphrase as owned [`String`]
606    ///
607    /// This is a convenience function, as much of [`nethsm_sdk_rs`] exclusively deals with owned
608    /// strings.
609    pub fn expose_owned(&self) -> String {
610        self.0.expose_secret().to_owned()
611    }
612
613    /// Exposes the secret passphrase as borrowed [`str`]
614    pub fn expose_borrowed(&self) -> &str {
615        self.0.expose_secret()
616    }
617}
618
619impl Display for Passphrase {
620    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621        write!(f, "[REDACTED]")
622    }
623}
624
625impl FromStr for Passphrase {
626    type Err = Error;
627
628    fn from_str(s: &str) -> Result<Self, Self::Err> {
629        Ok(Self(SecretString::from(s.to_string())))
630    }
631}
632
633impl Serialize for Passphrase {
634    /// Serializes a [`Passphrase`].
635    ///
636    /// # Warning
637    ///
638    /// This may be used to write a passphrase to file!
639    /// Take precautions so that passphrases can not leak to the environment.
640    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
641    where
642        S: serde::Serializer,
643    {
644        self.0.expose_secret().serialize(serializer)
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use rstest::rstest;
651    use testresult::TestResult;
652
653    use super::*;
654
655    #[test]
656    fn passphrase_display() -> TestResult {
657        let passphrase = Passphrase::new("a-secret-passphrase".to_string());
658        assert_eq!(format!("{passphrase}"), "[REDACTED]");
659        Ok(())
660    }
661
662    #[rstest]
663    #[case(Credentials::new(UserId::new("user".to_string())?, Some(Passphrase::new("a-secret-passphrase".to_string()))), "user ([REDACTED])")]
664    #[case(Credentials::new(UserId::new("user".to_string())?, None), "user")]
665    fn credentials_display(#[case] credentials: Credentials, #[case] expected: &str) -> TestResult {
666        assert_eq!(credentials.to_string(), expected);
667        Ok(())
668    }
669
670    #[rstest]
671    #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
672    #[case("f", Some(UserId::SystemWide("f".to_string())))]
673    #[case("1", Some(UserId::SystemWide("1".to_string())))]
674    #[case("foo;-", None)]
675    #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
676    #[case("FOO", None)]
677    #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
678    #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
679    #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
680    #[case("~bar", None)]
681    #[case("", None)]
682    #[case("foo;-~bar\\", None)]
683    #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
684    #[case("foo~bar~baz", None)]
685    #[case("FOO~bar", None)]
686    #[case("foo~BAR", None)]
687    fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
688        if let Some(user_id) = user_id {
689            assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
690        } else {
691            assert!(UserId::from_str(input).is_err());
692        }
693
694        Ok(())
695    }
696
697    #[rstest]
698    #[case(UserId::SystemWide("user".to_string()), None)]
699    #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some(NamespaceId("namespace".to_string())))]
700    fn user_id_namespace(#[case] input: UserId, #[case] result: Option<NamespaceId>) -> TestResult {
701        assert_eq!(input.namespace(), result.as_ref());
702        Ok(())
703    }
704
705    #[rstest]
706    #[case(UserId::SystemWide("user".to_string()), false)]
707    #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
708    fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
709        assert_eq!(input.is_namespaced(), result);
710        Ok(())
711    }
712
713    #[rstest]
714    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
715    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
716    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
717    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
718    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
719    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
720    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
721    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
722    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
723    #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
724    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
725    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
726    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
727    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
728    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
729    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
730    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
731    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
732    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
733    #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
734    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
735    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
736    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
737    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
738    #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
739    fn validate_namespace_access(
740        #[case] caller: UserId,
741        #[case] namespace_support: NamespaceSupport,
742        #[case] target: Option<UserId>,
743        #[case] role: Option<UserRole>,
744        #[case] result: Option<()>,
745    ) -> TestResult {
746        if result.is_some() {
747            assert!(
748                caller
749                    .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
750                    .is_ok()
751            );
752        } else {
753            assert!(
754                caller
755                    .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
756                    .is_err()
757            )
758        }
759        Ok(())
760    }
761}