From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 9261D62C5E for ; Wed, 28 Oct 2020 12:36:42 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 904F61E812 for ; Wed, 28 Oct 2020 12:36:42 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 06D6C1E7FC for ; Wed, 28 Oct 2020 12:36:41 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id BEC7C459B0 for ; Wed, 28 Oct 2020 12:36:40 +0100 (CET) From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= To: pbs-devel@lists.proxmox.com Date: Wed, 28 Oct 2020 12:36:23 +0100 Message-Id: <20201028113632.814586-3-f.gruenbichler@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201028113632.814586-1-f.gruenbichler@proxmox.com> References: <20201028113632.814586-1-f.gruenbichler@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.029 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [userid.rs, mod.rs] Subject: [pbs-devel] [PATCH proxmox-backup 01/16] api: add Authid as wrapper around Userid X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 28 Oct 2020 11:36:42 -0000 with an optional Tokenname, appended with '!' as delimiter in the string representation like for PVE. Signed-off-by: Fabian Grünbichler --- 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@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@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 _ = ::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 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 _ = ::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 _ = ::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 for Tokenname { + fn borrow(&self) -> &TokennameRef { + TokennameRef::new(self.0.as_str()) + } +} + +impl AsRef 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 for Tokenname { + type Error = Error; + + fn try_from(s: String) -> Result { + 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 { - 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 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 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 +} + +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) -> 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@pam" auth id. + pub fn backup_auth_id() -> &'static Self { + &*BACKUP_AUTHID + } + + /// Get the "root@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@pam".to_string(), 6)); + pub static ref ROOT_AUTHID: Authid = Authid::from(Userid::new("root@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 for Authid { + fn from(parts: Userid) -> Self { + Self::new(parts, None) + } +} + +impl From<(Userid, Option)> for Authid { + fn from(parts: (Userid, Option)) -> 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 { + 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 for Authid { + type Error = Error; + + fn try_from(mut data: String) -> Result { + 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@pam".parse().expect("parsing Userid failed"); + assert_eq!(userid.name().as_str(), "test"); + assert_eq!(userid.realm(), "pam"); + assert_eq!(userid, "test@pam"); + + let auth_id: Authid = "test@pam".parse().expect("parsing user Authid failed"); + assert_eq!(auth_id.to_string(), "test@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@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@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