From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 7F8481FF15C for ; Fri, 17 Oct 2025 11:36:41 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0DDF024AF8; Fri, 17 Oct 2025 11:37:03 +0200 (CEST) Date: Fri, 17 Oct 2025 11:36:28 +0200 Message-Id: Cc: "pdm-devel" From: "Lukas Wagner" To: "Proxmox Datacenter Manager development discussion" Mime-Version: 1.0 X-Mailer: aerc 0.20.1-0-g2ecb8770224a References: <20251014143709.413690-1-s.sterz@proxmox.com> <20251014143709.413690-5-s.sterz@proxmox.com> In-Reply-To: <20251014143709.413690-5-s.sterz@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1760693784895 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.028 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pdm-devel] [PATCH datacenter-manager v2 2/3] server: access: use token endpoints from proxmox-access-control X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" LGTM! Reviewed-by: Lukas Wagner Tested-by: Lukas Wagner On Tue Oct 14, 2025 at 4:37 PM CEST, Shannon Sterz wrote: > this allows keeping this logic in one place accross products > > Signed-off-by: Shannon Sterz > --- > server/src/api/access/users.rs | 349 ++++----------------------------- > 1 file changed, 34 insertions(+), 315 deletions(-) > > diff --git a/server/src/api/access/users.rs b/server/src/api/access/users.rs > index 7b6839e..da598d8 100644 > --- a/server/src/api/access/users.rs > +++ b/server/src/api/access/users.rs > @@ -1,20 +1,16 @@ > //! User Management > > use anyhow::{bail, format_err, Error}; > -use serde::{Deserialize, Serialize}; > -use serde_json::{json, Value}; > use std::collections::HashMap; > > -use proxmox_access_control::types::{ > - ApiToken, User, UserUpdater, UserWithTokens, ENABLE_USER_SCHEMA, EXPIRE_USER_SCHEMA, > -}; > -use proxmox_access_control::{token_shadow, CachedUserInfo}; > +use proxmox_access_control::types::{ApiToken, User, UserUpdater, UserWithTokens}; > +use proxmox_access_control::CachedUserInfo; > use proxmox_router::{ApiMethod, Permission, Router, RpcEnvironment, SubdirMap}; > use proxmox_schema::api; > > use pdm_api_types::{ > - Authid, ConfigDigest, DeletableUserProperty, Tokenname, Userid, PDM_PASSWORD_SCHEMA, > - PRIV_ACCESS_MODIFY, PRIV_SYS_AUDIT, SINGLE_LINE_COMMENT_SCHEMA, > + Authid, ConfigDigest, DeletableUserProperty, Userid, PDM_PASSWORD_SCHEMA, PRIV_ACCESS_MODIFY, > + PRIV_SYS_AUDIT, > }; > > fn new_user_with_tokens(user: User) -> UserWithTokens { > @@ -382,336 +378,59 @@ pub fn delete_user(userid: Userid, digest: Option) -> Result<(), E > Ok(()) > } > > -#[api( > - input: { > - properties: { > - userid: { > - type: Userid, > - }, > - "token-name": { > - type: Tokenname, > - }, > - }, > - }, > - returns: { type: ApiToken }, > - access: { > - permission: &Permission::Or(&[ > +const API_METHOD_READ_TOKEN_WITH_ACCESS: ApiMethod = > + proxmox_access_control::api::API_METHOD_READ_TOKEN.access( > + None, > + &Permission::Or(&[ > &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false), > &Permission::UserParam("userid"), > ]), > - }, > -)] > -/// Read user's API token metadata > -pub fn read_token( > - userid: Userid, > - token_name: Tokenname, > - _info: &ApiMethod, > - rpcenv: &mut dyn RpcEnvironment, > -) -> Result { > - let (config, digest) = proxmox_access_control::user::config()?; > + ); > > - let tokenid = Authid::from((userid, Some(token_name))); > - > - rpcenv["digest"] = hex::encode(digest).into(); > - config.lookup("token", &tokenid.to_string()) > -} > - > -#[api( > - protected: true, > - input: { > - properties: { > - userid: { > - type: Userid, > - }, > - "token-name": { > - type: Tokenname, > - }, > - comment: { > - optional: true, > - schema: SINGLE_LINE_COMMENT_SCHEMA, > - }, > - enable: { > - schema: ENABLE_USER_SCHEMA, > - optional: true, > - }, > - expire: { > - schema: EXPIRE_USER_SCHEMA, > - optional: true, > - }, > - digest: { > - optional: true, > - type: ConfigDigest, > - }, > - }, > - }, > - access: { > - permission: &Permission::Or(&[ > +const API_METHOD_UPDATE_TOKEN_WITH_ACCESS: ApiMethod = > + proxmox_access_control::api::API_METHOD_UPDATE_TOKEN.access( > + None, > + &Permission::Or(&[ > &Permission::Privilege(&["access", "users"], PRIV_ACCESS_MODIFY, false), > &Permission::UserParam("userid"), > ]), > - }, > - returns: { > - description: "API token identifier + generated secret.", > - properties: { > - value: { > - type: String, > - description: "The API token secret", > - }, > - tokenid: { > - type: String, > - description: "The API token identifier", > - }, > - }, > - }, > -)] > -/// Generate a new API token with given metadata > -pub fn generate_token( > - userid: Userid, > - token_name: Tokenname, > - comment: Option, > - enable: Option, > - expire: Option, > - digest: Option, > -) -> Result { > - let _lock = proxmox_access_control::user::lock_config()?; > + ); > > - let (mut config, config_digest) = proxmox_access_control::user::config()?; > - config_digest.detect_modification(digest.as_ref())?; > - > - let tokenid = Authid::from((userid.clone(), Some(token_name.clone()))); > - let tokenid_string = tokenid.to_string(); > - > - if config.sections.contains_key(&tokenid_string) { > - bail!( > - "token '{}' for user '{}' already exists.", > - token_name.as_str(), > - userid > - ); > - } > - > - let secret = format!("{:x}", proxmox_uuid::Uuid::generate()); > - token_shadow::set_secret(&tokenid, &secret)?; > - > - let token = ApiToken { > - tokenid, > - comment, > - enable, > - expire, > - }; > - > - config.set_data(&tokenid_string, "token", &token)?; > - > - proxmox_access_control::user::save_config(&config)?; > - > - Ok(json!({ > - "tokenid": tokenid_string, > - "value": secret > - })) > -} > - > -#[api( > - protected: true, > - input: { > - properties: { > - userid: { > - type: Userid, > - }, > - "token-name": { > - type: Tokenname, > - }, > - comment: { > - optional: true, > - schema: SINGLE_LINE_COMMENT_SCHEMA, > - }, > - enable: { > - schema: ENABLE_USER_SCHEMA, > - optional: true, > - }, > - expire: { > - schema: EXPIRE_USER_SCHEMA, > - optional: true, > - }, > - digest: { > - optional: true, > - type: ConfigDigest, > - }, > - }, > - }, > - access: { > - permission: &Permission::Or(&[ > +const API_METHOD_GENERATE_TOKEN_WITH_ACCESS: ApiMethod = > + proxmox_access_control::api::API_METHOD_GENERATE_TOKEN.access( > + None, > + &Permission::Or(&[ > &Permission::Privilege(&["access", "users"], PRIV_ACCESS_MODIFY, false), > &Permission::UserParam("userid"), > ]), > - }, > -)] > -/// Update user's API token metadata > -pub fn update_token( > - userid: Userid, > - token_name: Tokenname, > - comment: Option, > - enable: Option, > - expire: Option, > - digest: Option, > -) -> Result<(), Error> { > - let _lock = proxmox_access_control::user::lock_config()?; > + ); > > - let (mut config, config_digest) = proxmox_access_control::user::config()?; > - config_digest.detect_modification(digest.as_ref())?; > - > - let tokenid = Authid::from((userid, Some(token_name))); > - let tokenid_string = tokenid.to_string(); > - > - let mut data: ApiToken = config.lookup("token", &tokenid_string)?; > - > - if let Some(comment) = comment { > - let comment = comment.trim().to_string(); > - if comment.is_empty() { > - data.comment = None; > - } else { > - data.comment = Some(comment); > - } > - } > - > - if let Some(enable) = enable { > - data.enable = if enable { None } else { Some(false) }; > - } > - > - if let Some(expire) = expire { > - data.expire = if expire > 0 { Some(expire) } else { None }; > - } > - > - config.set_data(&tokenid_string, "token", &data)?; > - > - proxmox_access_control::user::save_config(&config)?; > - > - Ok(()) > -} > - > -#[api( > - protected: true, > - input: { > - properties: { > - userid: { > - type: Userid, > - }, > - "token-name": { > - type: Tokenname, > - }, > - digest: { > - optional: true, > - type: ConfigDigest, > - }, > - }, > - }, > - access: { > - permission: &Permission::Or(&[ > +const API_METHOD_DELETE_TOKEN_WITH_ACCESS: ApiMethod = > + proxmox_access_control::api::API_METHOD_DELETE_TOKEN.access( > + None, > + &Permission::Or(&[ > &Permission::Privilege(&["access", "users"], PRIV_ACCESS_MODIFY, false), > &Permission::UserParam("userid"), > ]), > - }, > -)] > -/// Delete a user's API token > -pub fn delete_token( > - userid: Userid, > - token_name: Tokenname, > - digest: Option, > -) -> Result<(), Error> { > - let _lock = proxmox_access_control::user::lock_config()?; > + ); > > - let (mut config, config_digest) = proxmox_access_control::user::config()?; > - config_digest.detect_modification(digest.as_ref())?; > - > - let tokenid = Authid::from((userid.clone(), Some(token_name.clone()))); > - let tokenid_string = tokenid.to_string(); > - > - match config.sections.get(&tokenid_string) { > - Some(_) => { > - config.sections.remove(&tokenid_string); > - } > - None => bail!( > - "token '{}' of user '{}' does not exist.", > - token_name.as_str(), > - userid > - ), > - } > - > - token_shadow::delete_secret(&tokenid)?; > - > - proxmox_access_control::user::save_config(&config)?; > - > - Ok(()) > -} > - > -#[api( > - properties: { > - "token-name": { type: Tokenname }, > - token: { type: ApiToken }, > - } > -)] > -#[derive(Serialize, Deserialize)] > -#[serde(rename_all = "kebab-case")] > -/// A Token Entry that contains the token-name > -pub struct TokenApiEntry { > - /// The Token name > - pub token_name: Tokenname, > - #[serde(flatten)] > - pub token: ApiToken, > -} > - > -#[api( > - input: { > - properties: { > - userid: { > - type: Userid, > - }, > - }, > - }, > - returns: { > - description: "List user's API tokens (with config digest).", > - type: Array, > - items: { type: TokenApiEntry }, > - }, > - access: { > - permission: &Permission::Or(&[ > +const API_METHOD_LIST_TOKENS_WITH_ACCESS: ApiMethod = > + proxmox_access_control::api::API_METHOD_LIST_TOKENS.access( > + None, > + &Permission::Or(&[ > &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false), > &Permission::UserParam("userid"), > ]), > - }, > -)] > -/// List user's API tokens > -pub fn list_tokens( > - userid: Userid, > - _info: &ApiMethod, > - rpcenv: &mut dyn RpcEnvironment, > -) -> Result, Error> { > - let (config, digest) = proxmox_access_control::user::config()?; > - > - let list: Vec = config.convert_to_typed_array("token")?; > - > - rpcenv["digest"] = hex::encode(digest).into(); > - > - let filter_by_owner = |token: ApiToken| { > - if token.tokenid.is_token() && token.tokenid.user() == &userid { > - let token_name = token.tokenid.tokenname().unwrap().to_owned(); > - Some(TokenApiEntry { token_name, token }) > - } else { > - None > - } > - }; > - > - let res = list.into_iter().filter_map(filter_by_owner).collect(); > - > - Ok(res) > -} > + ); > > const TOKEN_ITEM_ROUTER: Router = Router::new() > - .get(&API_METHOD_READ_TOKEN) > - .put(&API_METHOD_UPDATE_TOKEN) > - .post(&API_METHOD_GENERATE_TOKEN) > - .delete(&API_METHOD_DELETE_TOKEN); > + .get(&API_METHOD_READ_TOKEN_WITH_ACCESS) > + .put(&API_METHOD_UPDATE_TOKEN_WITH_ACCESS) > + .post(&API_METHOD_GENERATE_TOKEN_WITH_ACCESS) > + .delete(&API_METHOD_DELETE_TOKEN_WITH_ACCESS); > > const TOKEN_ROUTER: Router = Router::new() > - .get(&API_METHOD_LIST_TOKENS) > + .get(&API_METHOD_LIST_TOKENS_WITH_ACCESS) > .match_all("token-name", &TOKEN_ITEM_ROUTER); > > const USER_SUBDIRS: SubdirMap = &[("token", &TOKEN_ROUTER)]; _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel