From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup 01/16] api: add Authid as wrapper around Userid
Date: Wed, 28 Oct 2020 12:36:23 +0100 [thread overview]
Message-ID: <20201028113632.814586-3-f.gruenbichler@proxmox.com> (raw)
In-Reply-To: <20201028113632.814586-1-f.gruenbichler@proxmox.com>
with an optional Tokenname, appended with '!' as delimiter in the string
representation like for PVE.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@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@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 _ = <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@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<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@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
next prev parent reply other threads:[~2020-10-28 11:36 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-10-28 11:36 [pbs-devel] [PATCH proxmox-backup 00/16] API tokens Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-widget-toolkit] add PermissionView Fabian Grünbichler
2020-10-28 16:18 ` [pbs-devel] applied: " Thomas Lamprecht
2020-10-28 11:36 ` Fabian Grünbichler [this message]
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox] rpcenv: rename user to auth_id Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 02/16] config: add token.shadow file Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 03/16] replace Userid with Authid Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 04/16] REST: extract and handle API tokens Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 05/16] api: add API token endpoints Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 06/16] api: allow listing users + tokens Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 07/16] api: add permissions endpoint Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 08/16] client/remote: allow using ApiToken + secret Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 09/16] owner checks: handle backups owned by API tokens Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 10/16] tasks: allow unpriv users to read their tokens' tasks Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 11/16] manager: add token commands Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 12/16] manager: add user permissions command Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 13/16] gui: add permissions button to user view Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 14/16] gui: add API token UI Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 15/16] acls: allow viewing/editing user's token ACLs Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 16/16] gui: add API " Fabian Grünbichler
2020-10-29 14:23 ` [pbs-devel] applied: [PATCH proxmox-backup 00/16] API tokens Wolfgang Bumiller
2020-10-29 19:50 ` [pbs-devel] " Thomas Lamprecht
2020-10-30 8:03 ` Fabian Grünbichler
2020-10-30 8:48 ` Thomas Lamprecht
2020-10-30 9:55 ` Fabian Grünbichler
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20201028113632.814586-3-f.gruenbichler@proxmox.com \
--to=f.gruenbichler@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.