[pbs-devel] [PATCH proxmox-backup 01/16] api: add Authid as wrapper around Userid

Fabian Grünbichler f.gruenbichler at proxmox.com
Wed Oct 28 12:36:23 CET 2020


with an optional Tokenname, appended with '!' as delimiter in the string
representation like for PVE.

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

Notes:
    changes since RFC:
    - rewrite, incorporating Wolfgang's and Thomas' suggestion to use an Authid as
    supertype of Userid, to make the distinction clearer

 src/api2/types/mod.rs    |   4 +-
 src/api2/types/userid.rs | 383 +++++++++++++++++++++++++++++++++++----
 2 files changed, 355 insertions(+), 32 deletions(-)

diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs
index 1b9a305f..3f723e32 100644
--- a/src/api2/types/mod.rs
+++ b/src/api2/types/mod.rs
@@ -14,9 +14,11 @@ 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::Authid;
+pub use userid::{PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA, PROXMOX_GROUP_ID_SCHEMA};
 
 // File names: may not contain slashes, may not start with "."
 pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| {
diff --git a/src/api2/types/userid.rs b/src/api2/types/userid.rs
index 44cd10b7..2b5b43af 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`] or [`Authid`].
 //!
 //! Since they're all string types, they're organized as follows:
 //!
@@ -9,13 +10,16 @@
 //!   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 name (`String` equivalent)
+//! * [`TokennameRef`]: a borrowed `Tokenname` (`str` equivalent).
+//! * [`Userid`]: an owned user id (`"user at realm"`).
+//! * [`Authid`]: an owned Authentication ID (a `Userid` with an optional `Tokenname`).
+//! Note that `Userid` and `Authid` do 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.
+//! via the `as_str()` method. [`Realm`]s, [`Userid`]s and [`Authid`]s on the other
+//! hand can be compared with each other, as in those cases the comparison has meaning.
 
 use std::borrow::Borrow;
 use std::convert::TryFrom;
@@ -36,19 +40,42 @@ 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_AUTH_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_AUTH_ID_FORMAT: ApiStringFormat =
+    ApiStringFormat::Pattern(&PROXMOX_AUTH_ID_REGEX);
+
+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_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 +118,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,7 +293,132 @@ 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 authentication 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 token name part of an authentication id. This alone does NOT uniquely identify
+/// the user.
+///
+/// This is like a `str` to the `String` of a [`Tokenname`].
+#[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
 #[derive(Clone, Debug, Hash)]
 pub struct Userid {
     data: String,
@@ -366,10 +498,18 @@ 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 name = &id[..name_len];
+        let realm = &id[(name_len + 1)..];
+
+        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"))?;
@@ -388,6 +528,10 @@ impl TryFrom<String> for Userid {
             .rposition(|&b| b == b'@')
             .ok_or_else(|| format_err!("not a valid user id"))?;
 
+        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)..])
             .map_err(|_| format_err!("invalid realm in user id"))?;
 
@@ -413,5 +557,182 @@ impl PartialEq<String> for Userid {
     }
 }
 
+/// A complete authentication id consisting of a user id and an optional token name.
+#[derive(Clone, Debug, Hash)]
+pub struct Authid {
+    user: Userid,
+    tokenname: Option<Tokenname>
+}
+
+impl Authid {
+    pub const API_SCHEMA: Schema = StringSchema::new("Authentication ID")
+        .format(&PROXMOX_AUTH_ID_FORMAT)
+        .min_length(3)
+        .max_length(64)
+        .schema();
+
+    const fn new(user: Userid, tokenname: Option<Tokenname>) -> Self {
+        Self { user, tokenname }
+    }
+
+    pub fn user(&self) -> &Userid {
+        &self.user
+    }
+
+    pub fn is_token(&self) -> bool {
+        self.tokenname.is_some()
+    }
+
+    pub fn tokenname(&self) -> Option<&TokennameRef> {
+        match &self.tokenname {
+            Some(name) => Some(&name),
+            None => None,
+        }
+    }
+
+    /// Get the "backup at pam" auth id.
+    pub fn backup_auth_id() -> &'static Self {
+        &*BACKUP_AUTHID
+    }
+
+    /// Get the "root at pam" auth id.
+    pub fn root_auth_id() -> &'static Self {
+        &*ROOT_AUTHID
+    }
+}
+
+lazy_static! {
+    pub static ref BACKUP_AUTHID: Authid = Authid::from(Userid::new("backup at pam".to_string(), 6));
+    pub static ref ROOT_AUTHID: Authid = Authid::from(Userid::new("root at pam".to_string(), 4));
+}
+
+impl Eq for Authid {}
+
+impl PartialEq for Authid {
+    fn eq(&self, rhs: &Self) -> bool {
+        self.user == rhs.user && match (&self.tokenname, &rhs.tokenname) {
+            (Some(ours), Some(theirs)) => ours.as_str() == theirs.as_str(),
+            (None, None) => true,
+            _ => false,
+        }
+    }
+}
+
+impl From<Userid> for Authid {
+    fn from(parts: Userid) -> Self {
+        Self::new(parts, None)
+    }
+}
+
+impl From<(Userid, Option<Tokenname>)> for Authid {
+    fn from(parts: (Userid, Option<Tokenname>)) -> Self {
+        Self::new(parts.0, parts.1)
+    }
+}
+
+impl fmt::Display for Authid {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match &self.tokenname {
+            Some(token) => write!(f, "{}!{}", self.user, token.as_str()),
+            None => self.user.fmt(f),
+        }
+    }
+}
+
+impl std::str::FromStr for Authid {
+    type Err = Error;
+
+    fn from_str(id: &str) -> Result<Self, Error> {
+        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 user = Userid::from_str(&id[..realm_end])?;
+
+        if id.len() > realm_end {
+            let token = Tokenname::try_from(id[(realm_end + 1)..].to_string())?;
+            Ok(Self::new(user, Some(token)))
+        } else {
+            Ok(Self::new(user, None))
+        }
+    }
+}
+
+impl TryFrom<String> for Authid {
+    type Error = Error;
+
+    fn try_from(mut data: String) -> Result<Self, Error> {
+        let name_len = data
+            .as_bytes()
+            .iter()
+            .rposition(|&b| b == b'@')
+            .ok_or_else(|| format_err!("not a valid user id"))?;
+
+        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");
+        }
+
+        let tokenname = if data.len() > realm_end {
+            Some(Tokenname::try_from(data[(realm_end + 1)..].to_string())?)
+        } else {
+            None
+        };
+
+        data.truncate(realm_end);
+
+        let user:Userid = data.parse()?;
+
+        Ok(Self { user, tokenname })
+    }
+}
+
+#[test]
+fn test_token_id() {
+    let userid: Userid = "test at pam".parse().expect("parsing Userid failed");
+    assert_eq!(userid.name().as_str(), "test");
+    assert_eq!(userid.realm(), "pam");
+    assert_eq!(userid, "test at pam");
+
+    let auth_id: Authid = "test at pam".parse().expect("parsing user Authid failed");
+    assert_eq!(auth_id.to_string(), "test at pam".to_string());
+    assert!(!auth_id.is_token());
+
+    assert_eq!(auth_id.user(), &userid);
+
+    let user_auth_id = Authid::from(userid.clone());
+    assert_eq!(user_auth_id, auth_id);
+    assert!(!user_auth_id.is_token());
+
+    let auth_id: Authid = "test at pam!bar".parse().expect("parsing token Authid failed");
+    let token_userid = auth_id.user();
+    assert_eq!(&userid, token_userid);
+    assert!(auth_id.is_token());
+    assert_eq!(auth_id.tokenname().expect("Token has tokenname").as_str(), TokennameRef::new("bar").as_str());
+    assert_eq!(auth_id.to_string(), "test at pam!bar".to_string());
+}
+
 proxmox::forward_deserialize_to_from_str!(Userid);
 proxmox::forward_serialize_to_display!(Userid);
+
+proxmox::forward_deserialize_to_from_str!(Authid);
+proxmox::forward_serialize_to_display!(Authid);
-- 
2.20.1






More information about the pbs-devel mailing list