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 5B9AA60989 for ; Mon, 19 Oct 2020 09:40:11 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 496322981D for ; Mon, 19 Oct 2020 09:39:41 +0200 (CEST) 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 A47FD29813 for ; Mon, 19 Oct 2020 09:39:39 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 6D97645D69 for ; Mon, 19 Oct 2020 09:39:39 +0200 (CEST) From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= To: pbs-devel@lists.proxmox.com Date: Mon, 19 Oct 2020 09:39:08 +0200 Message-Id: <20201019073919.588521-5-f.gruenbichler@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201019073919.588521-1-f.gruenbichler@proxmox.com> References: <20201019073919.588521-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.032 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 Subject: [pbs-devel] [RFC proxmox-backup 04/15] Userid: extend schema with token name 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: Mon, 19 Oct 2020 07:40:11 -0000 similar to PVE, allow adding a !TOKENNAME suffix for API tokens belonging to a specifc user. Signed-off-by: Fabian Grünbichler --- 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, 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@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@realm"`), or API token ID (`"user@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 _ = ::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 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 _ = ::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, 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 { + 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@pam".to_string(), 6); - pub static ref ROOT_USERID: Userid = Userid::new("root@pam".to_string(), 4); + pub static ref BACKUP_USERID: Userid = Userid::new("backup@pam".to_string(), 6, 0); + pub static ref ROOT_USERID: Userid = Userid::new("root@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 { - 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 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 for Userid { } } +#[test] +fn test_token_id() { + use std::str::FromStr; + use std::convert::TryFrom; + + let userid = Userid::new("test@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@pam!bar"); + + assert_eq!(userid, Userid::from_str("test@pam!bar").expect("parsing token from str failed")); + assert_eq!(userid, Userid::try_from("test@pam!bar".to_string()).expect("parsing token from String failed")); + + let userid = Userid::new("test@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@pam"); + + assert_eq!(userid, Userid::from_str("test@pam").expect("parsing user from str failed")); + assert_eq!(userid, Userid::try_from("test@pam".to_string()).expect("parsing user from String failed")); + + Userid::from_str("test@pam!bar@baz").expect("username with @ and ! failed"); + Userid::try_from("test@pam!bar@baz".to_string()).expect("username with @ and ! failed"); + + Userid::from_str("test@pam!").expect_err("empty token should fail"); + Userid::from_str("t€st@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@pam").expect_err("newline in username not allowed"); + Userid::from_str("tes\0@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@pam".to_string(), 4, 0); + let (name, realm) = (userid.name(), userid.realm()); + assert_eq!(userid, Userid::from((name, realm))); + + let userid = Userid::new("test@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 { 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