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 B75B01FF183 for ; Wed, 24 Sep 2025 16:51:43 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 1C1EFB566; Wed, 24 Sep 2025 16:52:15 +0200 (CEST) From: Shannon Sterz To: pdm-devel@lists.proxmox.com Date: Wed, 24 Sep 2025 16:51:36 +0200 Message-ID: <20250924145137.407070-8-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20250924145137.407070-1-s.sterz@proxmox.com> References: <20250924145137.407070-1-s.sterz@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1758725485542 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.057 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: [pdm-devel] [PATCH datacenter-manager 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" 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)]; -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel