From: Shannon Sterz <s.sterz@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox v3 1/5] access-control: add acl feature to only expose types and the AclTree
Date: Thu, 6 Nov 2025 15:38:27 +0100 [thread overview]
Message-ID: <20251106143836.288888-2-s.sterz@proxmox.com> (raw)
In-Reply-To: <20251106143836.288888-1-s.sterz@proxmox.com>
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
next prev parent reply other threads:[~2025-11-06 14:38 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
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 [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20251106143836.288888-2-s.sterz@proxmox.com \
--to=s.sterz@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is 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.