* [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components
@ 2025-04-11 13:44 Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 1/6] access-control: add more types to prepare for api feature Shannon Sterz
` (11 more replies)
0 siblings, 12 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
this series aims to make more parts of our access control list
implementation re-usable between products. in a first step most of the
relevant api endpoints and api types are moved to
`proxmox-access-control`. this is done by adding a new `api` feature
that includes the necessary api endpoints. the `AccessControlConfig`
trait is also expanded to make the api endpoints more adaptable to
different products. by providing default implementations for the newly
added trait functions existing users don't need to change anything. it
also tries to make the code here easier to understand as the checks
could be hard to grasp previously.
next the series adds components to proxmox-yew-comp to provide a panel
for inspecting the current acl and adding or removing entries. this is
done by using the existing `RoleSelector` and `AuthidSelector`
components. the later is also slightly adapted to make it possible to
change the api endpoint that roles are fetched from as well as the
default role. the `AclView` component allows users of the crate to add
more options for adding ACL entries. meaning they can configure distinct
components for adding user, token or group permissions. this is done in
a generic fashion so that extending this menu does not require changing
the component again.
finally proxmox-datacenter-manager is adapted to use the new api
endpoints in `proxmox-access-control` and a permissions panel is
implemented. note that this would benefit from some clean-up once
permission path and such are cleaned up.
proxmox:
Shannon Sterz (6):
access-control: add more types to prepare for api feature
access-control: add acl api feature
access-control: add comments to roles function of AccessControlConfig
access-control: add generic roles endpoint to `api` feature
access-control: api: refactor validation checks to re-use existing
code
access-control: api: refactor extract_acl_node_data to be
non-recursive
proxmox-access-control/Cargo.toml | 8 +
proxmox-access-control/src/acl.rs | 12 +-
proxmox-access-control/src/api.rs | 349 ++++++++++++++++++
.../src/cached_user_info.rs | 4 +-
proxmox-access-control/src/init.rs | 35 +-
proxmox-access-control/src/lib.rs | 3 +
proxmox-access-control/src/types.rs | 87 ++++-
7 files changed, 486 insertions(+), 12 deletions(-)
create mode 100644 proxmox-access-control/src/api.rs
proxmox-yew-comp:
Shannon Sterz (3):
api-types/role_selector: depend on common `RoleInfo` type
acl: add a view and semi-generic `EditWindow` for acl entries
role_selector/acl_edit: make api endpoint and default role
configurable
src/acl/acl_edit.rs | 112 +++++++++++++++++
src/acl/acl_view.rs | 270 ++++++++++++++++++++++++++++++++++++++++
src/acl/mod.rs | 5 +
src/common_api_types.rs | 8 --
src/lib.rs | 3 +
src/role_selector.rs | 22 +++-
6 files changed, 407 insertions(+), 13 deletions(-)
create mode 100644 src/acl/acl_edit.rs
create mode 100644 src/acl/acl_view.rs
create mode 100644 src/acl/mod.rs
proxmox-datacenter-manager:
Shannon Sterz (2):
server: use proxmox-access-control api implementations
ui: configuration: add panel for viewing and editing acl entries
server/Cargo.toml | 2 +-
server/src/acl.rs | 102 ++++-
server/src/api/access/acl.rs | 357 ------------------
server/src/api/access/mod.rs | 4 +-
ui/src/configuration/mod.rs | 23 +-
.../configuration/permission_path_selector.rs | 86 +++++
6 files changed, 208 insertions(+), 366 deletions(-)
delete mode 100644 server/src/api/access/acl.rs
create mode 100644 ui/src/configuration/permission_path_selector.rs
Summary over all repositories:
19 files changed, 1101 insertions(+), 391 deletions(-)
--
Generated by git-murpp 0.8.0
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH proxmox v2 1/6] access-control: add more types to prepare for api feature
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 2/6] access-control: add acl " Shannon Sterz
` (10 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
this includes:
- `ACL_PATH_SCHEMA`: describes the format of valid acl paths
- `ACL_PROPAGATE_SCHEMA`: describes whether an acl entry propagates to
its child paths
- `AclUgidType`: which type an acl entry refers to, either a user or a
group
- `AclListItem`: describes an entry of the ACL
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
this was agregated from pdm and pbs:
- pdm: lib/pdm-api-types/src/acl.rs
- pbs: proxmox/pbs-api-types/src/acl.rs
proxmox-access-control/Cargo.toml | 3 ++
proxmox-access-control/src/types.rs | 59 ++++++++++++++++++++++++++++-
2 files changed, 60 insertions(+), 2 deletions(-)
diff --git a/proxmox-access-control/Cargo.toml b/proxmox-access-control/Cargo.toml
index 9c355344..23be7fcb 100644
--- a/proxmox-access-control/Cargo.toml
+++ b/proxmox-access-control/Cargo.toml
@@ -13,10 +13,13 @@ rust-version.workspace = true
[dependencies]
anyhow.workspace = true
+const_format.workspace = true
nix = { workspace = true, optional = true }
openssl = { workspace = true, optional = true }
+regex.workspace = true
serde.workspace = true
serde_json = { workspace = true, optional = true }
+serde_plain.workspace = true
proxmox-auth-api = { workspace = true, features = [ "api-types" ] }
proxmox-config-digest = { workspace = true, optional = true, features = [ "openssl" ] }
diff --git a/proxmox-access-control/src/types.rs b/proxmox-access-control/src/types.rs
index ae6de7cf..01d078de 100644
--- a/proxmox-access-control/src/types.rs
+++ b/proxmox-access-control/src/types.rs
@@ -1,10 +1,12 @@
use serde::{Deserialize, Serialize};
+use const_format::concatcp;
+
use proxmox_auth_api::types::{Authid, Userid, PROXMOX_TOKEN_ID_SCHEMA};
use proxmox_schema::{
api,
- api_types::{COMMENT_SCHEMA, SINGLE_LINE_COMMENT_FORMAT},
- BooleanSchema, IntegerSchema, Schema, StringSchema, Updater,
+ api_types::{COMMENT_SCHEMA, SAFE_ID_REGEX_STR, SINGLE_LINE_COMMENT_FORMAT},
+ const_regex, ApiStringFormat, BooleanSchema, IntegerSchema, Schema, StringSchema, Updater,
};
pub const ENABLE_USER_SCHEMA: Schema = BooleanSchema::new(
@@ -38,6 +40,23 @@ pub const EMAIL_SCHEMA: Schema = StringSchema::new("E-Mail Address.")
.max_length(64)
.schema();
+const_regex! {
+ pub ACL_PATH_REGEX = concatcp!(r"^(?:/|", r"(?:/", SAFE_ID_REGEX_STR, ")+", r")$");
+}
+
+pub const ACL_PATH_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&ACL_PATH_REGEX);
+
+pub const ACL_PATH_SCHEMA: Schema = StringSchema::new("Access control path.")
+ .format(&ACL_PATH_FORMAT)
+ .min_length(1)
+ .max_length(128)
+ .schema();
+
+pub const ACL_PROPAGATE_SCHEMA: Schema =
+ BooleanSchema::new("Allow to propagate (inherit) permissions.")
+ .default(true)
+ .schema();
+
#[api(
properties: {
user: {
@@ -192,3 +211,39 @@ impl User {
true
}
}
+
+#[api]
+/// Type of the 'ugid' property in the ACL entry list.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, Hash)]
+#[serde(rename_all = "lowercase")]
+pub enum AclUgidType {
+ /// An entry for a user (or token).
+ User,
+ /// An entry for a group.
+ Group,
+}
+
+serde_plain::derive_display_from_serialize!(AclUgidType);
+serde_plain::derive_fromstr_from_deserialize!(AclUgidType);
+
+#[api(
+ properties: {
+ propagate: { schema: ACL_PROPAGATE_SCHEMA, },
+ path: { schema: ACL_PATH_SCHEMA, },
+ ugid_type: { type: AclUgidType },
+ ugid: {
+ type: String,
+ description: "User or Group ID.",
+ },
+ }
+)]
+#[derive(Serialize, Deserialize, PartialEq, Clone, Hash)]
+/// Access control list entry.
+pub struct AclListItem {
+ pub path: String,
+ pub ugid: String,
+ pub ugid_type: AclUgidType,
+ pub propagate: bool,
+ /// A role represented as a string.
+ pub roleid: String,
+}
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH proxmox v2 2/6] access-control: add acl api feature
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 1/6] access-control: add more types to prepare for api feature Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 3/6] access-control: add comments to roles function of AccessControlConfig Shannon Sterz
` (9 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
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>
---
this was moved and adapted from pdm & pbs:
- pdm: server/src/api/access/acl.rs
- pbs: src/api2/access/acl.rs
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
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH proxmox v2 3/6] access-control: add comments to roles function of AccessControlConfig
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 1/6] access-control: add more types to prepare for api feature Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 2/6] access-control: add acl " Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 4/6] access-control: add generic roles endpoint to `api` feature Shannon Sterz
` (8 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
this will allow us to implement a generic `roles()` endpoint similar
to the one proxmox backup server already has at `/access/roles` and
that proxmox-yew-comp's `RoleSelector` already relies on.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
this was moved from pbs:
- pbs: /src/api2/access/role.rs
proxmox-access-control/src/acl.rs | 12 ++++++------
proxmox-access-control/src/cached_user_info.rs | 4 ++--
proxmox-access-control/src/init.rs | 5 +++--
3 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/proxmox-access-control/src/acl.rs b/proxmox-access-control/src/acl.rs
index b8041688..270292ac 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -647,11 +647,11 @@ mod test {
#[derive(Debug)]
struct TestAcmConfig<'a> {
- roles: HashMap<&'a str, u64>,
+ roles: HashMap<&'a str, (u64, &'a str)>,
}
impl AccessControlConfig for TestAcmConfig<'_> {
- fn roles(&self) -> &HashMap<&str, u64> {
+ fn roles(&self) -> &HashMap<&str, (u64, &str)> {
&self.roles
}
@@ -672,10 +672,10 @@ mod test {
static ACL_CONFIG: OnceLock<TestAcmConfig> = OnceLock::new();
let config = ACL_CONFIG.get_or_init(|| {
let mut roles = HashMap::new();
- roles.insert("NoAccess", 0);
- roles.insert("Admin", u64::MAX);
- roles.insert("DatastoreBackup", 4);
- roles.insert("DatastoreReader", 8);
+ roles.insert("NoAccess", (0, "comment"));
+ roles.insert("Admin", (u64::MAX, "comment"));
+ roles.insert("DatastoreBackup", (4, "comment"));
+ roles.insert("DatastoreReader", (8, "comment"));
TestAcmConfig { roles }
});
diff --git a/proxmox-access-control/src/cached_user_info.rs b/proxmox-access-control/src/cached_user_info.rs
index 4d011f00..f5ed2858 100644
--- a/proxmox-access-control/src/cached_user_info.rs
+++ b/proxmox-access-control/src/cached_user_info.rs
@@ -150,7 +150,7 @@ impl CachedUserInfo {
if self.is_superuser(auth_id) {
let acm_config = access_conf();
if let Some(admin) = acm_config.role_admin() {
- if let Some(admin) = acm_config.roles().get(admin) {
+ if let Some((admin, _)) = acm_config.roles().get(admin) {
return (*admin, *admin);
}
}
@@ -160,7 +160,7 @@ impl CachedUserInfo {
let mut privs: u64 = 0;
let mut propagated_privs: u64 = 0;
for (role, propagate) in roles {
- if let Some(role_privs) = access_conf().roles().get(role.as_str()) {
+ if let Some((role_privs, _)) = access_conf().roles().get(role.as_str()) {
if propagate {
propagated_privs |= role_privs;
}
diff --git a/proxmox-access-control/src/init.rs b/proxmox-access-control/src/init.rs
index a6d36780..c219a78e 100644
--- a/proxmox-access-control/src/init.rs
+++ b/proxmox-access-control/src/init.rs
@@ -16,8 +16,9 @@ pub trait AccessControlConfig: Send + Sync {
/// Returns a mapping of all recognized privileges and their corresponding `u64` value.
fn privileges(&self) -> &HashMap<&str, u64>;
- /// Returns a mapping of all recognized roles and their corresponding `u64` value.
- fn roles(&self) -> &HashMap<&str, u64>;
+ /// Returns a mapping of all recognized roles and their corresponding `u64` value as well as
+ /// a comment.
+ fn roles(&self) -> &HashMap<&str, (u64, &str)>;
/// Checks whether an `Authid` has super user privileges or not.
///
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH proxmox v2 4/6] access-control: add generic roles endpoint to `api` feature
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (2 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 3/6] access-control: add comments to roles function of AccessControlConfig Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 5/6] access-control: api: refactor validation checks to re-use existing code Shannon Sterz
` (7 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
since this is always the same between most products and we already
have access to all the relevant information
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
the api type was moved from proxmox-yew-comp and the api endpoint from
pbs:
- proxmox-yew-comp: src/common_api_types.rs
- pbs: src/api2/access/role.rs
proxmox-access-control/src/api.rs | 43 ++++++++++++++++++++++++++++-
proxmox-access-control/src/types.rs | 28 +++++++++++++++++++
2 files changed, 70 insertions(+), 1 deletion(-)
diff --git a/proxmox-access-control/src/api.rs b/proxmox-access-control/src/api.rs
index 4a6aabf5..bb872b97 100644
--- a/proxmox-access-control/src/api.rs
+++ b/proxmox-access-control/src/api.rs
@@ -7,7 +7,7 @@ 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::types::{AclListItem, AclUgidType, RoleInfo, ACL_PATH_SCHEMA, ACL_PROPAGATE_SCHEMA};
use crate::CachedUserInfo;
#[api(
@@ -276,3 +276,44 @@ fn extract_acl_node_data(
pub const ACL_ROUTER: Router = Router::new()
.get(&API_METHOD_READ_ACL)
.put(&API_METHOD_UPDATE_ACL);
+
+#[api(
+ returns: {
+ description: "List of roles.",
+ type: Array,
+ items: {
+ type: RoleInfo,
+ }
+ },
+ access: {
+ permission: &Permission::Anybody,
+ }
+)]
+/// A list of available roles
+fn list_roles() -> Result<Vec<RoleInfo>, Error> {
+ let list = access_conf()
+ .roles()
+ .iter()
+ .map(|(role, (privs, comment))| {
+ let priv_list = access_conf()
+ .privileges()
+ .iter()
+ .filter_map(|(name, privilege)| {
+ if privs & privilege > 0 {
+ Some(name.to_string())
+ } else {
+ None
+ }
+ });
+
+ RoleInfo {
+ roleid: role.to_string(),
+ privs: priv_list.collect(),
+ comment: Some(comment.to_string()),
+ }
+ });
+
+ Ok(list.collect())
+}
+
+pub const ROLE_ROUTER: Router = Router::new().get(&API_METHOD_LIST_ROLES);
diff --git a/proxmox-access-control/src/types.rs b/proxmox-access-control/src/types.rs
index 01d078de..ea64d333 100644
--- a/proxmox-access-control/src/types.rs
+++ b/proxmox-access-control/src/types.rs
@@ -247,3 +247,31 @@ pub struct AclListItem {
/// A role represented as a string.
pub roleid: String,
}
+
+#[api(
+ properties: {
+ privs: {
+ type: Array,
+ description: "List of Privileges",
+ items: {
+ type: String,
+ description: "A Privilege",
+ },
+ },
+ comment: {
+ schema: COMMENT_SCHEMA,
+ optional: true,
+ }
+ }
+)]
+/// A struct that the describes a role and shows the associated privileges.
+#[derive(Serialize, Deserialize, PartialEq, Clone)]
+pub struct RoleInfo {
+ /// The id of the role
+ pub roleid: String,
+ /// The privileges the role holds
+ pub privs: Vec<String>,
+ /// A comment describing the role
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub comment: Option<String>,
+}
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH proxmox v2 5/6] access-control: api: refactor validation checks to re-use existing code
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (3 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 4/6] access-control: add generic roles endpoint to `api` feature Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 6/6] access-control: api: refactor extract_acl_node_data to be non-recursive Shannon Sterz
` (6 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
also makes sure to make them easier to understand when auditing the
code. as these checks are fairly complex to understand otherwise. it
also make partial matches configurable through the
AccessControlConfig.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
proxmox-access-control/src/api.rs | 85 +++++++++++++++++++-----------
proxmox-access-control/src/init.rs | 8 +++
2 files changed, 63 insertions(+), 30 deletions(-)
diff --git a/proxmox-access-control/src/api.rs b/proxmox-access-control/src/api.rs
index bb872b97..bf3ffb21 100644
--- a/proxmox-access-control/src/api.rs
+++ b/proxmox-access-control/src/api.rs
@@ -48,13 +48,17 @@ pub fn read_acl(
.ok_or_else(|| format_err!("endpoint called without an auth id"))?
.parse()?;
- let top_level_privs = CachedUserInfo::new()?.lookup_privs(&auth_id, &["access", "acl"]);
+ // if a user does not have enough permissions to see all entries, we need to filter them
+ let filter_entries = CachedUserInfo::new()?
+ .check_privs(
+ &auth_id,
+ &["access", "acl"],
+ access_conf().acl_audit_privileges(),
+ access_conf().allow_partial_permission_match(),
+ )
+ .is_err();
- let filter = if top_level_privs & access_conf().acl_audit_privileges() == 0 {
- Some(auth_id)
- } else {
- None
- };
+ let filter = if filter_entries { Some(auth_id) } else { None };
let (mut tree, digest) = crate::acl::config()?;
@@ -136,34 +140,52 @@ pub fn update_acl(
.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"]);
+ let unprivileged_user = CachedUserInfo::new()?
+ .check_privs(
+ ¤t_auth_id,
+ &["access", "acl"],
+ access_conf.acl_modify_privileges(),
+ access_conf.allow_partial_permission_match(),
+ )
+ .is_err();
- 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.");
+ if unprivileged_user {
+ if group.is_none()
+ && !current_auth_id.is_token()
+ && auth_id
+ .as_ref()
+ .map(|id| id.is_token() && current_auth_id.user() == id.user())
+ .unwrap_or_default()
+ {
+ // a user is directly editing the privileges of their own tokens, this is always
+ // allowed
+ } else {
+ if group.is_some() {
+ bail!("Unprivileged users are not allowed to create group ACL item.");
+ }
+
+ let auth_id = auth_id.as_ref().ok_or_else(|| {
+ format_err!("Unprivileged user needs to provide auth_id to update ACL item.")
+ })?;
+
+ if current_auth_id.is_token() {
+ bail!("Unprivileged API tokens can't set ACL items.");
+ }
+
+ if !auth_id.is_token() {
+ bail!("Unprivileged users can only set ACL items for API tokens.");
+ }
+
+ if current_auth_id.user() != auth_id.user() {
+ bail!("Unprivileged users can only set ACL items for their own API tokens.");
+ }
+
+ // this should not be reachable, but just in case, bail here
+ bail!("Unprivileged user is trying to set an invalid 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 {
+ 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()?
@@ -178,6 +200,9 @@ pub fn update_acl(
}
}
}
+ } else if group.is_some() {
+ // FIXME: add support for groups
+ bail!("parameter 'group' - groups are currently not supported");
} else {
// FIXME: suggest groups here once they exist
bail!("missing 'userid' parameter");
diff --git a/proxmox-access-control/src/init.rs b/proxmox-access-control/src/init.rs
index c219a78e..39a12352 100644
--- a/proxmox-access-control/src/init.rs
+++ b/proxmox-access-control/src/init.rs
@@ -95,6 +95,14 @@ pub trait AccessControlConfig: Send + Sync {
let _ = path;
Ok(())
}
+
+ /// Whether the API endpoints to inspect the ACL should use partial permission matching or not.
+ ///
+ /// Override this if the product in question uses more than one bit to specify permissions (so,
+ /// in case it is *not* using a bitmap) and the match between permissions needs to be exact.
+ fn allow_partial_permission_match(&self) -> bool {
+ true
+ }
}
pub fn init<P: AsRef<Path>>(
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH proxmox v2 6/6] access-control: api: refactor extract_acl_node_data to be non-recursive
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (4 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 5/6] access-control: api: refactor validation checks to re-use existing code Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 1/3] api-types/role_selector: depend on common `RoleInfo` type Shannon Sterz
` (5 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
this should help reduce the memory foot print of this helper when
parsing deeply nested acl trees, as only one vector for return values
and no addtional stack frames are allocated.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
proxmox-access-control/src/api.rs | 81 ++++++++++++++++---------------
1 file changed, 43 insertions(+), 38 deletions(-)
diff --git a/proxmox-access-control/src/api.rs b/proxmox-access-control/src/api.rs
index bf3ffb21..0194d517 100644
--- a/proxmox-access-control/src/api.rs
+++ b/proxmox-access-control/src/api.rs
@@ -251,51 +251,56 @@ fn extract_acl_node_data(
}
}
- let mut list = Vec::new();
- let path_str = path.unwrap_or("/");
+ let mut to_return = Vec::new();
+ let mut nodes = vec![(path.unwrap_or("").to_string(), node)];
- 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;
+ while let Some((path, node)) = nodes.pop() {
+ let path_str = if path.is_empty() { "/" } else { &path };
+
+ 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 {
+ to_return.push(AclListItem {
+ path: path_str.to_owned(),
+ propagate: *propagate,
+ ugid_type: AclUgidType::User,
+ ugid: user.to_string(),
+ roleid: role.to_string(),
+ });
}
}
- 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 {
+ to_return.push(AclListItem {
+ path: path_str.to_owned(),
+ propagate: *propagate,
+ ugid_type: AclUgidType::Group,
+ ugid: group.to_string(),
+ roleid: role.to_string(),
+ });
+ }
+ }
+
+ if !exact {
+ nodes.extend(
+ node.children
+ .iter()
+ .map(|(comp, child)| (format!("{}/{comp}", path), child)),
+ );
}
}
- 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
+ to_return
}
pub const ACL_ROUTER: Router = Router::new()
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 1/3] api-types/role_selector: depend on common `RoleInfo` type
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (5 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 6/6] access-control: api: refactor extract_acl_node_data to be non-recursive Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 2/3] acl: add a view and semi-generic `EditWindow` for acl entries Shannon Sterz
` (4 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
instead of redefining it here. this should make sure that we stay in
sync with the common api definition in `proxmox-access-control`.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/common_api_types.rs | 8 --------
src/role_selector.rs | 2 +-
2 files changed, 1 insertion(+), 9 deletions(-)
diff --git a/src/common_api_types.rs b/src/common_api_types.rs
index 58aec58..7b22bb3 100644
--- a/src/common_api_types.rs
+++ b/src/common_api_types.rs
@@ -24,14 +24,6 @@ impl ExtractPrimaryKey for BasicRealmInfo {
}
}
-#[derive(Serialize, Deserialize, PartialEq, Clone)]
-pub struct RoleInfo {
- pub roleid: String,
- pub privs: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub comment: Option<String>,
-}
-
// copied from pbs_api_types::TaskListItem;
#[derive(Serialize, Deserialize, Clone, PartialEq)]
/// Task properties.
diff --git a/src/role_selector.rs b/src/role_selector.rs
index 7bde09f..20a8483 100644
--- a/src/role_selector.rs
+++ b/src/role_selector.rs
@@ -10,7 +10,7 @@ use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
use pwt::widget::form::{Selector, SelectorRenderArgs, ValidateFn};
use pwt::widget::GridPicker;
-use crate::common_api_types::RoleInfo;
+use proxmox_access_control::types::RoleInfo;
thread_local! {
static COLUMNS: Rc<Vec<DataTableHeader<RoleInfo>>> = Rc::new(vec![
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 2/3] acl: add a view and semi-generic `EditWindow` for acl entries
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (6 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 1/3] api-types/role_selector: depend on common `RoleInfo` type Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 3/3] role_selector/acl_edit: make api endpoint and default role configurable Shannon Sterz
` (3 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
since each product will always have different acl paths, editing them
cannot be made completelly generic. however, this does try to make
displaying them generic based on the common api implementations in
`proxmox-access-control`.
furthermore, a the `AclEditWindow` trait makes it possible for each
product to adapt the editing panels and the number of them that are
displayed in the ACL View configurable. allowing for greater
flexibility while trying to reduce duplicate code accross multiple
products.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/acl/acl_edit.rs | 92 +++++++++++++++
src/acl/acl_view.rs | 270 ++++++++++++++++++++++++++++++++++++++++++++
src/acl/mod.rs | 5 +
src/lib.rs | 3 +
4 files changed, 370 insertions(+)
create mode 100644 src/acl/acl_edit.rs
create mode 100644 src/acl/acl_view.rs
create mode 100644 src/acl/mod.rs
diff --git a/src/acl/acl_edit.rs b/src/acl/acl_edit.rs
new file mode 100644
index 0000000..b8bbd89
--- /dev/null
+++ b/src/acl/acl_edit.rs
@@ -0,0 +1,92 @@
+use yew::html::IntoPropValue;
+
+use pwt::prelude::*;
+use pwt::widget::form::{Checkbox, FormContext};
+use pwt::widget::{FieldLabel, InputPanel};
+
+use pwt_macros::builder;
+
+use crate::EditWindow;
+use crate::{AuthidSelector, RoleSelector};
+
+pub trait AclEditWindow: Into<EditWindow> {}
+
+#[derive(Clone, PartialEq, Properties)]
+#[builder]
+pub struct AclEdit {
+ /// Use API Tokens instead of Users.
+ #[prop_or_default]
+ #[builder]
+ use_tokens: bool,
+
+ /// The endpoint which will be used to create new ACL entries via a PUT request.
+ #[prop_or(String::from("/access/acl"))]
+ #[builder(IntoPropValue, into_prop_value)]
+ acl_api_endpoint: String,
+
+ #[prop_or_default]
+ input_panel: InputPanel,
+}
+
+impl AclEdit {
+ /// Create a new `AclEdit` that takes as input a field and its label which are used to select
+ /// the ACL path for a new ACL entry.
+ pub fn new(
+ path_selector_label: impl Into<FieldLabel>,
+ path_selector: impl FieldBuilder,
+ ) -> Self {
+ let path_selector = path_selector.name("path").required(true);
+ let input_panel = InputPanel::new().with_field(path_selector_label, path_selector);
+ yew::props!(Self { input_panel })
+ }
+}
+
+impl From<AclEdit> for EditWindow {
+ fn from(value: AclEdit) -> Self {
+ let field = AuthidSelector::new().name("auth-id").required(true);
+
+ let (title, authid_label, authid_field) = if value.use_tokens {
+ (
+ tr!("API Token Permission"),
+ tr!("API Token"),
+ field.include_users(false),
+ )
+ } else {
+ (
+ tr!("User Permission"),
+ tr!("User"),
+ field.include_tokens(false),
+ )
+ };
+
+ let input_panel = value
+ .input_panel
+ .clone()
+ .padding(4)
+ .with_field(authid_label, authid_field)
+ .with_field(tr!("Role"), RoleSelector::new().name("role").required(true))
+ .with_field(
+ tr!("Propagate"),
+ Checkbox::new().name("propagate").required(true),
+ );
+
+ let url = value.acl_api_endpoint.to_owned();
+
+ let on_submit = {
+ let url = url.clone();
+ move |form_ctx: FormContext| {
+ let url = url.clone();
+ async move {
+ let data = form_ctx.get_submit_data();
+ crate::http_put(url.as_str(), Some(data)).await
+ }
+ }
+ };
+
+ EditWindow::new(title)
+ .renderer(move |_form_ctx: &FormContext| input_panel.clone().into())
+ .on_submit(on_submit)
+ }
+}
+
+impl AclEditWindow for AclEdit {}
diff --git a/src/acl/acl_view.rs b/src/acl/acl_view.rs
new file mode 100644
index 0000000..58da3fd
--- /dev/null
+++ b/src/acl/acl_view.rs
@@ -0,0 +1,270 @@
+use std::borrow::BorrowMut;
+use std::future::Future;
+use std::pin::Pin;
+use std::rc::Rc;
+
+use anyhow::Error;
+use indexmap::IndexMap;
+use serde_json::json;
+
+use yew::html::IntoPropValue;
+use yew::virtual_dom::{Key, VComp, VNode};
+
+use pwt::css;
+use pwt::prelude::*;
+use pwt::state::{Selection, Store};
+use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
+use pwt::widget::menu::{Menu, MenuButton, MenuItem};
+use pwt::widget::Toolbar;
+
+use pwt_macros::builder;
+
+use proxmox_access_control::types::{AclListItem, AclUgidType};
+
+use crate::percent_encoding::percent_encode_component;
+use crate::utils::render_boolean;
+use crate::{
+ ConfirmButton, EditWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+};
+
+use super::acl_edit::AclEditWindow;
+
+#[derive(PartialEq, Properties)]
+#[builder]
+pub struct AclView {
+ /// Show the ACL entries for the specified API path and sub-paths only.
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or_default]
+ acl_path: Option<AttrValue>,
+
+ /// Specifies the endpoint from which to fetch the ACL entries from via GET and to update them
+ /// via PUT requests.
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or(String::from("/access/acl"))]
+ acl_api_endpoint: String,
+
+ /// Menu entries for editing the ACL. The key is used as the menu label while the value should
+ /// be a tuple containing icon class and the dialog for editing ACL entries.
+ #[prop_or_default]
+ // using an index map here preserves the insertion order
+ edit_acl_menu: IndexMap<AttrValue, (Classes, EditWindow)>,
+}
+
+impl AclView {
+ pub fn new() -> Self {
+ yew::props!(Self {})
+ }
+
+ pub fn with_acl_edit_menu_entry(
+ mut self,
+ lable: impl Into<AttrValue>,
+ icon: impl Into<Classes>,
+ dialog: impl AclEditWindow,
+ ) -> Self {
+ self.add_acl_edit_menu_entry(lable, icon, dialog);
+ self
+ }
+
+ pub fn add_acl_edit_menu_entry(
+ &mut self,
+ lable: impl Into<AttrValue>,
+ icon: impl Into<Classes>,
+ dialog: impl AclEditWindow,
+ ) {
+ self.edit_acl_menu
+ .borrow_mut()
+ .insert(lable.into(), (icon.into(), dialog.into()));
+ }
+}
+
+impl Default for AclView {
+ fn default() -> Self {
+ AclView::new()
+ }
+}
+
+impl From<AclView> for VNode {
+ fn from(value: AclView) -> Self {
+ VComp::new::<LoadableComponentMaster<ProxmoxAclView>>(Rc::new(value), None).into()
+ }
+}
+
+#[derive(Clone, PartialEq)]
+enum ViewState {
+ AddAcl(AttrValue),
+}
+
+enum Msg {
+ Reload,
+ Remove,
+}
+
+struct ProxmoxAclView {
+ selection: Selection,
+ store: Store<AclListItem>,
+}
+
+impl ProxmoxAclView {
+ fn colmuns() -> Rc<Vec<DataTableHeader<AclListItem>>> {
+ Rc::new(vec![
+ DataTableColumn::new(tr!("Path"))
+ .flex(1)
+ .render(|item: &AclListItem| item.path.as_str().into())
+ .sorter(|a: &AclListItem, b: &AclListItem| a.path.cmp(&b.path))
+ .sort_order(true)
+ .into(),
+ DataTableColumn::new(tr!("User/Group/API Token"))
+ .flex(1)
+ .render(|item: &AclListItem| item.ugid.as_str().into())
+ .sorter(|a: &AclListItem, b: &AclListItem| a.ugid.cmp(&b.ugid))
+ .sort_order(true)
+ .into(),
+ DataTableColumn::new(tr!("Role"))
+ .flex(1)
+ .render(|item: &AclListItem| item.roleid.as_str().into())
+ .sorter(|a: &AclListItem, b: &AclListItem| a.roleid.cmp(&b.roleid))
+ .into(),
+ DataTableColumn::new(tr!("Propagate"))
+ .render(|item: &AclListItem| render_boolean(item.propagate).as_str().into())
+ .sorter(|a: &AclListItem, b: &AclListItem| a.propagate.cmp(&b.propagate))
+ .into(),
+ ])
+ }
+}
+
+impl LoadableComponent for ProxmoxAclView {
+ type Properties = AclView;
+ type Message = Msg;
+ type ViewState = ViewState;
+
+ fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+ let link = ctx.link();
+ link.repeated_load(5000);
+
+ let selection = Selection::new().on_select(link.callback(|_| Msg::Reload));
+
+ let store = Store::with_extract_key(|record: &AclListItem| {
+ let acl_id = format!("{} for {} - {}", record.path, record.ugid, record.roleid);
+ Key::from(acl_id)
+ });
+
+ Self { selection, store }
+ }
+
+ fn load(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
+ let store = self.store.clone();
+ let url = &ctx.props().acl_api_endpoint;
+
+ let path = if let Some(path) = &ctx.props().acl_path {
+ format!("{url}&path={}", percent_encode_component(path))
+ } else {
+ url.to_owned()
+ };
+
+ Box::pin(async move {
+ let data = crate::http_get(&path, None).await?;
+ store.write().set_data(data);
+ Ok(())
+ })
+ }
+
+ fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
+ let selected_id = self.selection.selected_key().map(|k| k.to_string());
+ let disabled = selected_id.is_none();
+
+ let mut toolbar = Toolbar::new()
+ .class("pwt-w-100")
+ .class("pwt-overflow-hidden")
+ .border_bottom(true);
+
+ if !ctx.props().edit_acl_menu.is_empty() {
+ let add_menu = ctx.props().edit_acl_menu.iter().fold(
+ Menu::new(),
+ |add_menu, (label, (icon, _))| {
+ let msg = label.to_owned();
+
+ add_menu.with_item(
+ MenuItem::new(label.to_owned())
+ .icon_class(icon.to_owned())
+ .on_select(ctx.link().change_view_callback(move |_| {
+ Some(ViewState::AddAcl(msg.clone()))
+ })),
+ )
+ },
+ );
+
+ toolbar.add_child(MenuButton::new(tr!("Add")).show_arrow(true).menu(add_menu));
+ }
+
+ toolbar.add_child(
+ ConfirmButton::new(tr!("Remove ACL Entry"))
+ .confirm_message(tr!("Are you sure you want to remove this ACL entry?"))
+ .disabled(disabled)
+ .on_activate(ctx.link().callback(|_| Msg::Remove)),
+ );
+
+ Some(toolbar.into())
+ }
+
+ fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::Reload => true,
+ Msg::Remove => {
+ if let Some(key) = self.selection.selected_key() {
+ if let Some(record) = self.store.read().lookup_record(&key).cloned() {
+ let link = ctx.link();
+ let url = ctx.props().acl_api_endpoint.to_owned();
+
+ link.clone().spawn(async move {
+ let data = match record.ugid_type {
+ AclUgidType::User => json!({
+ "delete": true,
+ "path": record.path,
+ "role": record.roleid,
+ "auth-id": record.ugid,
+ }),
+ AclUgidType::Group => json!({
+ "delete": true,
+ "path": record.path,
+ "role": record.roleid,
+ "group": record.ugid,
+ }),
+ };
+
+ match crate::http_put(url, Some(data)).await {
+ Ok(()) => link.send_reload(),
+ Err(err) => link.show_error("Removing ACL failed", err, true),
+ }
+ });
+ }
+ }
+ false
+ }
+ }
+ }
+
+ fn main_view(&self, _ctx: &LoadableComponentContext<Self>) -> Html {
+ DataTable::new(Self::colmuns(), self.store.clone())
+ .class(css::FlexFit)
+ .selection(self.selection.clone())
+ .into()
+ }
+
+ fn dialog_view(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ view_state: &Self::ViewState,
+ ) -> Option<Html> {
+ match view_state {
+ ViewState::AddAcl(key) => ctx.props().edit_acl_menu.get(key).map(|(_, dialog)| {
+ dialog
+ .clone()
+ .on_done(ctx.link().change_view_callback(|_| None))
+ .into()
+ }),
+ }
+ }
+}
diff --git a/src/acl/mod.rs b/src/acl/mod.rs
new file mode 100644
index 0000000..cfe6aa6
--- /dev/null
+++ b/src/acl/mod.rs
@@ -0,0 +1,5 @@
+pub(crate) mod acl_edit;
+pub use acl_edit::AclEdit;
+
+pub(crate) mod acl_view;
+pub use acl_view::AclView;
diff --git a/src/lib.rs b/src/lib.rs
index 091cb72..3dacc20 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -25,6 +25,9 @@ pub use auth_edit_ldap::{AuthEditLDAP, ProxmoxAuthEditLDAP};
mod authid_selector;
pub use authid_selector::AuthidSelector;
+mod acl;
+pub use acl::{AclEdit, AclView};
+
mod bandwidth_selector;
pub use bandwidth_selector::{BandwidthSelector, ProxmoxBandwidthSelector};
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH yew-comp v2 3/3] role_selector/acl_edit: make api endpoint and default role configurable
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (7 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 2/3] acl: add a view and semi-generic `EditWindow` for acl entries Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] server: use proxmox-access-control api implementations Shannon Sterz
` (2 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
and expose it via the AclEdit component too so that users can more
easily adapt the components to their needs.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/acl/acl_edit.rs | 22 +++++++++++++++++++++-
src/role_selector.rs | 20 ++++++++++++++++----
2 files changed, 37 insertions(+), 5 deletions(-)
diff --git a/src/acl/acl_edit.rs b/src/acl/acl_edit.rs
index b8bbd89..03421b0 100644
--- a/src/acl/acl_edit.rs
+++ b/src/acl/acl_edit.rs
@@ -24,6 +24,16 @@ pub struct AclEdit {
#[builder(IntoPropValue, into_prop_value)]
acl_api_endpoint: String,
+ /// The endpoint which will be used to create new ACL entries via a PUT request.
+ #[prop_or_default]
+ #[builder(IntoPropValue, into_prop_value)]
+ role_api_endpoint: Option<String>,
+
+ /// The role that will be pre-selected in the role selector, `NoAccess` by default.
+ #[prop_or_default]
+ #[builder(IntoPropValue, into_prop_value)]
+ default_role: Option<AttrValue>,
+
#[prop_or_default]
input_panel: InputPanel,
}
@@ -59,12 +69,22 @@ impl From<AclEdit> for EditWindow {
)
};
+ let mut role_selector = RoleSelector::new().name("role").required(true);
+
+ if let Some(role_api_endpoint) = value.role_api_endpoint {
+ role_selector.set_role_api_endpoint(role_api_endpoint);
+ }
+
+ if let Some(default_role) = value.default_role {
+ role_selector.set_default_role(default_role.clone());
+ }
+
let input_panel = value
.input_panel
.clone()
.padding(4)
.with_field(authid_label, authid_field)
- .with_field(tr!("Role"), RoleSelector::new().name("role").required(true))
+ .with_field(tr!("Role"), role_selector)
.with_field(
tr!("Propagate"),
Checkbox::new().name("propagate").required(true),
diff --git a/src/role_selector.rs b/src/role_selector.rs
index 20a8483..429f58c 100644
--- a/src/role_selector.rs
+++ b/src/role_selector.rs
@@ -1,5 +1,6 @@
use anyhow::format_err;
use std::rc::Rc;
+use yew::html::IntoPropValue;
use yew::virtual_dom::Key;
@@ -37,11 +38,22 @@ thread_local! {
}
use pwt::props::{FieldBuilder, WidgetBuilder};
-use pwt_macros::widget;
+use pwt_macros::{builder, widget};
#[widget(comp=ProxmoxRoleSelector, @input)]
#[derive(Clone, Properties, PartialEq)]
-pub struct RoleSelector {}
+#[builder]
+pub struct RoleSelector {
+ /// The default role.
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or(Some(AttrValue::from("NoAccess")))]
+ default_role: Option<AttrValue>,
+
+ /// The API endpoint from which to fetch the existing roles from.
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or(String::from("/access/roles"))]
+ role_api_endpoint: String,
+}
impl Default for RoleSelector {
fn default() -> Self {
@@ -104,8 +116,8 @@ impl Component for ProxmoxRoleSelector {
.with_std_props(&props.std_props)
.with_input_props(&props.input_props)
.required(true)
- .default("NoAccess")
- .loader("/access/roles")
+ .default(ctx.props().default_role.clone())
+ .loader(ctx.props().role_api_endpoint.clone())
.validate(self.validate.clone())
.into()
}
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 1/2] server: use proxmox-access-control api implementations
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (8 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 3/3] role_selector/acl_edit: make api endpoint and default role configurable Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: configuration: add panel for viewing and editing acl entries Shannon Sterz
2025-04-17 15:46 ` [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Thomas Lamprecht
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
instead of re-implementing most of the code here, take advantage of
the new `api` feature that allows for sharing the common api
implementations.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
server/Cargo.toml | 2 +-
server/src/acl.rs | 102 +++++++++-
server/src/api/access/acl.rs | 357 -----------------------------------
server/src/api/access/mod.rs | 4 +-
4 files changed, 100 insertions(+), 365 deletions(-)
delete mode 100644 server/src/api/access/acl.rs
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 7b0058e..2d58cea 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -32,7 +32,7 @@ tokio-stream.workspace = true
tracing.workspace = true
url.workspace = true
-proxmox-access-control = { workspace = true, features = [ "impl" ] }
+proxmox-access-control = { workspace = true, features = [ "api" ] }
proxmox-async.workspace = true
proxmox-auth-api = { workspace = true, features = [ "api", "ticket", "pam-authenticator", "password-authenticator" ] }
proxmox-daemon.workspace = true
diff --git a/server/src/acl.rs b/server/src/acl.rs
index a99eede..52a1f97 100644
--- a/server/src/acl.rs
+++ b/server/src/acl.rs
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::OnceLock;
-use anyhow::{Context as _, Error};
+use anyhow::{format_err, Context as _, Error};
use proxmox_access_control::types::User;
use proxmox_auth_api::types::Authid;
@@ -10,7 +10,7 @@ use proxmox_section_config::SectionConfigData;
struct AccessControlConfig;
static PRIVILEGES: OnceLock<HashMap<&str, u64>> = OnceLock::new();
-static ROLES: OnceLock<HashMap<&str, u64>> = OnceLock::new();
+static ROLES: OnceLock<HashMap<&str, (u64, &str)>> = OnceLock::new();
impl proxmox_access_control::init::AccessControlConfig for AccessControlConfig {
fn privileges(&self) -> &HashMap<&str, u64> {
@@ -18,11 +18,11 @@ impl proxmox_access_control::init::AccessControlConfig for AccessControlConfig {
}
#[rustfmt::skip]
- fn roles(&self) -> &HashMap<&str, u64> {
+ fn roles(&self) -> &HashMap<&str, (u64, &str)> {
ROLES.get_or_init(|| {
[
- ("Administrator", pdm_api_types::ROLE_ADMINISTRATOR),
- ("Auditor", pdm_api_types::ROLE_AUDITOR),
+ ("Administrator", (pdm_api_types::ROLE_ADMINISTRATOR, "Administrators can inspect and modify the system.")),
+ ("Auditor", (pdm_api_types::ROLE_AUDITOR, "An Auditor can inspect many aspects of the system, but not change them.")),
//("SystemAdministrator", pdm_api_types::ROLE_SYS_ADMINISTRATOR),
//("SystemAuditor", pdm_api_types::ROLE_SYS_AUDITOR),
//("ResourceAdministrator", pdm_api_types::ROLE_RESOURCE_ADMINISTRATOR),
@@ -63,6 +63,98 @@ impl proxmox_access_control::init::AccessControlConfig for AccessControlConfig {
Ok(())
}
+
+ fn acl_audit_privileges(&self) -> u64 {
+ pdm_api_types::PRIV_ACCESS_AUDIT
+ }
+
+ fn acl_modify_privileges(&self) -> u64 {
+ pdm_api_types::PRIV_ACCESS_MODIFY
+ }
+
+ fn check_acl_path(&self, path: &str) -> Result<(), Error> {
+ let components = proxmox_access_control::acl::split_acl_path(path);
+
+ let components_len = components.len();
+
+ if components_len == 0 {
+ return Ok(());
+ }
+ match components[0] {
+ "access" => {
+ if components_len == 1 {
+ return Ok(());
+ }
+ match components[1] {
+ "acl" | "users" | "realm" => {
+ if components_len == 2 {
+ return Ok(());
+ }
+ }
+ _ => {}
+ }
+ }
+ "resource" => {
+ // `/resource` and `/resource/{remote}`
+ if components_len <= 2 {
+ return Ok(());
+ }
+ // `/resource/{remote-id}/{resource-type=guest,storage}/{resource-id}`
+ match components[2] {
+ "guest" | "storage" => {
+ // /resource/{remote-id}/{resource-type}
+ // /resource/{remote-id}/{resource-type}/{resource-id}
+ if components_len <= 4 {
+ return Ok(());
+ }
+ }
+ _ => {}
+ }
+ }
+ "system" => {
+ if components_len == 1 {
+ return Ok(());
+ }
+ match components[1] {
+ "certificates" | "disks" | "log" | "notifications" | "status" | "tasks"
+ | "time" => {
+ if components_len == 2 {
+ return Ok(());
+ }
+ }
+ "services" => {
+ // /system/services/{service}
+ if components_len <= 3 {
+ return Ok(());
+ }
+ }
+ "network" => {
+ if components_len == 2 {
+ return Ok(());
+ }
+ match components[2] {
+ "dns" => {
+ if components_len == 3 {
+ return Ok(());
+ }
+ }
+ "interfaces" => {
+ // /system/network/interfaces/{iface}
+ if components_len <= 4 {
+ return Ok(());
+ }
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+
+ Err(format_err!("invalid acl path '{}'.", path))
+ }
}
pub(crate) fn init() {
diff --git a/server/src/api/access/acl.rs b/server/src/api/access/acl.rs
deleted file mode 100644
index a51899b..0000000
--- a/server/src/api/access/acl.rs
+++ /dev/null
@@ -1,357 +0,0 @@
-use anyhow::{bail, Error};
-
-use proxmox_access_control::acl::AclTreeNode;
-use proxmox_access_control::CachedUserInfo;
-use proxmox_config_digest::{ConfigDigest, PROXMOX_CONFIG_DIGEST_SCHEMA};
-use proxmox_router::{Permission, Router, RpcEnvironment};
-use proxmox_schema::api;
-
-use pdm_api_types::{
- AclListItem, AclUgidType, Authid, Role, ACL_PATH_SCHEMA, ACL_PROPAGATE_SCHEMA,
- PRIV_ACCESS_AUDIT, PRIV_ACCESS_MODIFY, PROXMOX_GROUP_ID_SCHEMA,
-};
-
-pub(super) const ROUTER: Router = Router::new()
- .get(&API_METHOD_READ_ACL)
- .put(&API_METHOD_UPDATE_ACL);
-
-// FIXME: copied from PBS
-fn extract_acl_node_data(
- node: &AclTreeNode,
- path: &str,
- list: &mut Vec<AclListItem>,
- exact: bool,
- auth_id_filter: &Option<Authid>,
-) {
- // 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;
- }
- }
-
- 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: if path.is_empty() {
- String::from("/")
- } else {
- path.to_string()
- },
- 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: if path.is_empty() {
- String::from("/")
- } else {
- path.to_string()
- },
- propagate: *propagate,
- ugid_type: AclUgidType::Group,
- ugid: group.to_string(),
- roleid: role.to_string(),
- });
- }
- }
- if exact {
- return;
- }
- for (comp, child) in &node.children {
- let new_path = format!("{}/{}", path, comp);
- extract_acl_node_data(child, &new_path, list, exact, auth_id_filter);
- }
-}
-
-#[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 Sys.Audit on '/access/acl', or just the ACLs containing the user's API tokens.",
- },
-)]
-/// Read Access Control List (ACLs).
-fn read_acl(
- path: Option<String>,
- exact: bool,
- rpcenv: &mut dyn RpcEnvironment,
-) -> Result<Vec<AclListItem>, Error> {
- let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
-
- let user_info = CachedUserInfo::new()?;
-
- let top_level_privs = user_info.lookup_privs(&auth_id, &["access", "acl"]);
- let auth_id_filter = if (top_level_privs & PRIV_ACCESS_AUDIT) == 0 {
- Some(auth_id)
- } else {
- None
- };
-
- let (mut tree, digest) = proxmox_access_control::acl::config()?;
-
- let mut list: Vec<AclListItem> = Vec::new();
- if let Some(path) = &path {
- if let Some(node) = &tree.find_node(path) {
- extract_acl_node_data(node, path, &mut list, exact, &auth_id_filter);
- }
- } else {
- extract_acl_node_data(&tree.root, "", &mut list, exact, &auth_id_filter);
- }
-
- rpcenv["digest"] = hex::encode(digest).into();
-
- Ok(list)
-}
-
-#[api(
- protected: true,
- input: {
- properties: {
- path: {
- schema: ACL_PATH_SCHEMA,
- },
- role: {
- type: Role,
- },
- 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 Permissions.Modify on '/access/acl', limited to updating ACLs of the user's API tokens otherwise."
- },
-)]
-/// Update Access Control List (ACLs).
-#[allow(clippy::too_many_arguments)]
-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 current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
-
- let user_info = CachedUserInfo::new()?;
-
- let top_level_privs = user_info.lookup_privs(¤t_auth_id, &["access", "acl"]);
- if top_level_privs & PRIV_ACCESS_MODIFY == 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.");
- }
- };
- }
-
- let _lock = proxmox_access_control::acl::lock_config()?;
-
- let (mut tree, expected_digest) = proxmox_access_control::acl::config()?;
-
- expected_digest.detect_modification(digest.as_ref())?;
-
- let propagate = propagate.unwrap_or(true);
-
- if let Some(ref _group) = group {
- bail!("parameter 'group' - groups are currently not supported.");
- } else if let Some(ref auth_id) = auth_id {
- if !delete {
- // Note: we allow to delete non-existent users
- let user_cfg = proxmox_access_control::user::cached_config()?;
- if !user_cfg.sections.contains_key(&auth_id.to_string()) {
- bail!(format!(
- "no such {}.",
- if auth_id.is_token() {
- "API token"
- } else {
- "user"
- }
- ));
- }
- }
- } else {
- bail!("missing 'userid' or 'group' parameter.");
- }
-
- if !delete {
- // Note: we allow to delete entries with invalid path
- check_acl_path(&path)?;
- }
-
- 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);
- }
- }
-
- proxmox_access_control::acl::save_config(&tree)?;
-
- Ok(())
-}
-///
-/// Check whether a given ACL `path` conforms to the expected schema.
-///
-/// Currently this just checks for the number of components for various sub-trees.
-fn check_acl_path(path: &str) -> Result<(), Error> {
- let components = proxmox_access_control::acl::split_acl_path(path);
-
- let components_len = components.len();
-
- if components_len == 0 {
- return Ok(());
- }
- match components[0] {
- "access" => {
- if components_len == 1 {
- return Ok(());
- }
- match components[1] {
- "acl" | "users" | "realm" => {
- if components_len == 2 {
- return Ok(());
- }
- }
- _ => {}
- }
- }
- "resource" => {
- // `/resource` and `/resource/{remote}`
- if components_len <= 2 {
- return Ok(());
- }
- // `/resource/{remote-id}/{resource-type=guest,storage}/{resource-id}`
- match components[2] {
- "guest" | "storage" => {
- // /resource/{remote-id}/{resource-type}
- // /resource/{remote-id}/{resource-type}/{resource-id}
- if components_len <= 4 {
- return Ok(());
- }
- }
- _ => {}
- }
- }
- "system" => {
- if components_len == 1 {
- return Ok(());
- }
- match components[1] {
- "certificates" | "disks" | "log" | "notifications" | "status" | "tasks"
- | "time" => {
- if components_len == 2 {
- return Ok(());
- }
- }
- "services" => {
- // /system/services/{service}
- if components_len <= 3 {
- return Ok(());
- }
- }
- "network" => {
- if components_len == 2 {
- return Ok(());
- }
- match components[2] {
- "dns" => {
- if components_len == 3 {
- return Ok(());
- }
- }
- "interfaces" => {
- // /system/network/interfaces/{iface}
- if components_len <= 4 {
- return Ok(());
- }
- }
- _ => {}
- }
- }
- _ => {}
- }
- }
- _ => {}
- }
-
- bail!("invalid acl path '{}'.", path);
-}
diff --git a/server/src/api/access/mod.rs b/server/src/api/access/mod.rs
index befe2f1..345b22f 100644
--- a/server/src/api/access/mod.rs
+++ b/server/src/api/access/mod.rs
@@ -13,19 +13,19 @@ use proxmox_sortable_macro::sortable;
use pdm_api_types::{Authid, ACL_PATH_SCHEMA, PRIVILEGES, PRIV_ACCESS_AUDIT};
-mod acl;
mod domains;
mod tfa;
mod users;
#[sortable]
const SUBDIRS: SubdirMap = &sorted!([
- ("acl", &acl::ROUTER),
+ ("acl", &proxmox_access_control::api::ACL_ROUTER),
("domains", &domains::ROUTER),
(
"permissions",
&Router::new().get(&API_METHOD_LIST_PERMISSIONS)
),
+ ("roles", &proxmox_access_control::api::ROLE_ROUTER),
("tfa", &tfa::ROUTER),
(
"ticket",
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: configuration: add panel for viewing and editing acl entries
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (9 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] server: use proxmox-access-control api implementations Shannon Sterz
@ 2025-04-11 13:44 ` Shannon Sterz
2025-04-17 15:46 ` [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Thomas Lamprecht
11 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2025-04-11 13:44 UTC (permalink / raw)
To: pdm-devel
using the new generic acl viewing and editing components from
proxmox-yew-comp make it possible for users to edit acl entries
note that the path selector is based on the current definition of
acceptable paths in the `AccessControlConfig`, since those are the
only valid paths. however, the convenience could be improved by, for
example, adding entries for specific resources. however, the acl paths
here don't seem fixed yet, so leave this as-is for now.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
ui/src/configuration/mod.rs | 23 ++++-
.../configuration/permission_path_selector.rs | 86 +++++++++++++++++++
2 files changed, 108 insertions(+), 1 deletion(-)
create mode 100644 ui/src/configuration/permission_path_selector.rs
diff --git a/ui/src/configuration/mod.rs b/ui/src/configuration/mod.rs
index ca16685..2a8b991 100644
--- a/ui/src/configuration/mod.rs
+++ b/ui/src/configuration/mod.rs
@@ -1,3 +1,4 @@
+use permission_path_selector::PermissionPathSelector;
use pwt::prelude::*;
use pwt::props::StorageLocation;
use pwt::state::NavigationContainer;
@@ -6,8 +7,9 @@ use pwt::widget::{Container, MiniScrollMode, Panel, TabBarItem, TabPanel};
use proxmox_yew_comp::configuration::TimePanel;
use proxmox_yew_comp::configuration::{DnsPanel, NetworkView};
use proxmox_yew_comp::tfa::TfaView;
-use proxmox_yew_comp::UserPanel;
+use proxmox_yew_comp::{AclEdit, AclView, UserPanel};
+mod permission_path_selector;
mod webauthn;
pub use webauthn::WebauthnPanel;
@@ -40,6 +42,8 @@ pub fn system_configuration() -> Html {
#[function_component(AccessControl)]
pub fn access_control() -> Html {
+ let acl_edit = AclEdit::new(tr!("Path"), PermissionPathSelector::new()).default_role("Auditor");
+
let panel = TabPanel::new()
.state_id(StorageLocation::session("AccessControlState"))
//.title(tr!("Configuration") + ": " + &tr!("Access Control"))
@@ -71,6 +75,23 @@ pub fn access_control() -> Html {
.with_child(TfaView::new())
.into()
},
+ )
+ .with_item_builder(
+ TabBarItem::new()
+ .key("permissions")
+ .icon_class("fa fa-unlock")
+ .label(tr!("Permissions")),
+ move |_| {
+ Container::new()
+ .class("pwt-content-spacer")
+ .class(pwt::css::FlexFit)
+ .with_child(AclView::new().with_acl_edit_menu_entry(
+ tr!("User Permission"),
+ "fa fa-fw fa-user",
+ acl_edit.clone().use_tokens(false),
+ ))
+ .into()
+ },
);
NavigationContainer::new().with_child(panel).into()
diff --git a/ui/src/configuration/permission_path_selector.rs b/ui/src/configuration/permission_path_selector.rs
new file mode 100644
index 0000000..713c8c9
--- /dev/null
+++ b/ui/src/configuration/permission_path_selector.rs
@@ -0,0 +1,86 @@
+use std::rc::Rc;
+
+use yew::html::IntoPropValue;
+
+use pwt::widget::form::Combobox;
+use pwt::{prelude::*, AsyncPool};
+
+use pwt_macros::{builder, widget};
+
+static PREDEFINED_PATHS: &[&str] = &[
+ "/",
+ "/access",
+ "/access/acl",
+ "/access/users",
+ "/resource",
+ "/system",
+ "/system/certificates",
+ "/system/disks",
+ "/system/log",
+ "/system/notifications",
+ "/system/network",
+ "/system/network/dns",
+ "/system/network/interfaces",
+ "/system/services",
+ "/system/status",
+ "/system/tasks",
+ "/system/time",
+ "/system/services",
+];
+
+#[widget(comp=PdmPermissionPathSelector, @input, @element)]
+#[derive(Clone, PartialEq, Properties)]
+#[builder]
+pub struct PermissionPathSelector {
+ /// Default value
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or_default]
+ default: Option<AttrValue>,
+}
+
+impl PermissionPathSelector {
+ pub(super) fn new() -> Self {
+ yew::props!(Self {})
+ }
+}
+
+enum Msg {}
+
+struct PdmPermissionPathSelector {
+ items: Rc<Vec<AttrValue>>,
+}
+
+impl PdmPermissionPathSelector {}
+
+impl Component for PdmPermissionPathSelector {
+ type Message = Msg;
+ type Properties = PermissionPathSelector;
+
+ fn create(_ctx: &Context<Self>) -> Self {
+ // TODO: fetch resources & remotes from the backend to improve the pre-defined selection of
+ // acl paths
+ Self {
+ items: Rc::new(
+ PREDEFINED_PATHS
+ .iter()
+ .map(|i| AttrValue::from(*i))
+ .collect(),
+ ),
+ }
+ }
+
+ fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
+ false
+ }
+
+ fn view(&self, ctx: &Context<Self>) -> Html {
+ let props = ctx.props();
+ Combobox::new()
+ .with_std_props(&props.std_props)
+ .with_input_props(&props.input_props)
+ .default(props.default.clone())
+ .items(Rc::clone(&self.items))
+ .editable(true)
+ .into()
+ }
+}
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
` (10 preceding siblings ...)
2025-04-11 13:44 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: configuration: add panel for viewing and editing acl entries Shannon Sterz
@ 2025-04-17 15:46 ` Thomas Lamprecht
11 siblings, 0 replies; 13+ messages in thread
From: Thomas Lamprecht @ 2025-04-17 15:46 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Shannon Sterz
Am 11.04.25 um 15:44 schrieb Shannon Sterz:
> this series aims to make more parts of our access control list
> implementation re-usable between products. in a first step most of the
> relevant api endpoints and api types are moved to
> `proxmox-access-control`. this is done by adding a new `api` feature
> that includes the necessary api endpoints. the `AccessControlConfig`
> trait is also expanded to make the api endpoints more adaptable to
> different products. by providing default implementations for the newly
> added trait functions existing users don't need to change anything. it
> also tries to make the code here easier to understand as the checks
> could be hard to grasp previously.
>
> next the series adds components to proxmox-yew-comp to provide a panel
> for inspecting the current acl and adding or removing entries. this is
> done by using the existing `RoleSelector` and `AuthidSelector`
> components. the later is also slightly adapted to make it possible to
> change the api endpoint that roles are fetched from as well as the
> default role. the `AclView` component allows users of the crate to add
> more options for adding ACL entries. meaning they can configure distinct
> components for adding user, token or group permissions. this is done in
> a generic fashion so that extending this menu does not require changing
> the component again.
>
> finally proxmox-datacenter-manager is adapted to use the new api
> endpoints in `proxmox-access-control` and a permissions panel is
> implemented. note that this would benefit from some clean-up once
> permission path and such are cleaned up.
>
I double-checked, so I hope I did not just overlook it, but I could not
find any (higher level) changelog since v1, neither a series wide one
nor a per patch one. At least one would be really nice to have, and
having both is always great from a reviewer POV IMO.
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 13+ messages in thread
end of thread, other threads:[~2025-04-17 15:46 UTC | newest]
Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-04-11 13:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 1/6] access-control: add more types to prepare for api feature Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 2/6] access-control: add acl " Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 3/6] access-control: add comments to roles function of AccessControlConfig Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 4/6] access-control: add generic roles endpoint to `api` feature Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 5/6] access-control: api: refactor validation checks to re-use existing code Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH proxmox v2 6/6] access-control: api: refactor extract_acl_node_data to be non-recursive Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 1/3] api-types/role_selector: depend on common `RoleInfo` type Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 2/3] acl: add a view and semi-generic `EditWindow` for acl entries Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH yew-comp v2 3/3] role_selector/acl_edit: make api endpoint and default role configurable Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] server: use proxmox-access-control api implementations Shannon Sterz
2025-04-11 13:44 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: configuration: add panel for viewing and editing acl entries Shannon Sterz
2025-04-17 15:46 ` [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/11] ACL edit api and ui components Thomas Lamprecht
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.