all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends
@ 2025-10-22 13:11 Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 1/4] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
                   ` (9 more replies)
  0 siblings, 10 replies; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

this patch series adds support for querying acl entries from the
front-end. it also makes it possible to reactively render ui components
depending on the user's privileges and refreshes this information every
time a new ticket is set.

the first four patches make it possible to use the AclTree by itself in
the ui. first by creating a new feature that exposes only it and some
types to dependent crates. then some functions that basically just query
the AclTree are moved to the AclTree itself to make it easier to re-use
them. the third patch derives Debug and PartialEq on the AclTree and
AclTreeNode to make it easier to handle these types in the ui. finally
the last commit allows to query all of a user's acl entries via the
API_METHOD_READ_ACL endpoint.

the next two patches first add an AclContext and AclContextProvider
implementation to proxmox-yew-comp. these allow applications to provide
acl information that components can hook into and get reactively
re-rendered. it also triggers reloading the acl information every time a
user logs in or a ticket gets refreshed.

lastly, proxmox-datacenter-manager is adapted to use this new
functionality. the seventh commit moves the AccessControlConfig to the
shared api types crate, so we can re-use it in the front-end. then an
AclContextProvider is added to the main ui component. this allows
components to retrieve said AclContext and use it to conditionally
render ui components. the last two commits add just such functionality
to the notes section of the pdm ui.

Follow-up
---------

if this series is applied, more ui components will need to be hooked
into the context to more widely use this functionality accross the
application.

proxmox:

Shannon Sterz (4):
  access-control: add acl feature to only expose types and the AclTree
  access-control: move functions querying privileges to the AclTree
  access-control: derive Debug and PartialEq on AclTree and AclTreeNode
  access-control: allow reading all acls of the current authid

 proxmox-access-control/Cargo.toml             |   6 +-
 proxmox-access-control/src/acl.rs             | 132 +++++++++++++++++-
 proxmox-access-control/src/api/acl.rs         |  37 ++++-
 .../src/cached_user_info.rs                   |  91 +-----------
 proxmox-access-control/src/init.rs            |  13 +-
 proxmox-access-control/src/lib.rs             |   4 +-
 6 files changed, 184 insertions(+), 99 deletions(-)


proxmox-yew-comp:

Shannon Sterz (2):
  acl_context: add AclContext and AclContextProvider
  http_helpers: reload LocalAclTree when logging in or refreshing a
    ticket

 Cargo.toml          |   2 +-
 src/acl_context.rs  | 204 ++++++++++++++++++++++++++++++++++++++++++++
 src/http_helpers.rs |   5 ++
 src/lib.rs          |   3 +
 4 files changed, 213 insertions(+), 1 deletion(-)
 create mode 100644 src/acl_context.rs


proxmox-datacenter-manager:

Shannon Sterz (2):
  server/api-types: move AccessControlConfig to shared api types
  ui: add an AclContext via the AclContextProvider to the main app ui

 lib/pdm-api-types/Cargo.toml |   1 +
 lib/pdm-api-types/src/acl.rs | 158 ++++++++++++++++++++++++++++++++++
 server/src/acl.rs            | 162 +----------------------------------
 ui/Cargo.toml                |   1 +
 ui/src/main.rs               |  14 ++-
 5 files changed, 173 insertions(+), 163 deletions(-)


proxmox-yew-comp:

Shannon Sterz (1):
  notes view: allow hiding the toolbar if editing isn't supported

 src/notes_view.rs | 36 +++++++++++++++++++++++-------------
 1 file changed, 23 insertions(+), 13 deletions(-)


proxmox-datacenter-manager:

Shannon Sterz (1):
  ui: main menu: use the AclContext to hide the Notes if appropriate

 ui/src/main_menu.rs | 66 +++++++++++++++++++++++++++++++--------------
 1 file changed, 46 insertions(+), 20 deletions(-)


Summary over all repositories:
  17 files changed, 639 insertions(+), 296 deletions(-)

--
Generated by git-murpp 0.8.1


_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH proxmox v2 1/4] access-control: add acl feature to only expose types and the AclTree
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-23  9:24   ` Dominik Csapak
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 2/4] access-control: move functions querying privileges to " Shannon Sterz
                   ` (8 subsequent siblings)
  9 siblings, 1 reply; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

this is useful, when an application wants to only handle an acl tree
without depending on more complex features provided by the rest of the
`impl` feature or its bigger dependencies (e.g. openssl).

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 proxmox-access-control/Cargo.toml  |  6 +++++-
 proxmox-access-control/src/acl.rs  | 19 ++++++++++++++++++-
 proxmox-access-control/src/init.rs | 13 ++++++++++++-
 proxmox-access-control/src/lib.rs  |  4 ++--
 4 files changed, 37 insertions(+), 5 deletions(-)

diff --git a/proxmox-access-control/Cargo.toml b/proxmox-access-control/Cargo.toml
index d23d272d..0eaf9fdf 100644
--- a/proxmox-access-control/Cargo.toml
+++ b/proxmox-access-control/Cargo.toml
@@ -35,18 +35,22 @@ proxmox-uuid = { workspace = true }
 
 [features]
 default = []
+acl = [
+    "dep:proxmox-section-config",
+]
 api = [
     "impl",
     "dep:hex",
 ]
 impl = [
+    "acl",
     "dep:nix",
     "dep:openssl",
     "dep:proxmox-config-digest",
     "dep:proxmox-product-config",
     "dep:proxmox-router",
-    "dep:proxmox-section-config",
     "dep:proxmox-shared-memory",
     "dep:proxmox-sys",
     "dep:serde_json",
 ]
+
diff --git a/proxmox-access-control/src/acl.rs b/proxmox-access-control/src/acl.rs
index 270292ac..877b003c 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -1,15 +1,25 @@
-use std::collections::{BTreeMap, BTreeSet, HashMap};
+#[cfg(feature = "impl")]
+use std::collections::BTreeSet;
+use std::collections::{BTreeMap, HashMap};
+#[cfg(feature = "impl")]
 use std::io::Write;
+#[cfg(feature = "impl")]
 use std::path::Path;
+#[cfg(feature = "impl")]
 use std::sync::{Arc, OnceLock, RwLock};
 
 use anyhow::{bail, Error};
 
 use proxmox_auth_api::types::{Authid, Userid};
+#[cfg(feature = "impl")]
 use proxmox_config_digest::ConfigDigest;
+#[cfg(feature = "impl")]
 use proxmox_product_config::{open_api_lockfile, replace_privileged_config, ApiLockGuard};
 
 use crate::init::{access_conf, acl_config, acl_config_lock};
+use crate::init::access_conf;
+#[cfg(feature = "impl")]
+use crate::init::{acl_config, acl_config_lock};
 
 pub fn split_acl_path(path: &str) -> Vec<&str> {
     let items = path.split('/');
@@ -302,6 +312,7 @@ impl AclTree {
         node.insert_user_role(auth_id.to_owned(), role.to_string(), propagate);
     }
 
+    #[cfg(feature = "impl")]
     fn write_node_config(node: &AclTreeNode, path: &str, w: &mut dyn Write) -> Result<(), Error> {
         let mut role_ug_map0: HashMap<_, BTreeSet<_>> = HashMap::new();
         let mut role_ug_map1: HashMap<_, BTreeSet<_>> = HashMap::new();
@@ -402,6 +413,7 @@ impl AclTree {
         Ok(())
     }
 
+    #[cfg(feature = "impl")]
     fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
         Self::write_node_config(&self.root, "", w)
     }
@@ -449,6 +461,7 @@ impl AclTree {
         Ok(())
     }
 
+    #[cfg(feature = "impl")]
     fn load(filename: &Path) -> Result<(Self, ConfigDigest), Error> {
         let mut tree = Self::new();
 
@@ -553,11 +566,13 @@ impl AclTree {
 }
 
 /// Get exclusive lock
+#[cfg(feature = "impl")]
 pub fn lock_config() -> Result<ApiLockGuard, Error> {
     open_api_lockfile(acl_config_lock(), None, true)
 }
 
 /// Reads the [`AclTree`] from `acl.cfg` in the configuration directory.
+#[cfg(feature = "impl")]
 pub fn config() -> Result<(AclTree, ConfigDigest), Error> {
     let path = acl_config();
     AclTree::load(&path)
@@ -568,6 +583,7 @@ pub fn config() -> Result<(AclTree, ConfigDigest), Error> {
 ///
 /// Since the AclTree is used for every API request's permission check, this caching mechanism
 /// allows to skip reading and parsing the file again if it is unchanged.
+#[cfg(feature = "impl")]
 pub fn cached_config() -> Result<Arc<AclTree>, Error> {
     struct ConfigCache {
         data: Option<Arc<AclTree>>,
@@ -621,6 +637,7 @@ pub fn cached_config() -> Result<Arc<AclTree>, Error> {
 
 /// Saves an [`AclTree`] to `acl.cfg` in the configuration directory, ensuring proper ownership and
 /// file permissions.
+#[cfg(feature = "impl")]
 pub fn save_config(acl: &AclTree) -> Result<(), Error> {
     let mut raw: Vec<u8> = Vec::new();
     acl.write_config(&mut raw)?;
diff --git a/proxmox-access-control/src/init.rs b/proxmox-access-control/src/init.rs
index 39a12352..cf5f795d 100644
--- a/proxmox-access-control/src/init.rs
+++ b/proxmox-access-control/src/init.rs
@@ -1,4 +1,5 @@
 use std::collections::HashMap;
+#[cfg(feature = "impl")]
 use std::path::{Path, PathBuf};
 use std::sync::OnceLock;
 
@@ -8,6 +9,7 @@ use proxmox_auth_api::types::{Authid, Userid};
 use proxmox_section_config::SectionConfigData;
 
 static ACCESS_CONF: OnceLock<&'static dyn AccessControlConfig> = OnceLock::new();
+#[cfg(feature = "impl")]
 static ACCESS_CONF_DIR: OnceLock<PathBuf> = OnceLock::new();
 
 /// This trait specifies the functions a product needs to implement to get ACL tree based access
@@ -105,6 +107,7 @@ pub trait AccessControlConfig: Send + Sync {
     }
 }
 
+#[cfg(feature = "impl")]
 pub fn init<P: AsRef<Path>>(
     acm_config: &'static dyn AccessControlConfig,
     config_dir: P,
@@ -113,13 +116,14 @@ pub fn init<P: AsRef<Path>>(
     init_access_config_dir(config_dir)
 }
 
+#[cfg(feature = "impl")]
 pub(crate) fn init_access_config_dir<P: AsRef<Path>>(config_dir: P) -> Result<(), Error> {
     ACCESS_CONF_DIR
         .set(config_dir.as_ref().to_owned())
         .map_err(|_e| format_err!("cannot initialize acl tree config twice!"))
 }
 
-pub(crate) fn init_access_config(config: &'static dyn AccessControlConfig) -> Result<(), Error> {
+pub fn init_access_config(config: &'static dyn AccessControlConfig) -> Result<(), Error> {
     ACCESS_CONF
         .set(config)
         .map_err(|_e| format_err!("cannot initialize acl tree config twice!"))
@@ -131,32 +135,39 @@ pub(crate) fn access_conf() -> &'static dyn AccessControlConfig {
         .expect("please initialize the acm config before using it!")
 }
 
+#[cfg(feature = "impl")]
 fn conf_dir() -> &'static PathBuf {
     ACCESS_CONF_DIR
         .get()
         .expect("please initialize acm config dir before using it!")
 }
 
+#[cfg(feature = "impl")]
 pub(crate) fn acl_config() -> PathBuf {
     conf_dir().join("acl.cfg")
 }
 
+#[cfg(feature = "impl")]
 pub(crate) fn acl_config_lock() -> PathBuf {
     conf_dir().join(".acl.lck")
 }
 
+#[cfg(feature = "impl")]
 pub(crate) fn user_config() -> PathBuf {
     conf_dir().join("user.cfg")
 }
 
+#[cfg(feature = "impl")]
 pub(crate) fn user_config_lock() -> PathBuf {
     conf_dir().join(".user.lck")
 }
 
+#[cfg(feature = "impl")]
 pub(crate) fn token_shadow() -> PathBuf {
     conf_dir().join("token.shadow")
 }
 
+#[cfg(feature = "impl")]
 pub(crate) fn token_shadow_lock() -> PathBuf {
     conf_dir().join("token.shadow.lock")
 }
diff --git a/proxmox-access-control/src/lib.rs b/proxmox-access-control/src/lib.rs
index 62683924..9195c999 100644
--- a/proxmox-access-control/src/lib.rs
+++ b/proxmox-access-control/src/lib.rs
@@ -2,13 +2,13 @@
 
 pub mod types;
 
-#[cfg(feature = "impl")]
+#[cfg(feature = "acl")]
 pub mod acl;
 
 #[cfg(feature = "api")]
 pub mod api;
 
-#[cfg(feature = "impl")]
+#[cfg(feature = "acl")]
 pub mod init;
 
 #[cfg(feature = "impl")]
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH proxmox v2 2/4] access-control: move functions querying privileges to the AclTree
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 1/4] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 3/4] access-control: derive Debug and PartialEq on AclTree and AclTreeNode Shannon Sterz
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

instead of keeping them in the CachedUserInfo. there is nothing that's
specific to the CachedUserInfo in these functions and having them in
the AclTree makes it possible to use them with the `acl` feature only
too.

to keep backward compatability, we keep the original functions in
CachedUserInfo but make them call the functions on the AclTree.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 proxmox-access-control/src/acl.rs             | 109 +++++++++++++++++-
 .../src/cached_user_info.rs                   |  91 +--------------
 2 files changed, 114 insertions(+), 86 deletions(-)

diff --git a/proxmox-access-control/src/acl.rs b/proxmox-access-control/src/acl.rs
index 877b003c..9fb97f55 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -16,7 +16,6 @@ use proxmox_config_digest::ConfigDigest;
 #[cfg(feature = "impl")]
 use proxmox_product_config::{open_api_lockfile, replace_privileged_config, ApiLockGuard};
 
-use crate::init::{access_conf, acl_config, acl_config_lock};
 use crate::init::access_conf;
 #[cfg(feature = "impl")]
 use crate::init::{acl_config, acl_config_lock};
@@ -563,6 +562,102 @@ impl AclTree {
 
         Ok(res)
     }
+
+    pub fn check_privs(
+        &self,
+        auth_id: &Authid,
+        path: &[&str],
+        required_privs: u64,
+        partial: bool,
+    ) -> Result<(), Error> {
+        let privs = self.lookup_privs(auth_id, path);
+        let allowed = if partial {
+            (privs & required_privs) != 0
+        } else {
+            (privs & required_privs) == required_privs
+        };
+        if !allowed {
+            // printing the path doesn't leak any information as long as we
+            // always check privilege before resource existence
+            let priv_names = privs_to_priv_names(required_privs);
+            let priv_names = if partial {
+                priv_names.join("|")
+            } else {
+                priv_names.join("&")
+            };
+            bail!(
+                "missing permissions '{priv_names}' on '/{}'",
+                path.join("/")
+            );
+        }
+        Ok(())
+    }
+
+    pub fn lookup_privs(&self, auth_id: &Authid, path: &[&str]) -> u64 {
+        let (privs, _) = self.lookup_privs_details(auth_id, path);
+        privs
+    }
+
+    pub fn lookup_privs_details(&self, auth_id: &Authid, path: &[&str]) -> (u64, u64) {
+        if access_conf().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) {
+                    return (*admin, *admin);
+                }
+            }
+        }
+
+        let roles = self.roles(auth_id, path);
+        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 propagate {
+                    propagated_privs |= role_privs;
+                }
+                privs |= role_privs;
+            }
+        }
+
+        if auth_id.is_token() {
+            // limit privs to that of owning user
+            let user_auth_id = Authid::from(auth_id.user().clone());
+            let (owner_privs, owner_propagated_privs) =
+                self.lookup_privs_details(&user_auth_id, path);
+            privs &= owner_privs;
+            propagated_privs &= owner_propagated_privs;
+        }
+
+        (privs, propagated_privs)
+    }
+
+    /// Checks whether the `auth_id` has any of the privileges `privs` on any object below `path`.
+    pub fn any_privs_below(
+        &self,
+        auth_id: &Authid,
+        path: &[&str],
+        privs: u64,
+    ) -> Result<bool, Error> {
+        // if the anchor path itself has matching propagated privs, we skip checking children
+        let (_privs, propagated_privs) = self.lookup_privs_details(auth_id, path);
+        if propagated_privs & privs != 0 {
+            return Ok(true);
+        }
+
+        // get all sub-paths with roles defined for `auth_id`
+        let paths = self.get_child_paths(auth_id, path)?;
+
+        for path in paths.iter() {
+            // early return if any sub-path has any of the privs we are looking for
+            if privs & self.lookup_privs(auth_id, &[path.as_str()]) != 0 {
+                return Ok(true);
+            }
+        }
+
+        // no paths or no matching paths
+        Ok(false)
+    }
 }
 
 /// Get exclusive lock
@@ -651,6 +746,18 @@ pub fn save_config(acl: &AclTree) -> Result<(), Error> {
     Ok(())
 }
 
+fn privs_to_priv_names(privs: u64) -> Vec<&'static str> {
+    access_conf()
+        .privileges()
+        .iter()
+        .fold(Vec::new(), |mut priv_names, (name, value)| {
+            if value & privs != 0 {
+                priv_names.push(name);
+            }
+            priv_names
+        })
+}
+
 #[cfg(test)]
 mod test {
     use std::{collections::HashMap, sync::OnceLock};
diff --git a/proxmox-access-control/src/cached_user_info.rs b/proxmox-access-control/src/cached_user_info.rs
index f5ed2858..8db37727 100644
--- a/proxmox-access-control/src/cached_user_info.rs
+++ b/proxmox-access-control/src/cached_user_info.rs
@@ -2,7 +2,7 @@
 
 use std::sync::{Arc, OnceLock, RwLock};
 
-use anyhow::{bail, Error};
+use anyhow::Error;
 
 use proxmox_auth_api::types::{Authid, Userid};
 use proxmox_router::UserInformation;
@@ -118,66 +118,16 @@ impl CachedUserInfo {
         required_privs: u64,
         partial: bool,
     ) -> Result<(), Error> {
-        let privs = self.lookup_privs(auth_id, path);
-        let allowed = if partial {
-            (privs & required_privs) != 0
-        } else {
-            (privs & required_privs) == required_privs
-        };
-        if !allowed {
-            // printing the path doesn't leak any information as long as we
-            // always check privilege before resource existence
-            let priv_names = privs_to_priv_names(required_privs);
-            let priv_names = if partial {
-                priv_names.join("|")
-            } else {
-                priv_names.join("&")
-            };
-            bail!(
-                "missing permissions '{priv_names}' on '/{}'",
-                path.join("/")
-            );
-        }
-        Ok(())
+        self.acl_tree
+            .check_privs(auth_id, path, required_privs, partial)
     }
 
     pub fn lookup_privs(&self, auth_id: &Authid, path: &[&str]) -> u64 {
-        let (privs, _) = self.lookup_privs_details(auth_id, path);
-        privs
+        self.acl_tree.lookup_privs(auth_id, path)
     }
 
     pub fn lookup_privs_details(&self, auth_id: &Authid, path: &[&str]) -> (u64, u64) {
-        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) {
-                    return (*admin, *admin);
-                }
-            }
-        }
-
-        let roles = self.acl_tree.roles(auth_id, path);
-        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 propagate {
-                    propagated_privs |= role_privs;
-                }
-                privs |= role_privs;
-            }
-        }
-
-        if auth_id.is_token() {
-            // limit privs to that of owning user
-            let user_auth_id = Authid::from(auth_id.user().clone());
-            let (owner_privs, owner_propagated_privs) =
-                self.lookup_privs_details(&user_auth_id, path);
-            privs &= owner_privs;
-            propagated_privs &= owner_propagated_privs;
-        }
-
-        (privs, propagated_privs)
+        self.acl_tree.lookup_privs_details(auth_id, path)
     }
 
     /// Checks whether the `auth_id` has any of the privileges `privs` on any object below `path`.
@@ -187,24 +137,7 @@ impl CachedUserInfo {
         path: &[&str],
         privs: u64,
     ) -> Result<bool, Error> {
-        // if the anchor path itself has matching propagated privs, we skip checking children
-        let (_privs, propagated_privs) = self.lookup_privs_details(auth_id, path);
-        if propagated_privs & privs != 0 {
-            return Ok(true);
-        }
-
-        // get all sub-paths with roles defined for `auth_id`
-        let paths = self.acl_tree.get_child_paths(auth_id, path)?;
-
-        for path in paths.iter() {
-            // early return if any sub-path has any of the privs we are looking for
-            if privs & self.lookup_privs(auth_id, &[path.as_str()]) != 0 {
-                return Ok(true);
-            }
-        }
-
-        // no paths or no matching paths
-        Ok(false)
+        self.acl_tree.any_privs_below(auth_id, path, privs)
     }
 }
 
@@ -232,15 +165,3 @@ impl UserInformation for CachedUserInfo {
         }
     }
 }
-
-pub fn privs_to_priv_names(privs: u64) -> Vec<&'static str> {
-    access_conf()
-        .privileges()
-        .iter()
-        .fold(Vec::new(), |mut priv_names, (name, value)| {
-            if value & privs != 0 {
-                priv_names.push(name);
-            }
-            priv_names
-        })
-}
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH proxmox v2 3/4] access-control: derive Debug and PartialEq on AclTree and AclTreeNode
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 1/4] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 2/4] access-control: move functions querying privileges to " Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 4/4] access-control: allow reading all acls of the current authid Shannon Sterz
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 proxmox-access-control/src/acl.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/proxmox-access-control/src/acl.rs b/proxmox-access-control/src/acl.rs
index 9fb97f55..2f9bb598 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -36,7 +36,7 @@ pub fn split_acl_path(path: &str) -> Vec<&str> {
 }
 
 /// Tree representing a parsed acl.cfg
-#[derive(Default)]
+#[derive(Default, Debug, PartialEq)]
 pub struct AclTree {
     /// Root node of the tree.
     ///
@@ -46,7 +46,7 @@ pub struct AclTree {
 }
 
 /// Node representing ACLs for a certain ACL path.
-#[derive(Default)]
+#[derive(Default, Debug, PartialEq)]
 pub struct AclTreeNode {
     /// `User` or `Token` ACLs for this node.
     pub users: HashMap<Authid, HashMap<String, bool>>,
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH proxmox v2 4/4] access-control: allow reading all acls of the current authid
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
                   ` (2 preceding siblings ...)
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 3/4] access-control: derive Debug and PartialEq on AclTree and AclTreeNode Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-23  9:31   ` Dominik Csapak
  2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 1/2] acl_context: add AclContext and AclContextProvider Shannon Sterz
                   ` (5 subsequent siblings)
  9 siblings, 1 reply; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

adds a parameter to the `API_METHOD_READ_ACL` endpoint to allow
listing all ACL entries of the currently authenticated Authid.
allowing a user to see their own ACLs does not really exposes any
additional confidential information. however, being able to query this
information allows us, for example, to adapt ui components to a users
capabilities.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 proxmox-access-control/src/api/acl.rs | 37 ++++++++++++++++++++++-----
 1 file changed, 31 insertions(+), 6 deletions(-)

diff --git a/proxmox-access-control/src/api/acl.rs b/proxmox-access-control/src/api/acl.rs
index 0194d517..07222939 100644
--- a/proxmox-access-control/src/api/acl.rs
+++ b/proxmox-access-control/src/api/acl.rs
@@ -23,6 +23,12 @@ use crate::CachedUserInfo;
                 optional: true,
                 default: false,
             },
+            "exact-authid": {
+                description: "Whether to return ACL entries for the exact current authid only.",
+                type: bool,
+                optional: true,
+                default: false,
+            }
         },
     },
     returns: {
@@ -34,13 +40,17 @@ use crate::CachedUserInfo;
     },
     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.",
+        description: "Returns all ACLs if a user has sufficient privileges on this endpoint. \
+            Otherwise it is limited to the user's API tokens. However, if `exact-authid` is \
+            specified, all ACLs of the current Auhtid will be returned, whether the Authid has \
+            privileges to list other ACLs here or not.",
     },
 )]
 /// Get ACL entries, can be filter by path.
 pub fn read_acl(
     path: Option<String>,
     exact: bool,
+    exact_authid: bool,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Vec<AclListItem>, Error> {
     let auth_id = rpcenv
@@ -58,7 +68,11 @@ pub fn read_acl(
         )
         .is_err();
 
-    let filter = if filter_entries { Some(auth_id) } else { None };
+    let filter = if filter_entries || exact_authid {
+        Some(auth_id)
+    } else {
+        None
+    };
 
     let (mut tree, digest) = crate::acl::config()?;
 
@@ -74,7 +88,13 @@ pub fn read_acl(
 
     rpcenv["digest"] = hex::encode(digest).into();
 
-    Ok(extract_acl_node_data(node, path.as_deref(), exact, &filter))
+    Ok(extract_acl_node_data(
+        node,
+        path.as_deref(),
+        exact_authid,
+        exact,
+        &filter,
+    ))
 }
 
 #[api(
@@ -241,7 +261,8 @@ pub fn update_acl(
 fn extract_acl_node_data(
     node: &AclTreeNode,
     path: Option<&str>,
-    exact: bool,
+    exact_authid: bool,
+    exact_path: bool,
     auth_id_filter: &Option<Authid>,
 ) -> Vec<AclListItem> {
     // tokens can't have tokens, so we can early return
@@ -259,7 +280,11 @@ fn extract_acl_node_data(
 
         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() {
+                if exact_authid {
+                    if user != auth_id_filter {
+                        continue;
+                    }
+                } else if !user.is_token() || user.user() != auth_id_filter.user() {
                     continue;
                 }
             }
@@ -291,7 +316,7 @@ fn extract_acl_node_data(
             }
         }
 
-        if !exact {
+        if !exact_path {
             nodes.extend(
                 node.children
                     .iter()
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH yew-comp v2 1/2] acl_context: add AclContext and AclContextProvider
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
                   ` (3 preceding siblings ...)
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 4/4] access-control: allow reading all acls of the current authid Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-23 10:00   ` Dominik Csapak
  2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 2/2] http_helpers: reload LocalAclTree when logging in or refreshing a ticket Shannon Sterz
                   ` (4 subsequent siblings)
  9 siblings, 1 reply; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

these components allow an application to provide a context that
compononets can use to check the privileges of the current user. thus,
they can omit ui elements if the user lacks the permissions to use
them.

by using a context, all components that use it will get reactively
re-rendered if the context changes.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 Cargo.toml         |   2 +-
 src/acl_context.rs | 204 +++++++++++++++++++++++++++++++++++++++++++++
 src/lib.rs         |   3 +
 3 files changed, 208 insertions(+), 1 deletion(-)
 create mode 100644 src/acl_context.rs

diff --git a/Cargo.toml b/Cargo.toml
index dc06ccc..520cb71 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -79,7 +79,7 @@ proxmox-auth-api = { version = "1", default-features = false, features = [
   "api-types",
 ] }
 proxmox-apt-api-types = { version = "2.0", optional = true }
-proxmox-access-control = "1.1"
+proxmox-access-control = { version = "1.1", features = ["acl"]}
 proxmox-dns-api = { version = "1", optional = true }
 proxmox-network-api = { version = "1", optional = true }
 
diff --git a/src/acl_context.rs b/src/acl_context.rs
new file mode 100644
index 0000000..7cfa450
--- /dev/null
+++ b/src/acl_context.rs
@@ -0,0 +1,204 @@
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use serde::{Deserialize, Serialize};
+use yew::prelude::*;
+
+use proxmox_access_control::acl::AclTree;
+use proxmox_access_control::types::{AclListItem, AclUgidType};
+use pwt::state::PersistentState;
+use pwt::AsyncAbortGuard;
+
+use pbs_api_types::Authid;
+
+use crate::CLIENT;
+
+thread_local! {
+    // Set by the current `AclContextProvider`, only one `AclContextProvider` should be used at a
+    // time. `LocalAclTree::load()` will use this callback, if present, to inform the `AclContext`
+    // that a new `AclTree` has been loaded. If the tree is different from the previously used
+    // tree, all components using the `AclContext` will be re-rendered with the new information.
+    static ACL_TREE_UPDATE_CB: Rc<RefCell<Option<Callback<Rc<AclTree>>>>> = Rc::new(RefCell::new(None));
+}
+
+#[derive(Clone)]
+pub struct AclContext {
+    acl_tree: UseReducerHandle<LocalAclTree>,
+    _abort_guard: Rc<AsyncAbortGuard>,
+}
+
+impl AclContext {
+    /// Allows checking whether a users has sufficient privileges for a given ACL path.
+    ///
+    /// # Panics
+    ///
+    /// Requires that the access control configuration is initialized via
+    /// `proxmox_access_control::init::init_access_config` and will panic otherwise.
+    pub fn check_privs(&self, path: &[&str], required_privs: u64) -> bool {
+        self.acl_tree.check_privs(path, required_privs)
+    }
+
+    /// Allows checking whether a user has any of the specified privileges under a certain ACL path.
+    ///
+    /// # Panics
+    ///
+    /// Requires that the access control configuration is initialized via
+    /// `proxmox_access_control::init::init_access_config` and will panic otherwise.
+    pub fn any_privs_below(&self, path: &[&str], required_privs: u64) -> bool {
+        self.acl_tree.any_privs_below(path, required_privs)
+    }
+}
+
+// Needed for yew to determine whether components using the context need re-rendering. Only the
+// AclTree matters here, so ignore the other fields.
+impl PartialEq for AclContext {
+    fn eq(&self, other: &Self) -> bool {
+        self.acl_tree.eq(&other.acl_tree)
+    }
+}
+
+#[derive(Properties, Debug, PartialEq)]
+pub struct AclContextProviderProps {
+    #[prop_or_default]
+    pub children: Html,
+}
+
+#[function_component]
+pub fn AclContextProvider(props: &AclContextProviderProps) -> Html {
+    let reduce_handle = use_reducer_eq(LocalAclTree::new);
+    let acl_tree = reduce_handle.clone();
+
+    ACL_TREE_UPDATE_CB.with(|cb| {
+        cb.replace(Some(Callback::from(move |tree: Rc<AclTree>| {
+            reduce_handle.dispatch(tree);
+        })));
+    });
+
+    let context = AclContext {
+        acl_tree,
+        _abort_guard: Rc::new(AsyncAbortGuard::spawn(
+            async move { LocalAclTree::load().await },
+        )),
+    };
+
+    html!(
+        <ContextProvider<AclContext> context={context} >
+            {props.children.clone()}
+        </ContextProvider<AclContext>>
+    )
+}
+
+#[derive(Clone, PartialEq)]
+pub(crate) struct LocalAclTree {
+    acl_tree: Rc<AclTree>,
+}
+
+impl LocalAclTree {
+    const LOCAL_KEY: &str = "ProxmoxLocalAclTree";
+
+    /// Create a new `LocalAclTree` from the local storage. If no previous tree was persisted, an
+    /// empty tree will be used by default.
+    fn new() -> Self {
+        let saved_tree: PersistentState<SavedAclNodes> = PersistentState::new(Self::LOCAL_KEY);
+
+        LocalAclTree {
+            acl_tree: Rc::new((&saved_tree.into_inner()).into()),
+        }
+    }
+
+    fn check_privs(&self, path: &[&str], required_privs: u64) -> bool {
+        let Some(auth_id) = Self::get_current_authid() else {
+            log::error!("Could not get current user's authid, cannot check permissions.");
+            return false;
+        };
+
+        self.acl_tree
+            .check_privs(&auth_id, path, required_privs, true)
+            .is_ok()
+    }
+
+    fn any_privs_below(&self, path: &[&str], required_privs: u64) -> bool {
+        let Some(auth_id) = Self::get_current_authid() else {
+            log::error!("Could not get current user's authid, cannot check permissions.");
+            return false;
+        };
+
+        self.acl_tree
+            .any_privs_below(&auth_id, path, required_privs)
+            .unwrap_or_default()
+    }
+
+    /// Loads the currently logged in user's ACL list entries and assembles a local ACL tree. On
+    /// successful load, a copy will be persisted to local storage. If `ACL_TREE_UPDATE_CB`
+    /// contains a callback, it will be used to update the current `AclContext`.
+    pub(crate) async fn load() {
+        let Some(authid) = Self::get_current_authid() else {
+            log::error!("Could not get current Authid, please login first.");
+            return;
+        };
+
+        let nodes: Vec<AclListItem> =
+            match crate::http_get("/access/acl?exact-authid=true", None).await {
+                Ok(nodes) => nodes,
+                Err(e) => {
+                    log::error!("Could not load acl tree - {e:#}");
+                    return;
+                }
+            };
+
+        let to_save = SavedAclNodes {
+            authid: Some(authid),
+            nodes,
+        };
+
+        if let Some(ref cb) = *ACL_TREE_UPDATE_CB.with(|t| t.clone()).borrow() {
+            cb.emit(Rc::new((&to_save).into()));
+        }
+
+        let mut saved_tree: PersistentState<SavedAclNodes> = PersistentState::new(Self::LOCAL_KEY);
+        saved_tree.update(to_save);
+    }
+
+    fn get_current_authid() -> Option<Authid> {
+        let authid = CLIENT.with_borrow(|t| t.get_auth())?;
+        authid.userid.parse::<Authid>().ok()
+    }
+}
+
+impl Reducible for LocalAclTree {
+    type Action = Rc<AclTree>;
+
+    fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
+        Rc::new(Self { acl_tree: action })
+    }
+}
+
+#[derive(Deserialize, Serialize, PartialEq, Clone, Default)]
+struct SavedAclNodes {
+    authid: Option<Authid>,
+    nodes: Vec<AclListItem>,
+}
+
+impl From<&SavedAclNodes> for AclTree {
+    fn from(value: &SavedAclNodes) -> Self {
+        let mut tree = AclTree::new();
+
+        if let Some(ref authid) = value.authid {
+            for entry in &value.nodes {
+                match entry.ugid_type {
+                    AclUgidType::User => {
+                        tree.insert_user_role(&entry.path, authid, &entry.roleid, entry.propagate)
+                    }
+                    AclUgidType::Group => tree.insert_group_role(
+                        &entry.path,
+                        &entry.ugid,
+                        &entry.roleid,
+                        entry.propagate,
+                    ),
+                }
+            }
+        }
+
+        tree
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 85e2b60..a0b5772 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,8 @@
 pub mod acme;
 
+mod acl_context;
+pub use acl_context::{AclContext, AclContextProvider};
+
 mod api_load_callback;
 pub use api_load_callback::{ApiLoadCallback, IntoApiLoadCallback};
 
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH yew-comp v2 2/2] http_helpers: reload LocalAclTree when logging in or refreshing a ticket
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
                   ` (4 preceding siblings ...)
  2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 1/2] acl_context: add AclContext and AclContextProvider Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] server/api-types: move AccessControlConfig to shared api types Shannon Sterz
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

so the ui renders ui elements appropriatelly for a newly authenticated
user or if the user's permissions have changed since the last ticket
refresh.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 src/http_helpers.rs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/http_helpers.rs b/src/http_helpers.rs
index 94cc078..ed6671f 100644
--- a/src/http_helpers.rs
+++ b/src/http_helpers.rs
@@ -15,6 +15,7 @@ use proxmox_client::HttpApiClient;
 use proxmox_login::{ticket::Validity, Authentication, TicketResult};
 use yew::Callback;
 
+use crate::acl_context::LocalAclTree;
 use crate::{json_object_to_query, ExistingProduct, HttpClientWasm, ProjectInfo};
 
 static LAST_NOTIFY_EPOCH: AtomicU32 = AtomicU32::new(0);
@@ -129,6 +130,7 @@ async fn ticket_refresh_loop() {
                         Ok(TicketResult::Full(auth)) | Ok(TicketResult::HttpOnly(auth)) => {
                             log::info!("ticket_refresh_loop: Got ticket update.");
                             client.set_auth(auth.clone());
+                            LocalAclTree::load().await;
                         }
                         _ => { /* do nothing */ }
                     }
@@ -189,11 +191,13 @@ pub async fn http_login(
         TicketResult::Full(auth) => {
             client.set_auth(auth.clone());
             update_global_client(client);
+            LocalAclTree::load().await;
             Ok(TicketResult::Full(auth))
         }
         TicketResult::HttpOnly(auth) => {
             client.set_auth(auth.clone());
             update_global_client(client);
+            LocalAclTree::load().await;
             Ok(TicketResult::HttpOnly(auth))
         }
         challenge => Ok(challenge),
@@ -209,6 +213,7 @@ pub async fn http_login_tfa(
     let auth = client.login_tfa(challenge, request).await?;
     client.set_auth(auth.clone());
     update_global_client(client);
+    LocalAclTree::load().await;
     Ok(auth)
 }
 
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 1/2] server/api-types: move AccessControlConfig to shared api types
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
                   ` (5 preceding siblings ...)
  2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 2/2] http_helpers: reload LocalAclTree when logging in or refreshing a ticket Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: add an AclContext via the AclContextProvider to the main app ui Shannon Sterz
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

this doesn't really contain any information that is secret. moving it
to the shared api types allows re-using it in the ui to check what
privileges a user needs to access certain features.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 lib/pdm-api-types/Cargo.toml |   1 +
 lib/pdm-api-types/src/acl.rs | 158 ++++++++++++++++++++++++++++++++++
 server/src/acl.rs            | 162 +----------------------------------
 3 files changed, 161 insertions(+), 160 deletions(-)

diff --git a/lib/pdm-api-types/Cargo.toml b/lib/pdm-api-types/Cargo.toml
index e66558b..4b0edde 100644
--- a/lib/pdm-api-types/Cargo.toml
+++ b/lib/pdm-api-types/Cargo.toml
@@ -14,6 +14,7 @@ serde.workspace = true
 serde_plain.workspace = true
 
 proxmox-acme-api.workspace = true
+proxmox-access-control.workspace = true
 proxmox-auth-api = { workspace = true, features = ["api-types"] }
 proxmox-lang.workspace = true
 proxmox-config-digest.workspace = true
diff --git a/lib/pdm-api-types/src/acl.rs b/lib/pdm-api-types/src/acl.rs
index 9e69c2f..baba3da 100644
--- a/lib/pdm-api-types/src/acl.rs
+++ b/lib/pdm-api-types/src/acl.rs
@@ -1,6 +1,12 @@
+use std::collections::HashMap;
 use std::str::FromStr;
+use std::sync::LazyLock;
 
+use anyhow::{format_err, Context, Error};
 use const_format::concatcp;
+use proxmox_access_control::types::User;
+use proxmox_auth_api::types::Authid;
+use proxmox_section_config::SectionConfigData;
 use serde::de::{value, IntoDeserializer};
 use serde::{Deserialize, Serialize};
 
@@ -179,3 +185,155 @@ pub struct AclListItem {
     pub propagate: bool,
     pub roleid: String,
 }
+
+pub struct AccessControlConfig;
+
+impl proxmox_access_control::init::AccessControlConfig for AccessControlConfig {
+    fn privileges(&self) -> &HashMap<&str, u64> {
+        static PRIVS: LazyLock<HashMap<&str, u64>> =
+            LazyLock::new(|| PRIVILEGES.iter().copied().collect());
+
+        &PRIVS
+    }
+
+    #[rustfmt::skip]
+    fn roles(&self) -> &HashMap<&str, (u64, &str)> {
+        static ROLES: LazyLock<HashMap<&str, (u64, &str)>> = LazyLock::new(|| {
+            [
+                ("Administrator", (ROLE_ADMINISTRATOR, "Administrators can inspect and modify the system.")),
+                ("Auditor", (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),
+                //("ResourceAuditor", pdm_api_types::ROLE_RESOURCE_AUDITOR),
+                //("AccessAuditor", pdm_api_types::ROLE_ACCESS_AUDITOR),
+            ]
+            .into_iter()
+            .collect()
+        });
+
+        &ROLES
+    }
+
+    fn is_superuser(&self, auth_id: &Authid) -> bool {
+        !auth_id.is_token() && auth_id.user() == "root@pam"
+    }
+
+    fn role_admin(&self) -> Option<&str> {
+        Some("Administrator")
+    }
+
+    fn init_user_config(&self, config: &mut SectionConfigData) -> Result<(), Error> {
+        if !config.sections.contains_key("root@pam") {
+            config
+                .set_data(
+                    "root@pam",
+                    "user",
+                    User {
+                        userid: "root@pam".parse().expect("invalid user id"),
+                        comment: Some("Superuser".to_string()),
+                        enable: None,
+                        expire: None,
+                        firstname: None,
+                        lastname: None,
+                        email: None,
+                    },
+                )
+                .context("failed to insert default user into user config")?
+        }
+
+        Ok(())
+    }
+
+    fn acl_audit_privileges(&self) -> u64 {
+        PRIV_ACCESS_AUDIT
+    }
+
+    fn acl_modify_privileges(&self) -> u64 {
+        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))
+    }
+}
diff --git a/server/src/acl.rs b/server/src/acl.rs
index 52a1f97..f421814 100644
--- a/server/src/acl.rs
+++ b/server/src/acl.rs
@@ -1,164 +1,6 @@
-use std::collections::HashMap;
-use std::sync::OnceLock;
-
-use anyhow::{format_err, Context as _, Error};
-
-use proxmox_access_control::types::User;
-use proxmox_auth_api::types::Authid;
-use proxmox_section_config::SectionConfigData;
-
-struct AccessControlConfig;
-
-static PRIVILEGES: 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> {
-        PRIVILEGES.get_or_init(|| pdm_api_types::PRIVILEGES.iter().copied().collect())
-    }
-
-    #[rustfmt::skip]
-    fn roles(&self) -> &HashMap<&str, (u64, &str)> {
-        ROLES.get_or_init(|| {
-            [
-                ("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),
-                //("ResourceAuditor", pdm_api_types::ROLE_RESOURCE_AUDITOR),
-                //("AccessAuditor", pdm_api_types::ROLE_ACCESS_AUDITOR),
-            ]
-            .into_iter()
-            .collect()
-        })
-    }
-
-    fn is_superuser(&self, auth_id: &Authid) -> bool {
-        !auth_id.is_token() && auth_id.user() == "root@pam"
-    }
-
-    fn role_admin(&self) -> Option<&str> {
-        Some("Administrator")
-    }
-
-    fn init_user_config(&self, config: &mut SectionConfigData) -> Result<(), Error> {
-        if !config.sections.contains_key("root@pam") {
-            config
-                .set_data(
-                    "root@pam",
-                    "user",
-                    User {
-                        userid: "root@pam".parse().expect("invalid user id"),
-                        comment: Some("Superuser".to_string()),
-                        enable: None,
-                        expire: None,
-                        firstname: None,
-                        lastname: None,
-                        email: None,
-                    },
-                )
-                .context("failed to insert default user into user config")?
-        }
-
-        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() {
-    static ACCESS_CONTROL_CONFIG: AccessControlConfig = AccessControlConfig;
+    static ACCESS_CONTROL_CONFIG: pdm_api_types::AccessControlConfig =
+        pdm_api_types::AccessControlConfig;
 
     proxmox_access_control::init::init(&ACCESS_CONTROL_CONFIG, pdm_buildcfg::configdir!("/access"))
         .expect("failed to setup access control config");
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: add an AclContext via the AclContextProvider to the main app ui
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
                   ` (6 preceding siblings ...)
  2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] server/api-types: move AccessControlConfig to shared api types Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 1/1] notes view: allow hiding the toolbar if editing isn't supported Shannon Sterz
  2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 1/1] ui: main menu: use the AclContext to hide the Notes if appropriate Shannon Sterz
  9 siblings, 0 replies; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

by adding an `AclContextProvider` each component below can query the
current `AclContext` and get re-rendered if it changes.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 ui/Cargo.toml  |  1 +
 ui/src/main.rs | 14 +++++++++++---
 2 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/ui/Cargo.toml b/ui/Cargo.toml
index 526380e..a901ff0 100644
--- a/ui/Cargo.toml
+++ b/ui/Cargo.toml
@@ -38,6 +38,7 @@ proxmox-human-byte = "1"
 proxmox-login = "1"
 proxmox-schema = "5"
 proxmox-rrd-api-types = "1"
+proxmox-access-control = { version = "1.1", features = []}
 pbs-api-types = "1.0.3"
 
 pdm-api-types = { version = "0.9", path = "../lib/pdm-api-types" }
diff --git a/ui/src/main.rs b/ui/src/main.rs
index 715ecec..3465d24 100644
--- a/ui/src/main.rs
+++ b/ui/src/main.rs
@@ -17,8 +17,8 @@ use pbs_api_types::TaskListItem;
 use proxmox_login::Authentication;
 use proxmox_yew_comp::utils::init_task_descr_table_base;
 use proxmox_yew_comp::{
-    authentication_from_cookie, http_get, register_auth_observer, AuthObserver, LoginPanel,
-    SubscriptionAlert,
+    authentication_from_cookie, http_get, register_auth_observer, AclContextProvider, AuthObserver,
+    LoginPanel, SubscriptionAlert,
 };
 
 //use pbs::MainMenu;
@@ -293,7 +293,9 @@ impl Component for DatacenterManagerApp {
         DesktopApp::new(html! {
             <ContextProvider<SearchProvider> context={search_context}>
                 <ContextProvider<RemoteList> {context}>
-                    {body}
+                    <AclContextProvider>
+                        {body}
+                    </AclContextProvider>
                 </ContextProvider<RemoteList>>
             </ContextProvider<SearchProvider>>
         })
@@ -342,5 +344,11 @@ fn main() {
 
     pwt::state::set_available_languages(proxmox_yew_comp::available_language_list());
 
+    if let Err(e) =
+        proxmox_access_control::init::init_access_config(&pdm_api_types::AccessControlConfig)
+    {
+        log::error!("could not initialize access control config - {e:#}");
+    }
+
     yew::Renderer::<DatacenterManagerApp>::new().render();
 }
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH yew-comp v2 1/1] notes view: allow hiding the toolbar if editing isn't supported
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
                   ` (7 preceding siblings ...)
  2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: add an AclContext via the AclContextProvider to the main app ui Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  2025-10-23  9:36   ` Dominik Csapak
  2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 1/1] ui: main menu: use the AclContext to hide the Notes if appropriate Shannon Sterz
  9 siblings, 1 reply; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
alternatively we could also pass down the path and permissions for the
the check and hook this component into the context itself. since we are
talking about a single boolean check here, i though this approach
provides less overhead and tried to keep it simple.

 src/notes_view.rs | 36 +++++++++++++++++++++++-------------
 1 file changed, 23 insertions(+), 13 deletions(-)

diff --git a/src/notes_view.rs b/src/notes_view.rs
index 315163e..9806d63 100644
--- a/src/notes_view.rs
+++ b/src/notes_view.rs
@@ -6,6 +6,7 @@ use anyhow::Error;
 use serde::{Deserialize, Serialize};
 use serde_json::{json, Value};

+use yew::html::IntoPropValue;
 use yew::virtual_dom::{VComp, VNode};

 use pwt::prelude::*;
@@ -64,6 +65,10 @@ pub struct NotesView {
     #[builder_cb(IntoSubmitCallback, into_submit_callback, NotesWithDigest)]
     #[prop_or_default]
     pub on_submit: Option<SubmitCallback<NotesWithDigest>>,
+
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or(true)]
+    pub allow_editing: bool,
 }

 impl NotesView {
@@ -163,20 +168,25 @@ impl LoadableComponent for ProxmoxNotesView {
     }
     fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
         let props = ctx.props();
-        let toolbar = Toolbar::new()
-            .class("pwt-w-100")
-            .class("pwt-overflow-hidden")
-            .class("pwt-border-bottom")
-            .with_child(
-                Button::new(tr!("Edit"))
-                    .disabled(props.on_submit.is_none())
-                    .onclick(
-                        ctx.link()
-                            .change_view_callback(|_| Some(ViewState::EditNotes)),
-                    ),
-            );

-        Some(toolbar.into())
+        if props.allow_editing {
+            let toolbar = Toolbar::new()
+                .class("pwt-w-100")
+                .class("pwt-overflow-hidden")
+                .class("pwt-border-bottom")
+                .with_child(
+                    Button::new(tr!("Edit"))
+                        .disabled(props.on_submit.is_none())
+                        .onclick(
+                            ctx.link()
+                                .change_view_callback(|_| Some(ViewState::EditNotes)),
+                        ),
+                );
+
+            return Some(toolbar.into());
+        }
+
+        None
     }

     fn main_view(&self, _ctx: &LoadableComponentContext<Self>) -> Html {
--
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 1/1] ui: main menu: use the AclContext to hide the Notes if appropriate
  2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
                   ` (8 preceding siblings ...)
  2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 1/1] notes view: allow hiding the toolbar if editing isn't supported Shannon Sterz
@ 2025-10-22 13:11 ` Shannon Sterz
  9 siblings, 0 replies; 15+ messages in thread
From: Shannon Sterz @ 2025-10-22 13:11 UTC (permalink / raw)
  To: pdm-devel

a user that does not have `PRIV_SYS_AUDIT` on `/system` is not allowed
to view the notes and one that lacks `PRIV_SYS_MODIFY` on
`/system/notes` is not allowed to edit them. so hide the respective ui
elements when a user does not have the necessary permissions.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 ui/src/main_menu.rs | 66 +++++++++++++++++++++++++++++++--------------
 1 file changed, 46 insertions(+), 20 deletions(-)

diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs
index 7650b63..c411367 100644
--- a/ui/src/main_menu.rs
+++ b/ui/src/main_menu.rs
@@ -9,9 +9,10 @@ use pwt::state::Selection;
 use pwt::widget::nav::{Menu, MenuItem, NavigationDrawer};
 use pwt::widget::{Container, Row, SelectionView, SelectionViewRenderInfo};

-use proxmox_yew_comp::{NotesView, XTermJs};
+use proxmox_yew_comp::{AclContext, NotesView, XTermJs};

 use pdm_api_types::remotes::RemoteType;
+use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};

 use crate::remotes::RemotesPanel;
 use crate::sdn::evpn::EvpnPanel;
@@ -62,11 +63,14 @@ impl MainMenu {

 pub enum Msg {
     Select(Key),
+    UpdateAcl(AclContext),
 }

 pub struct PdmMainMenu {
     active: Key,
     menu_selection: Selection,
+    acl_context: AclContext,
+    _acl_context_listener: ContextHandle<AclContext>,
 }

 fn register_view(
@@ -109,10 +113,17 @@ impl Component for PdmMainMenu {
     type Message = Msg;
     type Properties = MainMenu;

-    fn create(_ctx: &Context<Self>) -> Self {
+    fn create(ctx: &Context<Self>) -> Self {
+        let (acl_context, acl_context_listener) = ctx
+            .link()
+            .context(ctx.link().callback(Msg::UpdateAcl))
+            .expect("acl context not present");
+
         Self {
             active: Key::from("dashboard"),
             menu_selection: Selection::new(),
+            acl_context,
+            _acl_context_listener: acl_context_listener,
         }
     }

@@ -122,6 +133,10 @@ impl Component for PdmMainMenu {
                 self.active = key;
                 true
             }
+            Msg::UpdateAcl(acl_context) => {
+                self.acl_context = acl_context;
+                true
+            }
         }
     }

@@ -144,25 +159,36 @@ impl Component for PdmMainMenu {
             move |_| Dashboard::new().into(),
         );

-        register_view(
-            &mut menu,
-            &mut content,
-            tr!("Notes"),
-            "notes",
-            Some("fa fa-sticky-note-o"),
-            move |_| {
-                let notes = NotesView::new("/config/notes").on_submit(|notes| async move {
-                    proxmox_yew_comp::http_put("/config/notes", Some(serde_json::to_value(&notes)?))
-                        .await
-                });
+        if self.acl_context.check_privs(&["system"], PRIV_SYS_AUDIT) {
+            let allow_editing = self
+                .acl_context
+                .check_privs(&["system", "notes"], PRIV_SYS_MODIFY);

-                Container::new()
-                    .class("pwt-content-spacer")
-                    .class(pwt::css::FlexFit)
-                    .with_child(notes)
-                    .into()
-            },
-        );
+            register_view(
+                &mut menu,
+                &mut content,
+                tr!("Notes"),
+                "notes",
+                Some("fa fa-sticky-note-o"),
+                move |_| {
+                    let notes = NotesView::new("/config/notes")
+                        .on_submit(|notes| async move {
+                            proxmox_yew_comp::http_put(
+                                "/config/notes",
+                                Some(serde_json::to_value(&notes)?),
+                            )
+                            .await
+                        })
+                        .allow_editing(allow_editing);
+
+                    Container::new()
+                        .class("pwt-content-spacer")
+                        .class(pwt::css::FlexFit)
+                        .with_child(notes)
+                        .into()
+                },
+            )
+        }

         let mut config_submenu = Menu::new();

--
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [pdm-devel] [PATCH proxmox v2 1/4] access-control: add acl feature to only expose types and the AclTree
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 1/4] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
@ 2025-10-23  9:24   ` Dominik Csapak
  0 siblings, 0 replies; 15+ messages in thread
From: Dominik Csapak @ 2025-10-23  9:24 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Shannon Sterz

one tiny nit inline

otherwise looks good to me

Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>

On 10/22/25 3:11 PM, Shannon Sterz wrote:
> this is useful, when an application wants to only handle an acl tree
> without depending on more complex features provided by the rest of the
> `impl` feature or its bigger dependencies (e.g. openssl).
> 
> Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
> ---
>   proxmox-access-control/Cargo.toml  |  6 +++++-
>   proxmox-access-control/src/acl.rs  | 19 ++++++++++++++++++-
>   proxmox-access-control/src/init.rs | 13 ++++++++++++-
>   proxmox-access-control/src/lib.rs  |  4 ++--
>   4 files changed, 37 insertions(+), 5 deletions(-)
> 
> diff --git a/proxmox-access-control/Cargo.toml b/proxmox-access-control/Cargo.toml
> index d23d272d..0eaf9fdf 100644
> --- a/proxmox-access-control/Cargo.toml
> +++ b/proxmox-access-control/Cargo.toml
> @@ -35,18 +35,22 @@ proxmox-uuid = { workspace = true }
>   
>   [features]
>   default = []
> +acl = [
> +    "dep:proxmox-section-config",
> +]
>   api = [
>       "impl",
>       "dep:hex",
>   ]
>   impl = [
> +    "acl",
>       "dep:nix",
>       "dep:openssl",
>       "dep:proxmox-config-digest",
>       "dep:proxmox-product-config",
>       "dep:proxmox-router",
> -    "dep:proxmox-section-config",
>       "dep:proxmox-shared-memory",
>       "dep:proxmox-sys",
>       "dep:serde_json",
>   ]
> +
> diff --git a/proxmox-access-control/src/acl.rs b/proxmox-access-control/src/acl.rs
> index 270292ac..877b003c 100644
> --- a/proxmox-access-control/src/acl.rs
> +++ b/proxmox-access-control/src/acl.rs
> @@ -1,15 +1,25 @@
> -use std::collections::{BTreeMap, BTreeSet, HashMap};
> +#[cfg(feature = "impl")]
> +use std::collections::BTreeSet;
> +use std::collections::{BTreeMap, HashMap};
> +#[cfg(feature = "impl")]
>   use std::io::Write;
> +#[cfg(feature = "impl")]
>   use std::path::Path;
> +#[cfg(feature = "impl")]
>   use std::sync::{Arc, OnceLock, RwLock};
>   
>   use anyhow::{bail, Error};
>   
>   use proxmox_auth_api::types::{Authid, Userid};
> +#[cfg(feature = "impl")]
>   use proxmox_config_digest::ConfigDigest;
> +#[cfg(feature = "impl")]
>   use proxmox_product_config::{open_api_lockfile, replace_privileged_config, ApiLockGuard};
>   
>   use crate::init::{access_conf, acl_config, acl_config_lock};
> +use crate::init::access_conf;

this now imports 'access_conf' twice, probably that line would belong to 
the next patch?

> +#[cfg(feature = "impl")]
> +use crate::init::{acl_config, acl_config_lock};
>   
>   pub fn split_acl_path(path: &str) -> Vec<&str> {
>       let items = path.split('/');
> @@ -302,6 +312,7 @@ impl AclTree {
>           node.insert_user_role(auth_id.to_owned(), role.to_string(), propagate);
>       }
>   
> +    #[cfg(feature = "impl")]
>       fn write_node_config(node: &AclTreeNode, path: &str, w: &mut dyn Write) -> Result<(), Error> {
>           let mut role_ug_map0: HashMap<_, BTreeSet<_>> = HashMap::new();
>           let mut role_ug_map1: HashMap<_, BTreeSet<_>> = HashMap::new();
> @@ -402,6 +413,7 @@ impl AclTree {
>           Ok(())
>       }
>   
> +    #[cfg(feature = "impl")]
>       fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
>           Self::write_node_config(&self.root, "", w)
>       }
> @@ -449,6 +461,7 @@ impl AclTree {
>           Ok(())
>       }
>   
> +    #[cfg(feature = "impl")]
>       fn load(filename: &Path) -> Result<(Self, ConfigDigest), Error> {
>           let mut tree = Self::new();
>   
> @@ -553,11 +566,13 @@ impl AclTree {
>   }
>   
>   /// Get exclusive lock
> +#[cfg(feature = "impl")]
>   pub fn lock_config() -> Result<ApiLockGuard, Error> {
>       open_api_lockfile(acl_config_lock(), None, true)
>   }
>   
>   /// Reads the [`AclTree`] from `acl.cfg` in the configuration directory.
> +#[cfg(feature = "impl")]
>   pub fn config() -> Result<(AclTree, ConfigDigest), Error> {
>       let path = acl_config();
>       AclTree::load(&path)
> @@ -568,6 +583,7 @@ pub fn config() -> Result<(AclTree, ConfigDigest), Error> {
>   ///
>   /// Since the AclTree is used for every API request's permission check, this caching mechanism
>   /// allows to skip reading and parsing the file again if it is unchanged.
> +#[cfg(feature = "impl")]
>   pub fn cached_config() -> Result<Arc<AclTree>, Error> {
>       struct ConfigCache {
>           data: Option<Arc<AclTree>>,
> @@ -621,6 +637,7 @@ pub fn cached_config() -> Result<Arc<AclTree>, Error> {
>   
>   /// Saves an [`AclTree`] to `acl.cfg` in the configuration directory, ensuring proper ownership and
>   /// file permissions.
> +#[cfg(feature = "impl")]
>   pub fn save_config(acl: &AclTree) -> Result<(), Error> {
>       let mut raw: Vec<u8> = Vec::new();
>       acl.write_config(&mut raw)?;
> diff --git a/proxmox-access-control/src/init.rs b/proxmox-access-control/src/init.rs
> index 39a12352..cf5f795d 100644
> --- a/proxmox-access-control/src/init.rs
> +++ b/proxmox-access-control/src/init.rs
> @@ -1,4 +1,5 @@
>   use std::collections::HashMap;
> +#[cfg(feature = "impl")]
>   use std::path::{Path, PathBuf};
>   use std::sync::OnceLock;
>   
> @@ -8,6 +9,7 @@ use proxmox_auth_api::types::{Authid, Userid};
>   use proxmox_section_config::SectionConfigData;
>   
>   static ACCESS_CONF: OnceLock<&'static dyn AccessControlConfig> = OnceLock::new();
> +#[cfg(feature = "impl")]
>   static ACCESS_CONF_DIR: OnceLock<PathBuf> = OnceLock::new();
>   
>   /// This trait specifies the functions a product needs to implement to get ACL tree based access
> @@ -105,6 +107,7 @@ pub trait AccessControlConfig: Send + Sync {
>       }
>   }
>   
> +#[cfg(feature = "impl")]
>   pub fn init<P: AsRef<Path>>(
>       acm_config: &'static dyn AccessControlConfig,
>       config_dir: P,
> @@ -113,13 +116,14 @@ pub fn init<P: AsRef<Path>>(
>       init_access_config_dir(config_dir)
>   }
>   
> +#[cfg(feature = "impl")]
>   pub(crate) fn init_access_config_dir<P: AsRef<Path>>(config_dir: P) -> Result<(), Error> {
>       ACCESS_CONF_DIR
>           .set(config_dir.as_ref().to_owned())
>           .map_err(|_e| format_err!("cannot initialize acl tree config twice!"))
>   }
>   
> -pub(crate) fn init_access_config(config: &'static dyn AccessControlConfig) -> Result<(), Error> {
> +pub fn init_access_config(config: &'static dyn AccessControlConfig) -> Result<(), Error> {
>       ACCESS_CONF
>           .set(config)
>           .map_err(|_e| format_err!("cannot initialize acl tree config twice!"))
> @@ -131,32 +135,39 @@ pub(crate) fn access_conf() -> &'static dyn AccessControlConfig {
>           .expect("please initialize the acm config before using it!")
>   }
>   
> +#[cfg(feature = "impl")]
>   fn conf_dir() -> &'static PathBuf {
>       ACCESS_CONF_DIR
>           .get()
>           .expect("please initialize acm config dir before using it!")
>   }
>   
> +#[cfg(feature = "impl")]
>   pub(crate) fn acl_config() -> PathBuf {
>       conf_dir().join("acl.cfg")
>   }
>   
> +#[cfg(feature = "impl")]
>   pub(crate) fn acl_config_lock() -> PathBuf {
>       conf_dir().join(".acl.lck")
>   }
>   
> +#[cfg(feature = "impl")]
>   pub(crate) fn user_config() -> PathBuf {
>       conf_dir().join("user.cfg")
>   }
>   
> +#[cfg(feature = "impl")]
>   pub(crate) fn user_config_lock() -> PathBuf {
>       conf_dir().join(".user.lck")
>   }
>   
> +#[cfg(feature = "impl")]
>   pub(crate) fn token_shadow() -> PathBuf {
>       conf_dir().join("token.shadow")
>   }
>   
> +#[cfg(feature = "impl")]
>   pub(crate) fn token_shadow_lock() -> PathBuf {
>       conf_dir().join("token.shadow.lock")
>   }
> diff --git a/proxmox-access-control/src/lib.rs b/proxmox-access-control/src/lib.rs
> index 62683924..9195c999 100644
> --- a/proxmox-access-control/src/lib.rs
> +++ b/proxmox-access-control/src/lib.rs
> @@ -2,13 +2,13 @@
>   
>   pub mod types;
>   
> -#[cfg(feature = "impl")]
> +#[cfg(feature = "acl")]
>   pub mod acl;
>   
>   #[cfg(feature = "api")]
>   pub mod api;
>   
> -#[cfg(feature = "impl")]
> +#[cfg(feature = "acl")]
>   pub mod init;
>   
>   #[cfg(feature = "impl")]



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [pdm-devel] [PATCH proxmox v2 4/4] access-control: allow reading all acls of the current authid
  2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 4/4] access-control: allow reading all acls of the current authid Shannon Sterz
@ 2025-10-23  9:31   ` Dominik Csapak
  0 siblings, 0 replies; 15+ messages in thread
From: Dominik Csapak @ 2025-10-23  9:31 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Shannon Sterz

High level question:

in proxmox-backup we already have a 'list_permissions' api call

would that (or something like that) not be better suited for the purpose?
(i don't know if it would be easy to refactor that api call here; 
probably not because it's pbs specific)

since with this api call, we'd only have the 'raw' acl entries and
must to calculate things like propagation, etc. on the frontend?

Or am I wrong with that assumption?

On 10/22/25 3:11 PM, Shannon Sterz wrote:
> adds a parameter to the `API_METHOD_READ_ACL` endpoint to allow
> listing all ACL entries of the currently authenticated Authid.
> allowing a user to see their own ACLs does not really exposes any
> additional confidential information. however, being able to query this
> information allows us, for example, to adapt ui components to a users
> capabilities.
> 
> Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
> ---
>   proxmox-access-control/src/api/acl.rs | 37 ++++++++++++++++++++++-----
>   1 file changed, 31 insertions(+), 6 deletions(-)
> 
> diff --git a/proxmox-access-control/src/api/acl.rs b/proxmox-access-control/src/api/acl.rs
> index 0194d517..07222939 100644
> --- a/proxmox-access-control/src/api/acl.rs
> +++ b/proxmox-access-control/src/api/acl.rs
> @@ -23,6 +23,12 @@ use crate::CachedUserInfo;
>                   optional: true,
>                   default: false,
>               },
> +            "exact-authid": {
> +                description: "Whether to return ACL entries for the exact current authid only.",
> +                type: bool,
> +                optional: true,
> +                default: false,
> +            }
>           },
>       },
>       returns: {
> @@ -34,13 +40,17 @@ use crate::CachedUserInfo;
>       },
>       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.",
> +        description: "Returns all ACLs if a user has sufficient privileges on this endpoint. \
> +            Otherwise it is limited to the user's API tokens. However, if `exact-authid` is \
> +            specified, all ACLs of the current Auhtid will be returned, whether the Authid has \
> +            privileges to list other ACLs here or not.",
>       },
>   )]
>   /// Get ACL entries, can be filter by path.
>   pub fn read_acl(
>       path: Option<String>,
>       exact: bool,
> +    exact_authid: bool,
>       rpcenv: &mut dyn RpcEnvironment,
>   ) -> Result<Vec<AclListItem>, Error> {
>       let auth_id = rpcenv
> @@ -58,7 +68,11 @@ pub fn read_acl(
>           )
>           .is_err();
>   
> -    let filter = if filter_entries { Some(auth_id) } else { None };
> +    let filter = if filter_entries || exact_authid {
> +        Some(auth_id)
> +    } else {
> +        None
> +    };
>   
>       let (mut tree, digest) = crate::acl::config()?;
>   
> @@ -74,7 +88,13 @@ pub fn read_acl(
>   
>       rpcenv["digest"] = hex::encode(digest).into();
>   
> -    Ok(extract_acl_node_data(node, path.as_deref(), exact, &filter))
> +    Ok(extract_acl_node_data(
> +        node,
> +        path.as_deref(),
> +        exact_authid,
> +        exact,
> +        &filter,
> +    ))
>   }
>   
>   #[api(
> @@ -241,7 +261,8 @@ pub fn update_acl(
>   fn extract_acl_node_data(
>       node: &AclTreeNode,
>       path: Option<&str>,
> -    exact: bool,
> +    exact_authid: bool,
> +    exact_path: bool,
>       auth_id_filter: &Option<Authid>,
>   ) -> Vec<AclListItem> {
>       // tokens can't have tokens, so we can early return
> @@ -259,7 +280,11 @@ fn extract_acl_node_data(
>   
>           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() {
> +                if exact_authid {
> +                    if user != auth_id_filter {
> +                        continue;
> +                    }
> +                } else if !user.is_token() || user.user() != auth_id_filter.user() {
>                       continue;
>                   }
>               }
> @@ -291,7 +316,7 @@ fn extract_acl_node_data(
>               }
>           }
>   
> -        if !exact {
> +        if !exact_path {
>               nodes.extend(
>                   node.children
>                       .iter()



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [pdm-devel] [PATCH yew-comp v2 1/1] notes view: allow hiding the toolbar if editing isn't supported
  2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 1/1] notes view: allow hiding the toolbar if editing isn't supported Shannon Sterz
@ 2025-10-23  9:36   ` Dominik Csapak
  0 siblings, 0 replies; 15+ messages in thread
From: Dominik Csapak @ 2025-10-23  9:36 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Shannon Sterz

alternatively we could determine this from the 'on_submit' existence?
IMHO it does not make much sense if someone
sets the 'on_submit' call but disables editing, or vice versa

if there is some use case I can't see right now, consider this
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>

On 10/22/25 3:11 PM, Shannon Sterz wrote:
> Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
> ---
> alternatively we could also pass down the path and permissions for the
> the check and hook this component into the context itself. since we are
> talking about a single boolean check here, i though this approach
> provides less overhead and tried to keep it simple.
> 
>   src/notes_view.rs | 36 +++++++++++++++++++++++-------------
>   1 file changed, 23 insertions(+), 13 deletions(-)
> 
> diff --git a/src/notes_view.rs b/src/notes_view.rs
> index 315163e..9806d63 100644
> --- a/src/notes_view.rs
> +++ b/src/notes_view.rs
> @@ -6,6 +6,7 @@ use anyhow::Error;
>   use serde::{Deserialize, Serialize};
>   use serde_json::{json, Value};
> 
> +use yew::html::IntoPropValue;
>   use yew::virtual_dom::{VComp, VNode};
> 
>   use pwt::prelude::*;
> @@ -64,6 +65,10 @@ pub struct NotesView {
>       #[builder_cb(IntoSubmitCallback, into_submit_callback, NotesWithDigest)]
>       #[prop_or_default]
>       pub on_submit: Option<SubmitCallback<NotesWithDigest>>,
> +
> +    #[builder(IntoPropValue, into_prop_value)]
> +    #[prop_or(true)]
> +    pub allow_editing: bool,
>   }
> 
>   impl NotesView {
> @@ -163,20 +168,25 @@ impl LoadableComponent for ProxmoxNotesView {
>       }
>       fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
>           let props = ctx.props();
> -        let toolbar = Toolbar::new()
> -            .class("pwt-w-100")
> -            .class("pwt-overflow-hidden")
> -            .class("pwt-border-bottom")
> -            .with_child(
> -                Button::new(tr!("Edit"))
> -                    .disabled(props.on_submit.is_none())
> -                    .onclick(
> -                        ctx.link()
> -                            .change_view_callback(|_| Some(ViewState::EditNotes)),
> -                    ),
> -            );
> 
> -        Some(toolbar.into())
> +        if props.allow_editing {
> +            let toolbar = Toolbar::new()
> +                .class("pwt-w-100")
> +                .class("pwt-overflow-hidden")
> +                .class("pwt-border-bottom")
> +                .with_child(
> +                    Button::new(tr!("Edit"))
> +                        .disabled(props.on_submit.is_none())
> +                        .onclick(
> +                            ctx.link()
> +                                .change_view_callback(|_| Some(ViewState::EditNotes)),
> +                        ),
> +                );
> +
> +            return Some(toolbar.into());
> +        }
> +
> +        None
>       }
> 
>       fn main_view(&self, _ctx: &LoadableComponentContext<Self>) -> Html {
> --
> 2.47.3
> 
> 
> 
> _______________________________________________
> pdm-devel mailing list
> pdm-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
> 
> 



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [pdm-devel] [PATCH yew-comp v2 1/2] acl_context: add AclContext and AclContextProvider
  2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 1/2] acl_context: add AclContext and AclContextProvider Shannon Sterz
@ 2025-10-23 10:00   ` Dominik Csapak
  0 siblings, 0 replies; 15+ messages in thread
From: Dominik Csapak @ 2025-10-23 10:00 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Shannon Sterz

Code looks ok, but I think there might be an easier way to
achieve a similar result:

Instead of having a new global callback that we update on each auth
change, couldn't we reverse that and have a "simple" component with
context that updates the tree when the auth client changes AFAICS we
already have a 'notify_auth_listener' so we could use that
(maybe we have to trigger that on each update, not sure)

I think such an approach would be
1. less code
2. easier to follow

what do you think?

On 10/22/25 3:11 PM, Shannon Sterz wrote:
> these components allow an application to provide a context that
> compononets can use to check the privileges of the current user. thus,
> they can omit ui elements if the user lacks the permissions to use
> them.
> 
> by using a context, all components that use it will get reactively
> re-rendered if the context changes.
> 
> Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
> ---
>   Cargo.toml         |   2 +-
>   src/acl_context.rs | 204 +++++++++++++++++++++++++++++++++++++++++++++
>   src/lib.rs         |   3 +
>   3 files changed, 208 insertions(+), 1 deletion(-)
>   create mode 100644 src/acl_context.rs
> 
> diff --git a/Cargo.toml b/Cargo.toml
> index dc06ccc..520cb71 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -79,7 +79,7 @@ proxmox-auth-api = { version = "1", default-features = false, features = [
>     "api-types",
>   ] }
>   proxmox-apt-api-types = { version = "2.0", optional = true }
> -proxmox-access-control = "1.1"
> +proxmox-access-control = { version = "1.1", features = ["acl"]}
>   proxmox-dns-api = { version = "1", optional = true }
>   proxmox-network-api = { version = "1", optional = true }
>   
> diff --git a/src/acl_context.rs b/src/acl_context.rs
> new file mode 100644
> index 0000000..7cfa450
> --- /dev/null
> +++ b/src/acl_context.rs
> @@ -0,0 +1,204 @@
> +use std::cell::RefCell;
> +use std::rc::Rc;
> +
> +use serde::{Deserialize, Serialize};
> +use yew::prelude::*;
> +
> +use proxmox_access_control::acl::AclTree;
> +use proxmox_access_control::types::{AclListItem, AclUgidType};
> +use pwt::state::PersistentState;
> +use pwt::AsyncAbortGuard;
> +
> +use pbs_api_types::Authid;
> +
> +use crate::CLIENT;
> +
> +thread_local! {
> +    // Set by the current `AclContextProvider`, only one `AclContextProvider` should be used at a
> +    // time. `LocalAclTree::load()` will use this callback, if present, to inform the `AclContext`
> +    // that a new `AclTree` has been loaded. If the tree is different from the previously used
> +    // tree, all components using the `AclContext` will be re-rendered with the new information.
> +    static ACL_TREE_UPDATE_CB: Rc<RefCell<Option<Callback<Rc<AclTree>>>>> = Rc::new(RefCell::new(None));
> +}
> +
> +#[derive(Clone)]
> +pub struct AclContext {
> +    acl_tree: UseReducerHandle<LocalAclTree>,
> +    _abort_guard: Rc<AsyncAbortGuard>,
> +}
> +
> +impl AclContext {
> +    /// Allows checking whether a users has sufficient privileges for a given ACL path.
> +    ///
> +    /// # Panics
> +    ///
> +    /// Requires that the access control configuration is initialized via
> +    /// `proxmox_access_control::init::init_access_config` and will panic otherwise.
> +    pub fn check_privs(&self, path: &[&str], required_privs: u64) -> bool {
> +        self.acl_tree.check_privs(path, required_privs)
> +    }
> +
> +    /// Allows checking whether a user has any of the specified privileges under a certain ACL path.
> +    ///
> +    /// # Panics
> +    ///
> +    /// Requires that the access control configuration is initialized via
> +    /// `proxmox_access_control::init::init_access_config` and will panic otherwise.
> +    pub fn any_privs_below(&self, path: &[&str], required_privs: u64) -> bool {
> +        self.acl_tree.any_privs_below(path, required_privs)
> +    }
> +}
> +
> +// Needed for yew to determine whether components using the context need re-rendering. Only the
> +// AclTree matters here, so ignore the other fields.
> +impl PartialEq for AclContext {
> +    fn eq(&self, other: &Self) -> bool {
> +        self.acl_tree.eq(&other.acl_tree)
> +    }
> +}
> +
> +#[derive(Properties, Debug, PartialEq)]
> +pub struct AclContextProviderProps {
> +    #[prop_or_default]
> +    pub children: Html,
> +}
> +
> +#[function_component]
> +pub fn AclContextProvider(props: &AclContextProviderProps) -> Html {
> +    let reduce_handle = use_reducer_eq(LocalAclTree::new);
> +    let acl_tree = reduce_handle.clone();
> +
> +    ACL_TREE_UPDATE_CB.with(|cb| {
> +        cb.replace(Some(Callback::from(move |tree: Rc<AclTree>| {
> +            reduce_handle.dispatch(tree);
> +        })));
> +    });
> +
> +    let context = AclContext {
> +        acl_tree,
> +        _abort_guard: Rc::new(AsyncAbortGuard::spawn(
> +            async move { LocalAclTree::load().await },
> +        )),
> +    };
> +
> +    html!(
> +        <ContextProvider<AclContext> context={context} >
> +            {props.children.clone()}
> +        </ContextProvider<AclContext>>
> +    )
> +}
> +
> +#[derive(Clone, PartialEq)]
> +pub(crate) struct LocalAclTree {
> +    acl_tree: Rc<AclTree>,
> +}
> +
> +impl LocalAclTree {
> +    const LOCAL_KEY: &str = "ProxmoxLocalAclTree";
> +
> +    /// Create a new `LocalAclTree` from the local storage. If no previous tree was persisted, an
> +    /// empty tree will be used by default.
> +    fn new() -> Self {
> +        let saved_tree: PersistentState<SavedAclNodes> = PersistentState::new(Self::LOCAL_KEY);
> +
> +        LocalAclTree {
> +            acl_tree: Rc::new((&saved_tree.into_inner()).into()),
> +        }
> +    }
> +
> +    fn check_privs(&self, path: &[&str], required_privs: u64) -> bool {
> +        let Some(auth_id) = Self::get_current_authid() else {
> +            log::error!("Could not get current user's authid, cannot check permissions.");
> +            return false;
> +        };
> +
> +        self.acl_tree
> +            .check_privs(&auth_id, path, required_privs, true)
> +            .is_ok()
> +    }
> +
> +    fn any_privs_below(&self, path: &[&str], required_privs: u64) -> bool {
> +        let Some(auth_id) = Self::get_current_authid() else {
> +            log::error!("Could not get current user's authid, cannot check permissions.");
> +            return false;
> +        };
> +
> +        self.acl_tree
> +            .any_privs_below(&auth_id, path, required_privs)
> +            .unwrap_or_default()
> +    }
> +
> +    /// Loads the currently logged in user's ACL list entries and assembles a local ACL tree. On
> +    /// successful load, a copy will be persisted to local storage. If `ACL_TREE_UPDATE_CB`
> +    /// contains a callback, it will be used to update the current `AclContext`.
> +    pub(crate) async fn load() {
> +        let Some(authid) = Self::get_current_authid() else {
> +            log::error!("Could not get current Authid, please login first.");
> +            return;
> +        };
> +
> +        let nodes: Vec<AclListItem> =
> +            match crate::http_get("/access/acl?exact-authid=true", None).await {
> +                Ok(nodes) => nodes,
> +                Err(e) => {
> +                    log::error!("Could not load acl tree - {e:#}");
> +                    return;
> +                }
> +            };
> +
> +        let to_save = SavedAclNodes {
> +            authid: Some(authid),
> +            nodes,
> +        };
> +
> +        if let Some(ref cb) = *ACL_TREE_UPDATE_CB.with(|t| t.clone()).borrow() {
> +            cb.emit(Rc::new((&to_save).into()));
> +        }
> +
> +        let mut saved_tree: PersistentState<SavedAclNodes> = PersistentState::new(Self::LOCAL_KEY);
> +        saved_tree.update(to_save);
> +    }
> +
> +    fn get_current_authid() -> Option<Authid> {
> +        let authid = CLIENT.with_borrow(|t| t.get_auth())?;
> +        authid.userid.parse::<Authid>().ok()
> +    }
> +}
> +
> +impl Reducible for LocalAclTree {
> +    type Action = Rc<AclTree>;
> +
> +    fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
> +        Rc::new(Self { acl_tree: action })
> +    }
> +}
> +
> +#[derive(Deserialize, Serialize, PartialEq, Clone, Default)]
> +struct SavedAclNodes {
> +    authid: Option<Authid>,
> +    nodes: Vec<AclListItem>,
> +}
> +
> +impl From<&SavedAclNodes> for AclTree {
> +    fn from(value: &SavedAclNodes) -> Self {
> +        let mut tree = AclTree::new();
> +
> +        if let Some(ref authid) = value.authid {
> +            for entry in &value.nodes {
> +                match entry.ugid_type {
> +                    AclUgidType::User => {
> +                        tree.insert_user_role(&entry.path, authid, &entry.roleid, entry.propagate)
> +                    }
> +                    AclUgidType::Group => tree.insert_group_role(
> +                        &entry.path,
> +                        &entry.ugid,
> +                        &entry.roleid,
> +                        entry.propagate,
> +                    ),
> +                }
> +            }
> +        }
> +
> +        tree
> +    }
> +}
> diff --git a/src/lib.rs b/src/lib.rs
> index 85e2b60..a0b5772 100644
> --- a/src/lib.rs
> +++ b/src/lib.rs
> @@ -1,5 +1,8 @@
>   pub mod acme;
>   
> +mod acl_context;
> +pub use acl_context::{AclContext, AclContextProvider};
> +
>   mod api_load_callback;
>   pub use api_load_callback::{ApiLoadCallback, IntoApiLoadCallback};
>   



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2025-10-23 10:00 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-10-22 13:11 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v2 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 1/4] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
2025-10-23  9:24   ` Dominik Csapak
2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 2/4] access-control: move functions querying privileges to " Shannon Sterz
2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 3/4] access-control: derive Debug and PartialEq on AclTree and AclTreeNode Shannon Sterz
2025-10-22 13:11 ` [pdm-devel] [PATCH proxmox v2 4/4] access-control: allow reading all acls of the current authid Shannon Sterz
2025-10-23  9:31   ` Dominik Csapak
2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 1/2] acl_context: add AclContext and AclContextProvider Shannon Sterz
2025-10-23 10:00   ` Dominik Csapak
2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 2/2] http_helpers: reload LocalAclTree when logging in or refreshing a ticket Shannon Sterz
2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] server/api-types: move AccessControlConfig to shared api types Shannon Sterz
2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] ui: add an AclContext via the AclContextProvider to the main app ui Shannon Sterz
2025-10-22 13:11 ` [pdm-devel] [PATCH yew-comp v2 1/1] notes view: allow hiding the toolbar if editing isn't supported Shannon Sterz
2025-10-23  9:36   ` Dominik Csapak
2025-10-22 13:11 ` [pdm-devel] [PATCH datacenter-manager v2 1/1] ui: main menu: use the AclContext to hide the Notes if appropriate Shannon Sterz

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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal