[pbs-devel] [RFC proxmox-backup 04/15] Userid: extend schema with token name

Fabian Grünbichler f.gruenbichler at proxmox.com
Mon Oct 19 09:39:08 CEST 2020


similar to PVE, allow adding a !TOKENNAME suffix for API tokens
belonging to a specifc user.

Signed-off-by: Fabian Grünbichler <f.gruenbichler at proxmox.com>
---

Notes:
    not too happy with the schema names here, suggestion welcome

 src/api2/access.rs               |   4 +-
 src/api2/access/acl.rs           |   2 +-
 src/api2/access/user.rs          |   8 +-
 src/api2/admin/datastore.rs      |   2 +-
 src/api2/config/remote.rs        |   4 +-
 src/api2/types/mod.rs            |  13 +-
 src/api2/types/userid.rs         | 367 +++++++++++++++++++++++++++----
 src/bin/proxmox-backup-client.rs |   2 +-
 src/config/remote.rs             |   2 +-
 src/config/user.rs               |   4 +-
 10 files changed, 343 insertions(+), 65 deletions(-)

diff --git a/src/api2/access.rs b/src/api2/access.rs
index c302e0c7..0c19dab6 100644
--- a/src/api2/access.rs
+++ b/src/api2/access.rs
@@ -87,7 +87,7 @@ fn authenticate_user(
     input: {
         properties: {
             username: {
-                type: Userid,
+                schema: PROXMOX_USER_ID_SCHEMA,
             },
             password: {
                 schema: PASSWORD_SCHEMA,
@@ -189,7 +189,7 @@ fn create_ticket(
     input: {
         properties: {
             userid: {
-                type: Userid,
+                schema: PROXMOX_USER_ID_SCHEMA,
             },
             password: {
                 schema: PASSWORD_SCHEMA,
diff --git a/src/api2/access/acl.rs b/src/api2/access/acl.rs
index 3282c66e..cf9671c9 100644
--- a/src/api2/access/acl.rs
+++ b/src/api2/access/acl.rs
@@ -142,7 +142,7 @@ pub fn read_acl(
             },
             userid: {
                 optional: true,
-                type: Userid,
+                schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
             },
             group: {
                 optional: true,
diff --git a/src/api2/access/user.rs b/src/api2/access/user.rs
index c041d804..6c292c2d 100644
--- a/src/api2/access/user.rs
+++ b/src/api2/access/user.rs
@@ -61,7 +61,7 @@ pub fn list_users(
     input: {
         properties: {
             userid: {
-                type: Userid,
+                schema: PROXMOX_USER_ID_SCHEMA,
             },
             comment: {
                 schema: SINGLE_LINE_COMMENT_SCHEMA,
@@ -127,7 +127,7 @@ pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error>
    input: {
         properties: {
             userid: {
-                type: Userid,
+                schema: PROXMOX_USER_ID_SCHEMA,
             },
          },
     },
@@ -155,7 +155,7 @@ pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result<
     input: {
         properties: {
             userid: {
-                type: Userid,
+                schema: PROXMOX_USER_ID_SCHEMA,
             },
             comment: {
                 optional: true,
@@ -267,7 +267,7 @@ pub fn update_user(
     input: {
         properties: {
             userid: {
-                type: Userid,
+                schema: PROXMOX_USER_ID_SCHEMA,
             },
             digest: {
                 optional: true,
diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index 75e6d32b..5c9902e1 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -1501,7 +1501,7 @@ fn set_notes(
                 schema: BACKUP_ID_SCHEMA,
             },
             "new-owner": {
-                type: Userid,
+                schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
             },
         },
     },
diff --git a/src/api2/config/remote.rs b/src/api2/config/remote.rs
index d419be2b..00a5de73 100644
--- a/src/api2/config/remote.rs
+++ b/src/api2/config/remote.rs
@@ -66,7 +66,7 @@ pub fn list_remotes(
                 default: 8007,
             },
             userid: {
-                type: Userid,
+                schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
             },
             password: {
                 schema: remote::REMOTE_PASSWORD_SCHEMA,
@@ -167,7 +167,7 @@ pub enum DeletableProperty {
             },
             userid: {
                 optional: true,
-                type: Userid,
+                schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
             },
             password: {
                 optional: true,
diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs
index 75b68879..65411f73 100644
--- a/src/api2/types/mod.rs
+++ b/src/api2/types/mod.rs
@@ -14,9 +14,10 @@ mod macros;
 #[macro_use]
 mod userid;
 pub use userid::{Realm, RealmRef};
+pub use userid::{Tokenname, TokennameRef};
 pub use userid::{Username, UsernameRef};
 pub use userid::Userid;
-pub use userid::PROXMOX_GROUP_ID_SCHEMA;
+pub use userid::{PROXMOX_USER_ID_SCHEMA, PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA, PROXMOX_USER_OR_TOKEN_ID_SCHEMA, PROXMOX_GROUP_ID_SCHEMA};
 
 // File names: may not contain slashes, may not start with "."
 pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| {
@@ -364,7 +365,7 @@ pub const BLOCKDEVICE_NAME_SCHEMA: Schema = StringSchema::new("Block device name
             },
         },
         owner: {
-            type: Userid,
+            schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
             optional: true,
         },
     },
@@ -440,7 +441,7 @@ pub struct SnapshotVerifyState {
             },
         },
         owner: {
-            type: Userid,
+            schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
             optional: true,
         },
     },
@@ -612,7 +613,7 @@ pub struct StorageStatus {
 #[api(
     properties: {
         upid: { schema: UPID_SCHEMA },
-        user: { type: Userid },
+        user: { schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA },
     },
 )]
 #[derive(Serialize, Deserialize)]
@@ -977,7 +978,7 @@ fn test_proxmox_user_id_schema() -> Result<(), anyhow::Error> {
     ];
 
     for name in invalid_user_ids.iter() {
-        if let Ok(_) = parse_simple_value(name, &Userid::API_SCHEMA) {
+        if let Ok(_) = parse_simple_value(name, &PROXMOX_USER_ID_SCHEMA) {
             bail!("test userid '{}' failed -  got Ok() while exception an error.", name);
         }
     }
@@ -991,7 +992,7 @@ fn test_proxmox_user_id_schema() -> Result<(), anyhow::Error> {
     ];
 
     for name in valid_user_ids.iter() {
-        let v = match parse_simple_value(name, &Userid::API_SCHEMA) {
+        let v = match parse_simple_value(name, &PROXMOX_USER_ID_SCHEMA) {
             Ok(v) => v,
             Err(err) => {
                 bail!("unable to parse userid '{}' - {}", name, err);
diff --git a/src/api2/types/userid.rs b/src/api2/types/userid.rs
index 44cd10b7..591c7d26 100644
--- a/src/api2/types/userid.rs
+++ b/src/api2/types/userid.rs
@@ -1,6 +1,7 @@
 //! Types for user handling.
 //!
-//! We have [`Username`]s and [`Realm`]s. To uniquely identify a user, they must be combined into a [`Userid`].
+//! We have [`Username`]s, [`Realm`]s and [`Tokenname`]s. To uniquely identify a user/API token, they
+//! must be combined into a [`Userid`].
 //!
 //! Since they're all string types, they're organized as follows:
 //!
@@ -9,10 +10,12 @@
 //!   with `String`, meaning you can only make references to it.
 //! * [`Realm`]: an owned realm (`String` equivalent).
 //! * [`RealmRef`]: a borrowed realm (`str` equivalent).
-//! * [`Userid`]: an owned user id (`"user at realm"`). Note that this does not have a separate
-//!   borrowed type.
+//! * [`Tokenname`]: an owned API token ID (`String` equivalent)
+//! * [`TokennameRef`]: a borrowed `Tokenname` (`str` equivalent).
+//! * [`Userid`]: an owned user id (`"user at realm"`), or API token ID (`"user at realm!tokenid"`). Note
+//! that this does not have a separate borrowed type.
 //!
-//! Note that `Username`s are not unique, therefore they do not implement `Eq` and cannot be
+//! Note that `Username`s and `Tokenname`s are not unique, therefore they do not implement `Eq` and cannot be
 //! compared directly. If a direct comparison is really required, they can be compared as strings
 //! via the `as_str()` method. [`Realm`]s and [`Userid`]s on the other hand can be compared with
 //! each other, as in those two cases the comparison has meaning.
@@ -36,19 +39,54 @@ use proxmox::const_regex;
 // also see "man useradd"
 macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") }
 macro_rules! GROUP_NAME_REGEX_STR { () => (USER_NAME_REGEX_STR!()) }
+macro_rules! TOKEN_NAME_REGEX_STR { () => (PROXMOX_SAFE_ID_REGEX_STR!()) }
 macro_rules! USER_ID_REGEX_STR { () => (concat!(USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!())) }
+macro_rules! APITOKEN_ID_REGEX_STR { () => (concat!(USER_ID_REGEX_STR!() , r"!", TOKEN_NAME_REGEX_STR!())) }
 
 const_regex! {
     pub PROXMOX_USER_NAME_REGEX = concat!(r"^",  USER_NAME_REGEX_STR!(), r"$");
+    pub PROXMOX_TOKEN_NAME_REGEX = concat!(r"^", TOKEN_NAME_REGEX_STR!(), r"$");
     pub PROXMOX_USER_ID_REGEX = concat!(r"^",  USER_ID_REGEX_STR!(), r"$");
+    pub PROXMOX_APITOKEN_ID_REGEX = concat!(r"^", APITOKEN_ID_REGEX_STR!(), r"$");
+    pub PROXMOX_USER_OR_APITOKEN_ID_REGEX = concat!(r"^", r"(?:", USER_ID_REGEX_STR!(), r"|", APITOKEN_ID_REGEX_STR!(), r")$");
     pub PROXMOX_GROUP_ID_REGEX = concat!(r"^",  GROUP_NAME_REGEX_STR!(), r"$");
 }
 
 pub const PROXMOX_USER_NAME_FORMAT: ApiStringFormat =
     ApiStringFormat::Pattern(&PROXMOX_USER_NAME_REGEX);
+pub const PROXMOX_TOKEN_NAME_FORMAT: ApiStringFormat =
+    ApiStringFormat::Pattern(&PROXMOX_TOKEN_NAME_REGEX);
 
 pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat =
     ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX);
+pub const PROXMOX_TOKEN_ID_FORMAT: ApiStringFormat =
+    ApiStringFormat::Pattern(&PROXMOX_APITOKEN_ID_REGEX);
+pub const PROXMOX_USER_OR_TOKEN_ID_FORMAT: ApiStringFormat =
+    ApiStringFormat::Pattern(&PROXMOX_USER_OR_APITOKEN_ID_REGEX);
+
+pub const PROXMOX_USER_ID_SCHEMA: Schema = StringSchema::new("User ID")
+    .format(&PROXMOX_USER_ID_FORMAT)
+    .min_length(3)
+    .max_length(64)
+    .schema();
+
+pub const PROXMOX_TOKEN_ID_SCHEMA: Schema = StringSchema::new("API Token ID")
+    .format(&PROXMOX_TOKEN_ID_FORMAT)
+    .min_length(3)
+    .max_length(64)
+    .schema();
+
+pub const PROXMOX_USER_OR_TOKEN_ID_SCHEMA: Schema = StringSchema::new("User ID (with optional API token subid)")
+    .format(&PROXMOX_USER_OR_TOKEN_ID_FORMAT)
+    .min_length(3)
+    .max_length(64)
+    .schema();
+
+pub const PROXMOX_TOKEN_NAME_SCHEMA: Schema = StringSchema::new("API Token name")
+    .format(&PROXMOX_TOKEN_NAME_FORMAT)
+    .min_length(3)
+    .max_length(64)
+    .schema();
 
 pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat =
     ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX);
@@ -91,26 +129,6 @@ pub struct Username(String);
 #[derive(Debug, Hash)]
 pub struct UsernameRef(str);
 
-#[doc(hidden)]
-/// ```compile_fail
-/// let a: Username = unsafe { std::mem::zeroed() };
-/// let b: Username = unsafe { std::mem::zeroed() };
-/// let _ = <Username as PartialEq>::eq(&a, &b);
-/// ```
-///
-/// ```compile_fail
-/// let a: &UsernameRef = unsafe { std::mem::zeroed() };
-/// let b: &UsernameRef = unsafe { std::mem::zeroed() };
-/// let _ = <&UsernameRef as PartialEq>::eq(a, b);
-/// ```
-///
-/// ```compile_fail
-/// let a: &UsernameRef = unsafe { std::mem::zeroed() };
-/// let b: &UsernameRef = unsafe { std::mem::zeroed() };
-/// let _ = <&UsernameRef as PartialEq>::eq(&a, &b);
-/// ```
-struct _AssertNoEqImpl;
-
 impl UsernameRef {
     fn new(s: &str) -> &Self {
         unsafe { &*(s as *const str as *const UsernameRef) }
@@ -286,24 +304,143 @@ impl PartialEq<Realm> for &RealmRef {
     }
 }
 
-/// A complete user id consting of a user name and a realm.
+#[api(
+    type: String,
+    format: &PROXMOX_TOKEN_NAME_FORMAT,
+)]
+/// The token ID part of an API token user id.
+///
+/// This alone does NOT uniquely identify the API token and therefore does not implement `Eq`. In
+/// order to compare token IDs directly, they need to be explicitly compared as strings by calling
+/// `.as_str()`.
+///
+/// ```compile_fail
+/// fn test(a: Tokenname, b: Tokenname) -> bool {
+///     a == b // illegal and does not compile
+/// }
+/// ```
+#[derive(Clone, Debug, Hash, Deserialize, Serialize)]
+pub struct Tokenname(String);
+
+/// A reference to a user name part of a user id. This alone does NOT uniquely identify the user.
+///
+/// This is like a `str` to the `String` of a [`Username`].
+#[derive(Debug, Hash)]
+pub struct TokennameRef(str);
+
+#[doc(hidden)]
+/// ```compile_fail
+/// let a: Username = unsafe { std::mem::zeroed() };
+/// let b: Username = unsafe { std::mem::zeroed() };
+/// let _ = <Username as PartialEq>::eq(&a, &b);
+/// ```
+///
+/// ```compile_fail
+/// let a: &UsernameRef = unsafe { std::mem::zeroed() };
+/// let b: &UsernameRef = unsafe { std::mem::zeroed() };
+/// let _ = <&UsernameRef as PartialEq>::eq(a, b);
+/// ```
+///
+/// ```compile_fail
+/// let a: &UsernameRef = unsafe { std::mem::zeroed() };
+/// let b: &UsernameRef = unsafe { std::mem::zeroed() };
+/// let _ = <&UsernameRef as PartialEq>::eq(&a, &b);
+/// ```
+///
+/// ```compile_fail
+/// let a: Tokenname = unsafe { std::mem::zeroed() };
+/// let b: Tokenname = unsafe { std::mem::zeroed() };
+/// let _ = <Tokenname as PartialEq>::eq(&a, &b);
+/// ```
+///
+/// ```compile_fail
+/// let a: &TokennameRef = unsafe { std::mem::zeroed() };
+/// let b: &TokennameRef = unsafe { std::mem::zeroed() };
+/// let _ = <&TokennameRef as PartialEq>::eq(a, b);
+/// ```
+///
+/// ```compile_fail
+/// let a: &TokennameRef = unsafe { std::mem::zeroed() };
+/// let b: &TokennameRef = unsafe { std::mem::zeroed() };
+/// let _ = <&TokennameRef as PartialEq>::eq(&a, &b);
+/// ```
+struct _AssertNoEqImpl;
+
+impl TokennameRef {
+    fn new(s: &str) -> &Self {
+        unsafe { &*(s as *const str as *const TokennameRef) }
+    }
+
+    pub fn as_str(&self) -> &str {
+        &self.0
+    }
+}
+
+impl std::ops::Deref for Tokenname {
+    type Target = TokennameRef;
+
+    fn deref(&self) -> &TokennameRef {
+        self.borrow()
+    }
+}
+
+impl Borrow<TokennameRef> for Tokenname {
+    fn borrow(&self) -> &TokennameRef {
+        TokennameRef::new(self.0.as_str())
+    }
+}
+
+impl AsRef<TokennameRef> for Tokenname {
+    fn as_ref(&self) -> &TokennameRef {
+        self.borrow()
+    }
+}
+
+impl ToOwned for TokennameRef {
+    type Owned = Tokenname;
+
+    fn to_owned(&self) -> Self::Owned {
+        Tokenname(self.0.to_owned())
+    }
+}
+
+impl TryFrom<String> for Tokenname {
+    type Error = Error;
+
+    fn try_from(s: String) -> Result<Self, Error> {
+        if !PROXMOX_TOKEN_NAME_REGEX.is_match(&s) {
+            bail!("invalid token name");
+        }
+
+        Ok(Self(s))
+    }
+}
+
+impl<'a> TryFrom<&'a str> for &'a TokennameRef {
+    type Error = Error;
+
+    fn try_from(s: &'a str) -> Result<&'a TokennameRef, Error> {
+        if !PROXMOX_TOKEN_NAME_REGEX.is_match(s) {
+            bail!("invalid token name in user id");
+        }
+
+        Ok(TokennameRef::new(s))
+    }
+}
+
+/// A complete user id consisting of a user name and a realm, and optional token name.
 #[derive(Clone, Debug, Hash)]
 pub struct Userid {
     data: String,
     name_len: usize,
+    token_len: usize,
     //name: Username,
     //realm: Realm,
 }
 
 impl Userid {
-    pub const API_SCHEMA: Schema = StringSchema::new("User ID")
-        .format(&PROXMOX_USER_ID_FORMAT)
-        .min_length(3)
-        .max_length(64)
-        .schema();
-
-    const fn new(data: String, name_len: usize) -> Self {
-        Self { data, name_len }
+    const fn new(data: String, name_len: usize, token_len: usize) -> Self {
+        Self { data, name_len, token_len }
     }
 
     pub fn name(&self) -> &UsernameRef {
@@ -311,7 +448,33 @@ impl Userid {
     }
 
     pub fn realm(&self) -> &RealmRef {
-        RealmRef::new(&self.data[(self.name_len + 1)..])
+        if self.token_len > 0 {
+            RealmRef::new(&self.data[(self.name_len + 1)..(self.data.len() - self.token_len - 1)])
+        } else {
+            RealmRef::new(&self.data[(self.name_len + 1)..])
+        }
+    }
+
+    pub fn tokenname(&self) -> Option<&TokennameRef> {
+        if self.token_len > 0 {
+            Some(TokennameRef::new(&self.data[(self.data.len() - self.token_len)..]))
+        } else {
+            None
+        }
+    }
+
+    pub fn is_tokenid(&self) -> bool {
+        self.token_len > 0
+    }
+
+    pub fn owner(&self) -> Result<Userid, Error> {
+        if !self.is_tokenid() {
+            bail!("userid is a regular user, not a token - can't determine owner");
+        }
+
+        let owner_str = &self.data.clone()[..self.data.len() - 1 - self.token_len];
+
+        Ok(Userid::new(owner_str.to_string(), self.name_len, 0))
     }
 
     pub fn as_str(&self) -> &str {
@@ -330,15 +493,17 @@ impl Userid {
 }
 
 lazy_static! {
-    pub static ref BACKUP_USERID: Userid = Userid::new("backup at pam".to_string(), 6);
-    pub static ref ROOT_USERID: Userid = Userid::new("root at pam".to_string(), 4);
+    pub static ref BACKUP_USERID: Userid = Userid::new("backup at pam".to_string(), 6, 0);
+    pub static ref ROOT_USERID: Userid = Userid::new("root at pam".to_string(), 4, 0);
 }
 
 impl Eq for Userid {}
 
 impl PartialEq for Userid {
     fn eq(&self, rhs: &Self) -> bool {
-        self.data == rhs.data && self.name_len == rhs.name_len
+        self.data == rhs.data
+            && self.name_len == rhs.name_len
+            && self.token_len == rhs.token_len
     }
 }
 
@@ -352,7 +517,23 @@ impl From<(&UsernameRef, &RealmRef)> for Userid {
     fn from(parts: (&UsernameRef, &RealmRef)) -> Self {
         let data = format!("{}@{}", parts.0.as_str(), parts.1.as_str());
         let name_len = parts.0.as_str().len();
-        Self { data, name_len }
+        let token_len = 0;
+        Self { data, name_len, token_len }
+    }
+}
+
+impl From<(Username, Realm, Tokenname)> for Userid {
+    fn from(parts: (Username, Realm, Tokenname)) -> Self {
+        Self::from((parts.0.as_ref(), parts.1.as_ref(), parts.2.as_ref()))
+    }
+}
+
+impl From<(&UsernameRef, &RealmRef, &TokennameRef)> for Userid {
+    fn from(parts: (&UsernameRef, &RealmRef, &TokennameRef)) -> Self {
+        let data = format!("{}@{}!{}", parts.0.as_str(), parts.1.as_str(), parts.2.as_str());
+        let name_len = parts.0.as_str().len();
+        let token_len = parts.2.as_str().len();
+        Self { data, name_len, token_len }
     }
 }
 
@@ -366,15 +547,42 @@ impl std::str::FromStr for Userid {
     type Err = Error;
 
     fn from_str(id: &str) -> Result<Self, Error> {
-        let (name, realm) = match id.as_bytes().iter().rposition(|&b| b == b'@') {
-            Some(pos) => (&id[..pos], &id[(pos + 1)..]),
-            None => bail!("not a valid user id"),
-        };
+        let name_len = id
+            .as_bytes()
+            .iter()
+            .rposition(|&b| b == b'@')
+            .ok_or_else(|| format_err!("not a valid user id"))?;
+
+        let realm_end = id
+            .as_bytes()
+            .iter()
+            .rposition(|&b| b == b'!')
+            .map(|pos| if pos < name_len { id.len() } else { pos })
+            .unwrap_or(id.len());
+
+        if realm_end == id.len() - 1 {
+            bail!("empty token name in userid");
+        }
+
+        let name = &id[..name_len];
+        let realm = &id[(name_len + 1)..realm_end];
 
+        if !PROXMOX_USER_NAME_REGEX.is_match(name) {
+            bail!("invalid user name in user id");
+        }
+
         PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(realm)
             .map_err(|_| format_err!("invalid realm in user id"))?;
 
-        Ok(Self::from((UsernameRef::new(name), RealmRef::new(realm))))
+        if id.len() > realm_end {
+            let token = &id[(realm_end + 1)..];
+            if !PROXMOX_TOKEN_NAME_REGEX.is_match(token) {
+                bail!("invalid token name in user id");
+            }
+            Ok(Self::from((UsernameRef::new(name), RealmRef::new(realm), TokennameRef::new(token))))
+        } else {
+            Ok(Self::from((UsernameRef::new(name), RealmRef::new(realm))))
+        }
     }
 }
 
@@ -388,10 +596,34 @@ impl TryFrom<String> for Userid {
             .rposition(|&b| b == b'@')
             .ok_or_else(|| format_err!("not a valid user id"))?;
 
-        PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(&data[(name_len + 1)..])
+        let realm_end = data
+            .as_bytes()
+            .iter()
+            .rposition(|&b| b == b'!')
+            .map(|pos| if pos < name_len { data.len() } else { pos })
+            .unwrap_or(data.len());
+
+        if realm_end == data.len() - 1 {
+            bail!("empty token name in userid");
+        }
+
+        if !PROXMOX_USER_NAME_REGEX.is_match(&data[..name_len]) {
+            bail!("invalid user name in user id");
+        }
+
+        PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(&data[(name_len + 1)..realm_end])
             .map_err(|_| format_err!("invalid realm in user id"))?;
 
-        Ok(Self { data, name_len })
+        let token_len = if realm_end == data.len() {
+            0
+        } else {
+            if !PROXMOX_TOKEN_NAME_REGEX.is_match(&data[(realm_end + 1)..]) {
+                bail!("invalid token name in user id");
+            }
+            data.len() - realm_end - 1
+        };
+
+        Ok(Self { data, name_len, token_len })
     }
 }
 
@@ -413,5 +645,50 @@ impl PartialEq<String> for Userid {
     }
 }
 
+#[test]
+fn test_token_id() {
+    use std::str::FromStr;
+    use std::convert::TryFrom;
+
+    let userid = Userid::new("test at pam!bar".to_string(), 4, 3);
+    assert_eq!(userid.name().as_str(), "test");
+    assert_eq!(userid.realm(), "pam");
+    assert_eq!(userid.tokenname().expect("token should return tokenname").as_str(), "bar");
+    assert_eq!(userid, "test at pam!bar");
+
+    assert_eq!(userid, Userid::from_str("test at pam!bar").expect("parsing token from str failed"));
+    assert_eq!(userid, Userid::try_from("test at pam!bar".to_string()).expect("parsing token from String failed"));
+
+    let userid = Userid::new("test at pam".to_string(), 4, 0);
+    assert_eq!(userid.name().as_str(), "test");
+    assert_eq!(userid.realm(), "pam");
+    // replace with .expect_none once that is stable
+    assert_eq!(userid.tokenname().is_none(), true);
+    assert_eq!(userid, "test at pam");
+
+    assert_eq!(userid, Userid::from_str("test at pam").expect("parsing user from str failed"));
+    assert_eq!(userid, Userid::try_from("test at pam".to_string()).expect("parsing user from String failed"));
+
+    Userid::from_str("test at pam!bar at baz").expect("username with @ and ! failed");
+    Userid::try_from("test at pam!bar at baz".to_string()).expect("username with @ and ! failed");
+
+    Userid::from_str("test at pam!").expect_err("empty token should fail");
+    Userid::from_str("t€st at pam").expect("strange chars in username allowed");
+    Userid::from_str("tes/@pam").expect_err("slash in username not allowed");
+    Userid::from_str("tes:@pam").expect_err("colon in username not allowed");
+    Userid::from_str("tes\n at pam").expect_err("newline in username not allowed");
+    Userid::from_str("tes\0 at pam").expect_err("\0 in username not allowed");
+    Userid::from_str("test@¶am").expect_err("strange chars in realm not allowed");
+
+    let userid = Userid::new("test at pam".to_string(), 4, 0);
+    let (name, realm) = (userid.name(), userid.realm());
+    assert_eq!(userid, Userid::from((name, realm)));
+
+    let userid = Userid::new("test at pam!test".to_string(), 4, 4);
+    let (name, realm, tokenname) = (userid.name(), userid.realm(), userid.tokenname().expect("tokenid should return tokennameref"));
+    assert_eq!(userid, Userid::from((name, realm, tokenname)));
+
+}
+
 proxmox::forward_deserialize_to_from_str!(Userid);
 proxmox::forward_serialize_to_display!(Userid);
diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs
index ffe5b3dd..1fbbca09 100644
--- a/src/bin/proxmox-backup-client.rs
+++ b/src/bin/proxmox-backup-client.rs
@@ -425,7 +425,7 @@ async fn list_backup_groups(param: Value) -> Result<Value, Error> {
                 description: "Backup group.",
             },
             "new-owner": {
-                type: Userid,
+                schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
             },
         }
    }
diff --git a/src/config/remote.rs b/src/config/remote.rs
index 9e597342..ac6079ac 100644
--- a/src/config/remote.rs
+++ b/src/config/remote.rs
@@ -45,7 +45,7 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
             type: u16,
         },
         userid: {
-            type: Userid,
+            schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
         },
         password: {
             schema: REMOTE_PASSWORD_SCHEMA,
diff --git a/src/config/user.rs b/src/config/user.rs
index b72fa40b..efb346d8 100644
--- a/src/config/user.rs
+++ b/src/config/user.rs
@@ -56,7 +56,7 @@ pub const EMAIL_SCHEMA: Schema = StringSchema::new("E-Mail Address.")
 #[api(
     properties: {
         userid: {
-            type: Userid,
+            schema: PROXMOX_USER_ID_SCHEMA,
         },
         comment: {
             optional: true,
@@ -109,7 +109,7 @@ fn init() -> SectionConfig {
     };
 
     let plugin = SectionConfigPlugin::new("user".to_string(), Some("userid".to_string()), obj_schema);
-    let mut config = SectionConfig::new(&Userid::API_SCHEMA);
+    let mut config = SectionConfig::new(&PROXMOX_USER_ID_SCHEMA);
 
     config.register_plugin(plugin);
 
-- 
2.20.1






More information about the pbs-devel mailing list