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