From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pdm-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 7F1551FF16B for <inbox@lore.proxmox.com>; Thu, 3 Apr 2025 16:19:00 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 384523DF2; Thu, 3 Apr 2025 16:18:49 +0200 (CEST) From: Shannon Sterz <s.sterz@proxmox.com> To: pdm-devel@lists.proxmox.com Date: Thu, 3 Apr 2025 16:17:59 +0200 Message-Id: <20250403141806.402974-3-s.sterz@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250403141806.402974-1-s.sterz@proxmox.com> References: <20250403141806.402974-1-s.sterz@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.018 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 proxmox 2/4] access-control: add acl api feature X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion <pdm-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pdm-devel>, <mailto:pdm-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pdm-devel/> List-Post: <mailto:pdm-devel@lists.proxmox.com> List-Help: <mailto:pdm-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel>, <mailto:pdm-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Proxmox Datacenter Manager development discussion <pdm-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" <pdm-devel-bounces@lists.proxmox.com> this moves this commonly re-implemented api endpoints to this shared crate so they can be easily re-used. for this to function a user of the crate needs to extend the `AccessControlConfig` with the following three functions: - `acl_audit_privilege()`: returns the privilege necessary to see all acl entries - `acl_modify_privilege()`: returns the privilege necessary to edit the acl beyond a user's api token privileges - `check_acl_path()`: checks whether a path is a valid acl path in the context of the product using this crate. by default all paths are considered valid. all three provide default implementations so that users that only use the `impl` feature don't need to change anything. Signed-off-by: Shannon Sterz <s.sterz@proxmox.com> --- proxmox-access-control/Cargo.toml | 5 + proxmox-access-control/src/api.rs | 278 +++++++++++++++++++++++++++++ proxmox-access-control/src/init.rs | 22 +++ proxmox-access-control/src/lib.rs | 3 + 4 files changed, 308 insertions(+) create mode 100644 proxmox-access-control/src/api.rs diff --git a/proxmox-access-control/Cargo.toml b/proxmox-access-control/Cargo.toml index 23be7fcb..03f5c9fd 100644 --- a/proxmox-access-control/Cargo.toml +++ b/proxmox-access-control/Cargo.toml @@ -17,6 +17,7 @@ const_format.workspace = true nix = { workspace = true, optional = true } openssl = { workspace = true, optional = true } regex.workspace = true +hex = { workspace = true, optional = true } serde.workspace = true serde_json = { workspace = true, optional = true } serde_plain.workspace = true @@ -33,6 +34,10 @@ proxmox-time = { workspace = true } [features] default = [] +api = [ + "impl", + "dep:hex", +] impl = [ "dep:nix", "dep:openssl", diff --git a/proxmox-access-control/src/api.rs b/proxmox-access-control/src/api.rs new file mode 100644 index 00000000..4a6aabf5 --- /dev/null +++ b/proxmox-access-control/src/api.rs @@ -0,0 +1,278 @@ +use anyhow::{bail, format_err, Error}; + +use proxmox_auth_api::types::{Authid, PROXMOX_GROUP_ID_SCHEMA}; +use proxmox_config_digest::{ConfigDigest, PROXMOX_CONFIG_DIGEST_SCHEMA}; +use proxmox_router::{Permission, Router, RpcEnvironment}; +use proxmox_schema::api; + +use crate::acl::AclTreeNode; +use crate::init::access_conf; +use crate::types::{AclListItem, AclUgidType, ACL_PATH_SCHEMA, ACL_PROPAGATE_SCHEMA}; +use crate::CachedUserInfo; + +#[api( + input: { + properties: { + path: { + schema: ACL_PATH_SCHEMA, + optional: true, + }, + exact: { + description: "If set, returns only ACL for the exact path.", + type: bool, + optional: true, + default: false, + }, + }, + }, + returns: { + description: "ACL entry list.", + type: Array, + items: { + type: AclListItem, + } + }, + access: { + permission: &Permission::Anybody, + description: "Returns all ACLs if user has sufficient privileges on this endpoint, otherwise it is limited to the user's API tokens.", + }, +)] +/// Get ACL entries, can be filter by path. +pub fn read_acl( + path: Option<String>, + exact: bool, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<Vec<AclListItem>, Error> { + let auth_id = rpcenv + .get_auth_id() + .ok_or_else(|| format_err!("endpoint called without an auth id"))? + .parse()?; + + let top_level_privs = CachedUserInfo::new()?.lookup_privs(&auth_id, &["access", "acl"]); + + let filter = if top_level_privs & access_conf().acl_audit_privileges() == 0 { + Some(auth_id) + } else { + None + }; + + let (mut tree, digest) = crate::acl::config()?; + + let node = if let Some(path) = &path { + if let Some(node) = tree.find_node(path) { + node + } else { + return Ok(Vec::new()); + } + } else { + &tree.root + }; + + rpcenv["digest"] = hex::encode(digest).into(); + + Ok(extract_acl_node_data(node, path.as_deref(), exact, &filter)) +} + +#[api( + protected: true, + input: { + properties: { + path: { + schema: ACL_PATH_SCHEMA, + }, + role: { + type: String, + description: "Name of a role that the auth id will be granted.", + }, + propagate: { + optional: true, + schema: ACL_PROPAGATE_SCHEMA, + }, + "auth-id": { + optional: true, + type: Authid, + }, + group: { + optional: true, + schema: PROXMOX_GROUP_ID_SCHEMA, + }, + delete: { + optional: true, + description: "Remove permissions (instead of adding it).", + type: bool, + default: false, + }, + digest: { + optional: true, + schema: PROXMOX_CONFIG_DIGEST_SCHEMA, + }, + }, + }, + access: { + permission: &Permission::Anybody, + description: "Requires sufficient permissions to edit the ACL, otherwise only editing the current user's API token permissions is allowed." + }, +)] +/// Update ACL +#[allow(clippy::too_many_arguments)] +pub fn update_acl( + path: String, + role: String, + propagate: Option<bool>, + auth_id: Option<Authid>, + group: Option<String>, + delete: bool, + digest: Option<ConfigDigest>, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + let access_conf = access_conf(); + + if !access_conf.roles().contains_key(role.as_str()) { + bail!("Role does not exist, please make sure to specify a valid role!") + } + + let current_auth_id: Authid = rpcenv + .get_auth_id() + .expect("auth id could not be determined") + .parse()?; + + let user_info = CachedUserInfo::new()?; + let top_level_privs = user_info.lookup_privs(¤t_auth_id, &["access", "acl"]); + + if top_level_privs & access_conf.acl_modify_privileges() == 0 { + if group.is_some() { + bail!("Unprivileged users are not allowed to create group ACL item."); + } + + match &auth_id { + Some(auth_id) => { + if current_auth_id.is_token() { + bail!("Unprivileged API tokens can't set ACL items."); + } else if !auth_id.is_token() { + bail!("Unprivileged users can only set ACL items for API tokens."); + } else if auth_id.user() != current_auth_id.user() { + bail!("Unprivileged users can only set ACL items for their own API tokens."); + } + } + None => { + bail!("Unprivileged user needs to provide auth_id to update ACL item."); + } + }; + } + + // FIXME: add support for group + if group.is_some() { + bail!("parameter 'group' - groups are currently not supported"); + } else if let Some(auth_id) = &auth_id { + // only allow deleting non-existing auth id's, not adding them + if !delete { + let exists = crate::user::cached_config()? + .sections + .contains_key(&auth_id.to_string()); + + if !exists { + if auth_id.is_token() { + bail!("no such API token"); + } else { + bail!("no such user.") + } + } + } + } else { + // FIXME: suggest groups here once they exist + bail!("missing 'userid' parameter"); + } + + // allow deleting invalid acl paths + if !delete { + access_conf.check_acl_path(&path)?; + } + + let _guard = crate::acl::lock_config()?; + let (mut tree, expected_digest) = crate::acl::config()?; + expected_digest.detect_modification(digest.as_ref())?; + + let propagate = propagate.unwrap_or(true); + + if let Some(auth_id) = &auth_id { + if delete { + tree.delete_user_role(&path, auth_id, &role); + } else { + tree.insert_user_role(&path, auth_id, &role, propagate); + } + } else if let Some(group) = &group { + if delete { + tree.delete_group_role(&path, group, &role); + } else { + tree.insert_group_role(&path, group, &role, propagate); + } + } + + crate::acl::save_config(&tree)?; + + Ok(()) +} + +fn extract_acl_node_data( + node: &AclTreeNode, + path: Option<&str>, + exact: bool, + auth_id_filter: &Option<Authid>, +) -> Vec<AclListItem> { + // tokens can't have tokens, so we can early return + if let Some(auth_id_filter) = auth_id_filter { + if auth_id_filter.is_token() { + return Vec::new(); + } + } + + let mut list = Vec::new(); + let path_str = path.unwrap_or("/"); + + for (user, roles) in &node.users { + if let Some(auth_id_filter) = auth_id_filter { + if !user.is_token() || user.user() != auth_id_filter.user() { + continue; + } + } + + for (role, propagate) in roles { + list.push(AclListItem { + path: path_str.to_owned(), + propagate: *propagate, + ugid_type: AclUgidType::User, + ugid: user.to_string(), + roleid: role.to_string(), + }); + } + } + + for (group, roles) in &node.groups { + if auth_id_filter.is_some() { + continue; + } + + for (role, propagate) in roles { + list.push(AclListItem { + path: path_str.to_owned(), + propagate: *propagate, + ugid_type: AclUgidType::Group, + ugid: group.to_string(), + roleid: role.to_string(), + }); + } + } + + if !exact { + list.extend(node.children.iter().flat_map(|(comp, child)| { + let new_path = format!("{}/{comp}", path.unwrap_or("")); + extract_acl_node_data(child, Some(&new_path), exact, auth_id_filter) + })); + } + + list +} + +pub const ACL_ROUTER: Router = Router::new() + .get(&API_METHOD_READ_ACL) + .put(&API_METHOD_UPDATE_ACL); diff --git a/proxmox-access-control/src/init.rs b/proxmox-access-control/src/init.rs index b0cf1a3e..a6d36780 100644 --- a/proxmox-access-control/src/init.rs +++ b/proxmox-access-control/src/init.rs @@ -72,6 +72,28 @@ pub trait AccessControlConfig: Send + Sync { let _ = config; Ok(()) } + + /// This is used to determined what access control list entries a user is allowed to read. + /// + /// Override this if you want to use the `api` feature. + fn acl_audit_privileges(&self) -> u64 { + 0 + } + + /// This is used to determine what privileges are needed to modify the access control list. + /// + /// Override this if you want to use the `api` feature. + fn acl_modify_privileges(&self) -> u64 { + 0 + } + + /// Used to determine which paths are valid in a given `AclTree`. + /// + /// Override this if you want to use the `api` feature. + fn check_acl_path(&self, path: &str) -> Result<(), Error> { + let _ = path; + Ok(()) + } } pub fn init<P: AsRef<Path>>( diff --git a/proxmox-access-control/src/lib.rs b/proxmox-access-control/src/lib.rs index c3aeb9db..62683924 100644 --- a/proxmox-access-control/src/lib.rs +++ b/proxmox-access-control/src/lib.rs @@ -5,6 +5,9 @@ pub mod types; #[cfg(feature = "impl")] pub mod acl; +#[cfg(feature = "api")] +pub mod api; + #[cfg(feature = "impl")] pub mod init; -- 2.39.5 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel