From: Shannon Sterz <s.sterz@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox v3 2/6] access-control: add acl api feature
Date: Thu, 28 Aug 2025 12:59:03 +0200 [thread overview]
Message-ID: <20250828105912.294887-3-s.sterz@proxmox.com> (raw)
In-Reply-To: <20250828105912.294887-1-s.sterz@proxmox.com>
move these commonly re-implemented api endpoints to this shared crate
so they can be easily re-used.
a user of the crate will need 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 tree 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 cd0f9daf..664096b5 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.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-08-28 10:59 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-08-28 10:59 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/11] ACL edit api and ui components Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH proxmox v3 1/6] access-control: add more types to prepare for api feature Shannon Sterz
2025-08-28 10:59 ` Shannon Sterz [this message]
2025-08-28 10:59 ` [pdm-devel] [PATCH proxmox v3 3/6] access-control: add comments to roles function of AccessControlConfig Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH proxmox v3 4/6] access-control: add generic roles endpoint to `api` feature Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH proxmox v3 5/6] access-control: api: refactor validation checks to re-use existing code Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH proxmox v3 6/6] access-control: api: refactor extract_acl_node_data to be non-recursive Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH yew-comp v3 1/3] api-types/role_selector: depend on common `RoleInfo` type Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH yew-comp v3 2/3] acl: add a view and semi-generic `EditWindow` for acl entries Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH yew-comp v3 3/3] role_selector/acl_edit: make api endpoint and default role configurable Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH datacenter-manager v3 1/2] server: use proxmox-access-control api implementations Shannon Sterz
2025-08-28 10:59 ` [pdm-devel] [PATCH datacenter-manager v3 2/2] ui: configuration: add panel for viewing and editing acl entries Shannon Sterz
2025-08-28 21:15 ` [pdm-devel] applied-series: [PATCH datacenter-manager/proxmox/yew-comp v3 00/11] ACL edit api and ui components Thomas Lamprecht
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=20250828105912.294887-3-s.sterz@proxmox.com \
--to=s.sterz@proxmox.com \
--cc=pdm-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox