* [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends
@ 2025-11-06 14:38 Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 1/5] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
` (9 more replies)
0 siblings, 10 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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 fourth 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 commit adds 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.
Changelog
---------
note that there was already a v2 [1] of this series, but this was a mistake
and should be considered a v1. sorry for the confusion.
changes since v2:
- combine impl only functions into private modules and impl blocks to
more cleanly separate them out (thanks @ Wolfgang Bumiller)
- add a small clean up commit for in-lining format string variables
changes since v1:
- move removing a use line to the right commit (thanks @ Dominik Csapak)
- instead of adapting the NodesView, simply avoid setting an on_submit
callback if the user doesn't have the permissions (thanks @ Dominik
Csapak)
proxmox:
Shannon Sterz (5):
access-control: add acl feature to only expose types and the AclTree
access-control: use format strings where possible
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 | 5 +-
proxmox-access-control/src/acl.rs | 509 +++++++++++-------
proxmox-access-control/src/api/acl.rs | 37 +-
.../src/cached_user_info.rs | 91 +---
proxmox-access-control/src/init.rs | 91 ++--
proxmox-access-control/src/lib.rs | 4 +-
proxmox-access-control/src/token_shadow.rs | 2 +-
proxmox-access-control/src/user.rs | 3 +-
8 files changed, 415 insertions(+), 327 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 (3):
server/api-types: move AccessControlConfig to shared api types
ui: add an AclContext via the AclContextProvider to the main app ui
ui: main menu: use the AclContext to hide the Notes if appropriate
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 ++-
ui/src/main_menu.rs | 68 ++++++++++-----
6 files changed, 221 insertions(+), 183 deletions(-)
Summary over all repositories:
18 files changed, 849 insertions(+), 511 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] 11+ messages in thread
* [pdm-devel] [PATCH proxmox v3 1/5] access-control: add acl feature to only expose types and the AclTree
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 2/5] access-control: use format strings where possible Shannon Sterz
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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).
access-control: clean up `cfg`s for impl feature
by moving code into separate impl blocks and module. this avoids
adding a lot of cfg macros separatelly.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
proxmox-access-control/Cargo.toml | 5 +-
proxmox-access-control/src/acl.rs | 386 +++++++++++----------
proxmox-access-control/src/init.rs | 91 ++---
proxmox-access-control/src/lib.rs | 4 +-
proxmox-access-control/src/token_shadow.rs | 2 +-
proxmox-access-control/src/user.rs | 3 +-
6 files changed, 266 insertions(+), 225 deletions(-)
diff --git a/proxmox-access-control/Cargo.toml b/proxmox-access-control/Cargo.toml
index d23d272d..6df3b3ba 100644
--- a/proxmox-access-control/Cargo.toml
+++ b/proxmox-access-control/Cargo.toml
@@ -35,17 +35,20 @@ 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 ec0a77b5..7813ea5d 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -1,15 +1,18 @@
-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;
-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;
-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;
pub fn split_acl_path(path: &str) -> Vec<&str> {
let items = path.split('/');
@@ -302,6 +305,120 @@ impl AclTree {
node.insert_user_role(auth_id.to_owned(), role.to_string(), propagate);
}
+ fn parse_acl_line(&mut self, line: &str) -> Result<(), Error> {
+ let items: Vec<&str> = line.split(':').collect();
+
+ if items.len() != 5 {
+ bail!("wrong number of items.");
+ }
+
+ if items[0] != "acl" {
+ bail!("line does not start with 'acl'.");
+ }
+
+ let propagate = if items[1] == "0" {
+ false
+ } else if items[1] == "1" {
+ true
+ } else {
+ bail!("expected '0' or '1' for propagate flag.");
+ };
+
+ let path_str = items[2];
+ let path = split_acl_path(path_str);
+ let node = self.get_or_insert_node(&path);
+
+ let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
+
+ let rolelist: Vec<&str> = items[4].split(',').map(|v| v.trim()).collect();
+
+ for user_or_group in &uglist {
+ for role in &rolelist {
+ if !access_conf().roles().contains_key(role) {
+ bail!("unknown role '{}'", role);
+ }
+ if let Some(group) = user_or_group.strip_prefix('@') {
+ node.insert_group_role(group.to_string(), role.to_string(), propagate);
+ } else {
+ node.insert_user_role(user_or_group.parse()?, role.to_string(), propagate);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// This is used for testing
+ pub fn from_raw(raw: &str) -> Result<Self, Error> {
+ let mut tree = Self::new();
+ for (linenr, line) in raw.lines().enumerate() {
+ let line = line.trim();
+ if line.is_empty() {
+ continue;
+ }
+ if let Err(err) = tree.parse_acl_line(line) {
+ bail!(
+ "unable to parse acl config data, line {} - {}",
+ linenr + 1,
+ err
+ );
+ }
+ }
+ Ok(tree)
+ }
+
+ /// Returns a map of role name and propagation status for a given `auth_id` and `path`.
+ ///
+ /// This will collect role mappings according to the following algorithm:
+ /// - iterate over all intermediate nodes along `path` and collect roles with `propagate` set
+ /// - get all (propagating and non-propagating) roles for last component of path
+ /// - more specific role maps replace less specific role maps
+ /// -- user/token is more specific than group at each level
+ /// -- roles lower in the tree are more specific than those higher up along the path
+ pub fn roles(&self, auth_id: &Authid, path: &[&str]) -> HashMap<String, bool> {
+ let mut node = &self.root;
+ let mut role_map = node.extract_roles(auth_id, path.is_empty());
+
+ let mut comp_iter = path.iter().peekable();
+
+ while let Some(comp) = comp_iter.next() {
+ let last_comp = comp_iter.peek().is_none();
+
+ let mut sub_comp_iter = comp.split('/').peekable();
+
+ while let Some(sub_comp) = sub_comp_iter.next() {
+ let last_sub_comp = last_comp && sub_comp_iter.peek().is_none();
+
+ node = match node.children.get(sub_comp) {
+ Some(n) => n,
+ None => return role_map, // path not found
+ };
+
+ let new_map = node.extract_roles(auth_id, last_sub_comp);
+ if !new_map.is_empty() {
+ // overwrite previous mappings
+ role_map = new_map;
+ }
+ }
+ }
+
+ role_map
+ }
+
+ pub fn get_child_paths(&self, auth_id: &Authid, path: &[&str]) -> Result<Vec<String>, Error> {
+ let mut res = Vec::new();
+
+ if let Some(node) = self.get_node(path) {
+ let path = path.join("/");
+ node.get_child_paths(path, auth_id, &mut res)?;
+ }
+
+ Ok(res)
+ }
+}
+
+#[cfg(feature = "impl")]
+impl AclTree {
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();
@@ -406,49 +523,6 @@ impl AclTree {
Self::write_node_config(&self.root, "", w)
}
- fn parse_acl_line(&mut self, line: &str) -> Result<(), Error> {
- let items: Vec<&str> = line.split(':').collect();
-
- if items.len() != 5 {
- bail!("wrong number of items.");
- }
-
- if items[0] != "acl" {
- bail!("line does not start with 'acl'.");
- }
-
- let propagate = if items[1] == "0" {
- false
- } else if items[1] == "1" {
- true
- } else {
- bail!("expected '0' or '1' for propagate flag.");
- };
-
- let path_str = items[2];
- let path = split_acl_path(path_str);
- let node = self.get_or_insert_node(&path);
-
- let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
-
- let rolelist: Vec<&str> = items[4].split(',').map(|v| v.trim()).collect();
-
- for user_or_group in &uglist {
- for role in &rolelist {
- if !access_conf().roles().contains_key(role) {
- bail!("unknown role '{}'", role);
- }
- if let Some(group) = user_or_group.strip_prefix('@') {
- node.insert_group_role(group.to_string(), role.to_string(), propagate);
- } else {
- node.insert_user_role(user_or_group.parse()?, role.to_string(), propagate);
- }
- }
- }
-
- Ok(())
- }
-
fn load(filename: &Path) -> Result<(Self, ConfigDigest), Error> {
let mut tree = Self::new();
@@ -482,156 +556,106 @@ impl AclTree {
Ok((tree, digest))
}
+}
- /// This is used for testing
- pub fn from_raw(raw: &str) -> Result<Self, Error> {
- let mut tree = Self::new();
- for (linenr, line) in raw.lines().enumerate() {
- let line = line.trim();
- if line.is_empty() {
- continue;
- }
- if let Err(err) = tree.parse_acl_line(line) {
- bail!(
- "unable to parse acl config data, line {} - {}",
- linenr + 1,
- err
- );
- }
- }
- Ok(tree)
+#[cfg(feature = "impl")]
+pub use impl_feature::{cached_config, config, lock_config, save_config};
+
+#[cfg(feature = "impl")]
+mod impl_feature {
+ use std::sync::{Arc, OnceLock, RwLock};
+
+ use anyhow::{bail, Error};
+
+ use proxmox_config_digest::ConfigDigest;
+ use proxmox_product_config::{open_api_lockfile, replace_privileged_config, ApiLockGuard};
+
+ use crate::acl::AclTree;
+ use crate::init::access_conf;
+ use crate::init::impl_feature::{acl_config, acl_config_lock};
+
+ /// Get exclusive lock
+ pub fn lock_config() -> Result<ApiLockGuard, Error> {
+ open_api_lockfile(acl_config_lock(), None, true)
}
- /// Returns a map of role name and propagation status for a given `auth_id` and `path`.
+ /// Reads the [`AclTree`] from `acl.cfg` in the configuration directory.
+ pub fn config() -> Result<(AclTree, ConfigDigest), Error> {
+ let path = acl_config();
+ AclTree::load(&path)
+ }
+
+ /// Returns a cached [`AclTree`] or a fresh copy read directly from `acl.cfg` in the configuration
+ /// directory.
///
- /// This will collect role mappings according to the following algorithm:
- /// - iterate over all intermediate nodes along `path` and collect roles with `propagate` set
- /// - get all (propagating and non-propagating) roles for last component of path
- /// - more specific role maps replace less specific role maps
- /// -- user/token is more specific than group at each level
- /// -- roles lower in the tree are more specific than those higher up along the path
- pub fn roles(&self, auth_id: &Authid, path: &[&str]) -> HashMap<String, bool> {
- let mut node = &self.root;
- let mut role_map = node.extract_roles(auth_id, path.is_empty());
-
- let mut comp_iter = path.iter().peekable();
-
- while let Some(comp) = comp_iter.next() {
- let last_comp = comp_iter.peek().is_none();
-
- let mut sub_comp_iter = comp.split('/').peekable();
-
- while let Some(sub_comp) = sub_comp_iter.next() {
- let last_sub_comp = last_comp && sub_comp_iter.peek().is_none();
-
- node = match node.children.get(sub_comp) {
- Some(n) => n,
- None => return role_map, // path not found
- };
-
- let new_map = node.extract_roles(auth_id, last_sub_comp);
- if !new_map.is_empty() {
- // overwrite previous mappings
- role_map = new_map;
- }
- }
+ /// 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.
+ pub fn cached_config() -> Result<Arc<AclTree>, Error> {
+ struct ConfigCache {
+ data: Option<Arc<AclTree>>,
+ last_mtime: i64,
+ last_mtime_nsec: i64,
}
- role_map
- }
+ static CACHED_CONFIG: OnceLock<RwLock<ConfigCache>> = OnceLock::new();
+ let cached_conf = CACHED_CONFIG.get_or_init(|| {
+ RwLock::new(ConfigCache {
+ data: None,
+ last_mtime: 0,
+ last_mtime_nsec: 0,
+ })
+ });
- pub fn get_child_paths(&self, auth_id: &Authid, path: &[&str]) -> Result<Vec<String>, Error> {
- let mut res = Vec::new();
+ let conf = acl_config();
+ let stat = match nix::sys::stat::stat(&conf) {
+ Ok(stat) => Some(stat),
+ Err(nix::errno::Errno::ENOENT) => None,
+ Err(err) => bail!("unable to stat '{}' - {err}", conf.display()),
+ };
- if let Some(node) = self.get_node(path) {
- let path = path.join("/");
- node.get_child_paths(path, auth_id, &mut res)?;
- }
-
- Ok(res)
- }
-}
-
-/// Get exclusive lock
-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.
-pub fn config() -> Result<(AclTree, ConfigDigest), Error> {
- let path = acl_config();
- AclTree::load(&path)
-}
-
-/// Returns a cached [`AclTree`] or a fresh copy read directly from `acl.cfg` in the configuration
-/// directory.
-///
-/// 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.
-pub fn cached_config() -> Result<Arc<AclTree>, Error> {
- struct ConfigCache {
- data: Option<Arc<AclTree>>,
- last_mtime: i64,
- last_mtime_nsec: i64,
- }
-
- static CACHED_CONFIG: OnceLock<RwLock<ConfigCache>> = OnceLock::new();
- let cached_conf = CACHED_CONFIG.get_or_init(|| {
- RwLock::new(ConfigCache {
- data: None,
- last_mtime: 0,
- last_mtime_nsec: 0,
- })
- });
-
- let conf = acl_config();
- let stat = match nix::sys::stat::stat(&conf) {
- Ok(stat) => Some(stat),
- Err(nix::errno::Errno::ENOENT) => None,
- Err(err) => bail!("unable to stat '{}' - {err}", conf.display()),
- };
-
- {
- // limit scope
- let cache = cached_conf.read().unwrap();
- if let Some(ref config) = cache.data {
- if let Some(stat) = stat {
- if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec
- {
+ {
+ // limit scope
+ let cache = cached_conf.read().unwrap();
+ if let Some(ref config) = cache.data {
+ if let Some(stat) = stat {
+ if stat.st_mtime == cache.last_mtime
+ && stat.st_mtime_nsec == cache.last_mtime_nsec
+ {
+ return Ok(config.clone());
+ }
+ } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 {
return Ok(config.clone());
}
- } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 {
- return Ok(config.clone());
}
}
+
+ let (config, _digest) = config()?;
+ let config = Arc::new(config);
+
+ let mut cache = cached_conf.write().unwrap();
+ if let Some(stat) = stat {
+ cache.last_mtime = stat.st_mtime;
+ cache.last_mtime_nsec = stat.st_mtime_nsec;
+ }
+ cache.data = Some(config.clone());
+
+ Ok(config)
}
- let (config, _digest) = config()?;
- let config = Arc::new(config);
+ /// Saves an [`AclTree`] to `acl.cfg` in the configuration directory, ensuring proper ownership and
+ /// file permissions.
+ pub fn save_config(acl: &AclTree) -> Result<(), Error> {
+ let mut raw: Vec<u8> = Vec::new();
+ acl.write_config(&mut raw)?;
- let mut cache = cached_conf.write().unwrap();
- if let Some(stat) = stat {
- cache.last_mtime = stat.st_mtime;
- cache.last_mtime_nsec = stat.st_mtime_nsec;
+ let conf = acl_config();
+ replace_privileged_config(conf, &raw)?;
+
+ // increase cache generation so we reload it next time we access it
+ access_conf().increment_cache_generation()?;
+
+ Ok(())
}
- cache.data = Some(config.clone());
-
- Ok(config)
-}
-
-/// Saves an [`AclTree`] to `acl.cfg` in the configuration directory, ensuring proper ownership and
-/// file permissions.
-pub fn save_config(acl: &AclTree) -> Result<(), Error> {
- let mut raw: Vec<u8> = Vec::new();
- acl.write_config(&mut raw)?;
-
- let conf = acl_config();
- replace_privileged_config(conf, &raw)?;
-
- // increase cache generation so we reload it next time we access it
- access_conf().increment_cache_generation()?;
-
- Ok(())
}
#[cfg(test)]
diff --git a/proxmox-access-control/src/init.rs b/proxmox-access-control/src/init.rs
index 39a12352..e64398e8 100644
--- a/proxmox-access-control/src/init.rs
+++ b/proxmox-access-control/src/init.rs
@@ -1,5 +1,4 @@
use std::collections::HashMap;
-use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use anyhow::{format_err, Error};
@@ -8,7 +7,6 @@ use proxmox_auth_api::types::{Authid, Userid};
use proxmox_section_config::SectionConfigData;
static ACCESS_CONF: OnceLock<&'static dyn AccessControlConfig> = OnceLock::new();
-static ACCESS_CONF_DIR: OnceLock<PathBuf> = OnceLock::new();
/// This trait specifies the functions a product needs to implement to get ACL tree based access
/// control management from this plugin.
@@ -105,21 +103,7 @@ pub trait AccessControlConfig: Send + Sync {
}
}
-pub fn init<P: AsRef<Path>>(
- acm_config: &'static dyn AccessControlConfig,
- config_dir: P,
-) -> Result<(), Error> {
- init_access_config(acm_config)?;
- init_access_config_dir(config_dir)
-}
-
-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 +115,61 @@ pub(crate) fn access_conf() -> &'static dyn AccessControlConfig {
.expect("please initialize the acm config before using it!")
}
-fn conf_dir() -> &'static PathBuf {
- ACCESS_CONF_DIR
- .get()
- .expect("please initialize acm config dir before using it!")
-}
+#[cfg(feature = "impl")]
+pub use impl_feature::init;
-pub(crate) fn acl_config() -> PathBuf {
- conf_dir().join("acl.cfg")
-}
+#[cfg(feature = "impl")]
+pub(crate) mod impl_feature {
+ use std::path::{Path, PathBuf};
+ use std::sync::OnceLock;
-pub(crate) fn acl_config_lock() -> PathBuf {
- conf_dir().join(".acl.lck")
-}
+ use anyhow::{format_err, Error};
-pub(crate) fn user_config() -> PathBuf {
- conf_dir().join("user.cfg")
-}
+ use crate::init::{init_access_config, AccessControlConfig};
-pub(crate) fn user_config_lock() -> PathBuf {
- conf_dir().join(".user.lck")
-}
+ static ACCESS_CONF_DIR: OnceLock<PathBuf> = OnceLock::new();
-pub(crate) fn token_shadow() -> PathBuf {
- conf_dir().join("token.shadow")
-}
+ pub fn init<P: AsRef<Path>>(
+ acm_config: &'static dyn AccessControlConfig,
+ config_dir: P,
+ ) -> Result<(), Error> {
+ init_access_config(acm_config)?;
+ init_access_config_dir(config_dir)
+ }
-pub(crate) fn token_shadow_lock() -> PathBuf {
- conf_dir().join("token.shadow.lock")
+ 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!"))
+ }
+
+ fn conf_dir() -> &'static PathBuf {
+ ACCESS_CONF_DIR
+ .get()
+ .expect("please initialize acm config dir before using it!")
+ }
+
+ pub(crate) fn acl_config() -> PathBuf {
+ conf_dir().join("acl.cfg")
+ }
+
+ pub(crate) fn acl_config_lock() -> PathBuf {
+ conf_dir().join(".acl.lck")
+ }
+
+ pub(crate) fn user_config() -> PathBuf {
+ conf_dir().join("user.cfg")
+ }
+
+ pub(crate) fn user_config_lock() -> PathBuf {
+ conf_dir().join(".user.lck")
+ }
+
+ pub(crate) fn token_shadow() -> PathBuf {
+ conf_dir().join("token.shadow")
+ }
+
+ 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")]
diff --git a/proxmox-access-control/src/token_shadow.rs b/proxmox-access-control/src/token_shadow.rs
index 373910f3..c586d834 100644
--- a/proxmox-access-control/src/token_shadow.rs
+++ b/proxmox-access-control/src/token_shadow.rs
@@ -6,7 +6,7 @@ use serde_json::{from_value, Value};
use proxmox_auth_api::types::Authid;
use proxmox_product_config::{open_api_lockfile, replace_config, ApiLockGuard};
-use crate::init::{token_shadow, token_shadow_lock};
+use crate::init::impl_feature::{token_shadow, token_shadow_lock};
// Get exclusive lock
fn lock_config() -> Result<ApiLockGuard, Error> {
diff --git a/proxmox-access-control/src/user.rs b/proxmox-access-control/src/user.rs
index 95b70f25..a4b59edc 100644
--- a/proxmox-access-control/src/user.rs
+++ b/proxmox-access-control/src/user.rs
@@ -9,7 +9,8 @@ use proxmox_product_config::{open_api_lockfile, replace_privileged_config, ApiLo
use proxmox_schema::*;
use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
-use crate::init::{access_conf, user_config, user_config_lock};
+use crate::init::access_conf;
+use crate::init::impl_feature::{user_config, user_config_lock};
use crate::types::{ApiToken, User};
fn get_or_init_config() -> &'static SectionConfig {
--
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] 11+ messages in thread
* [pdm-devel] [PATCH proxmox v3 2/5] access-control: use format strings where possible
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 1/5] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 3/5] access-control: move functions querying privileges to the AclTree Shannon Sterz
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
proxmox-access-control/src/acl.rs | 21 +++++++--------------
1 file changed, 7 insertions(+), 14 deletions(-)
diff --git a/proxmox-access-control/src/acl.rs b/proxmox-access-control/src/acl.rs
index 7813ea5d..f2589e57 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -335,7 +335,7 @@ impl AclTree {
for user_or_group in &uglist {
for role in &rolelist {
if !access_conf().roles().contains_key(role) {
- bail!("unknown role '{}'", role);
+ bail!("unknown role '{role}'");
}
if let Some(group) = user_or_group.strip_prefix('@') {
node.insert_group_role(group.to_string(), role.to_string(), propagate);
@@ -358,9 +358,8 @@ impl AclTree {
}
if let Err(err) = tree.parse_acl_line(line) {
bail!(
- "unable to parse acl config data, line {} - {}",
+ "unable to parse acl config data, line {} - {err}",
linenr + 1,
- err
);
}
}
@@ -493,10 +492,8 @@ impl AclTree {
let role_list = role_list(roles);
writeln!(
w,
- "acl:0:{}:{}:{}",
+ "acl:0:{}:{uglist}:{role_list}",
if path.is_empty() { "/" } else { path },
- uglist,
- role_list
)?;
}
@@ -504,10 +501,8 @@ impl AclTree {
let role_list = role_list(roles);
writeln!(
w,
- "acl:1:{}:{}:{}",
+ "acl:1:{}:{uglist}:{role_list}",
if path.is_empty() { "/" } else { path },
- uglist,
- role_list
)?;
}
@@ -532,7 +527,7 @@ impl AclTree {
if err.kind() == std::io::ErrorKind::NotFound {
String::new()
} else {
- bail!("unable to read acl config {:?} - {}", filename, err);
+ bail!("unable to read acl config {filename:?} - {err}");
}
}
};
@@ -546,10 +541,8 @@ impl AclTree {
}
if let Err(err) = tree.parse_acl_line(line) {
bail!(
- "unable to parse acl config {:?}, line {} - {}",
- filename,
- linenr + 1,
- err
+ "unable to parse acl config {filename:?}, line {} - {err}",
+ linenr + 1
);
}
}
--
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] 11+ messages in thread
* [pdm-devel] [PATCH proxmox v3 3/5] access-control: move functions querying privileges to the AclTree
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 1/5] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 2/5] access-control: use format strings where possible Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 4/5] access-control: derive Debug and PartialEq on AclTree and AclTreeNode Shannon Sterz
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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 | 108 ++++++++++++++++++
.../src/cached_user_info.rs | 91 +--------------
2 files changed, 114 insertions(+), 85 deletions(-)
diff --git a/proxmox-access-control/src/acl.rs b/proxmox-access-control/src/acl.rs
index f2589e57..cc3d04ff 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -414,6 +414,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)
+ }
}
#[cfg(feature = "impl")]
@@ -651,6 +747,18 @@ mod impl_feature {
}
}
+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] 11+ messages in thread
* [pdm-devel] [PATCH proxmox v3 4/5] access-control: derive Debug and PartialEq on AclTree and AclTreeNode
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
` (2 preceding siblings ...)
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 3/5] access-control: move functions querying privileges to the AclTree Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 5/5] access-control: allow reading all acls of the current authid Shannon Sterz
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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 cc3d04ff..a701f510 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -30,7 +30,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.
///
@@ -40,7 +40,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] 11+ messages in thread
* [pdm-devel] [PATCH proxmox v3 5/5] access-control: allow reading all acls of the current authid
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
` (3 preceding siblings ...)
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 4/5] access-control: derive Debug and PartialEq on AclTree and AclTreeNode Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH yew-comp v3 1/2] acl_context: add AclContext and AclContextProvider Shannon Sterz
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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 4326215c..8bdbe0ed 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] 11+ messages in thread
* [pdm-devel] [PATCH yew-comp v3 1/2] acl_context: add AclContext and AclContextProvider
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
` (4 preceding siblings ...)
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 5/5] access-control: allow reading all acls of the current authid Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH yew-comp v3 2/2] http_helpers: reload LocalAclTree when logging in or refreshing a ticket Shannon Sterz
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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 39109c0..1846091 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -81,7 +81,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 3a9e32b..c5d0acc 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] 11+ messages in thread
* [pdm-devel] [PATCH yew-comp v3 2/2] http_helpers: reload LocalAclTree when logging in or refreshing a ticket
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
` (5 preceding siblings ...)
2025-11-06 14:38 ` [pdm-devel] [PATCH yew-comp v3 1/2] acl_context: add AclContext and AclContextProvider Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 1/3] server/api-types: move AccessControlConfig to shared api types Shannon Sterz
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 1/3] server/api-types: move AccessControlConfig to shared api types
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
` (6 preceding siblings ...)
2025-11-06 14:38 ` [pdm-devel] [PATCH yew-comp v3 2/2] http_helpers: reload LocalAclTree when logging in or refreshing a ticket Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 2/3] ui: add an AclContext via the AclContextProvider to the main app ui Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 3/3] ui: main menu: use the AclContext to hide the Notes if appropriate Shannon Sterz
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 2/3] ui: add an AclContext via the AclContextProvider to the main app ui
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
` (7 preceding siblings ...)
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 1/3] server/api-types: move AccessControlConfig to shared api types Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 3/3] ui: main menu: use the AclContext to hide the Notes if appropriate Shannon Sterz
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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 cccb914..284dd31 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 f8a44f5..d9423d3 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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 3/3] ui: main menu: use the AclContext to hide the Notes if appropriate
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
` (8 preceding siblings ...)
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 2/3] ui: add an AclContext via the AclContextProvider to the main app ui Shannon Sterz
@ 2025-11-06 14:38 ` Shannon Sterz
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-06 14:38 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 | 68 ++++++++++++++++++++++++++++++++-------------
1 file changed, 48 insertions(+), 20 deletions(-)
diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs
index 43cc292..474e069 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::dashboard::view::View;
use crate::remotes::RemotesPanel;
@@ -63,11 +64,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(
@@ -110,10 +114,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,
}
}
@@ -123,6 +134,10 @@ impl Component for PdmMainMenu {
self.active = key;
true
}
+ Msg::UpdateAcl(acl_context) => {
+ self.acl_context = acl_context;
+ true
+ }
}
}
@@ -145,25 +160,38 @@ impl Component for PdmMainMenu {
move |_| View::new("dashboard").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(¬es)?))
- .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 mut notes = NotesView::new("/config/notes");
+
+ if allow_editing {
+ notes.set_on_submit(|notes| async move {
+ proxmox_yew_comp::http_put(
+ "/config/notes",
+ Some(serde_json::to_value(¬es)?),
+ )
+ .await
+ });
+ }
+
+ 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] 11+ messages in thread
end of thread, other threads:[~2025-11-06 14:38 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-11-06 14:38 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp v3 00/10] add support for checking acl permissions in (yew) front-ends Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 1/5] access-control: add acl feature to only expose types and the AclTree Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 2/5] access-control: use format strings where possible Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 3/5] access-control: move functions querying privileges to the AclTree Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 4/5] access-control: derive Debug and PartialEq on AclTree and AclTreeNode Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH proxmox v3 5/5] access-control: allow reading all acls of the current authid Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH yew-comp v3 1/2] acl_context: add AclContext and AclContextProvider Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH yew-comp v3 2/2] http_helpers: reload LocalAclTree when logging in or refreshing a ticket Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 1/3] server/api-types: move AccessControlConfig to shared api types Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 2/3] ui: add an AclContext via the AclContextProvider to the main app ui Shannon Sterz
2025-11-06 14:38 ` [pdm-devel] [PATCH datacenter-manager v3 3/3] ui: main menu: use the AclContext to hide the Notes if appropriate Shannon Sterz
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox