From: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox v8 1/6] token shadow: split AccessControlConfig and add token.shadow generation
Date: Thu, 9 Apr 2026 17:54:21 +0200 [thread overview]
Message-ID: <20260409155437.312760-2-s.rufinatscha@proxmox.com> (raw)
In-Reply-To: <20260409155437.312760-1-s.rufinatscha@proxmox.com>
Splits implementation hooks from AccessControlConfig and introduces
AccessControlBackend to keep AccessControlConfig focused on ACL metadata and
validation.
Also introduces generation hooks in AccessControlBackend to support token.shadow
caching.
Signed-off-by: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
---
Changes from v7 to v8:
* Split into AccessControlConfig + AccessControlBackend instead of
AccessControlConfig + AccessControlPermissions
* Gate AccessControlBackend behind #[cfg(feature = "impl")]
* Move init_user_config and cache_generation/increment_cache_generation
into AccessControlBackend
* Remove delegation methods on AccessControlConfig (no longer needed
with this split)
* Add init_separate() for cases where config and backend are different
objects; constrain init() with T: AccessControlConfig +
AccessControlBackend
* Callers use access_backend() instead of access_conf() for
cache/backend hooks
Changes from v6 to v7:
* Rebased
Changes from v5 to v6:
* Rebased
Changes from v4 to v5:
* Rebased
proxmox-access-control/src/acl.rs | 4 +-
.../src/cached_user_info.rs | 4 +-
proxmox-access-control/src/init.rs | 113 +++++++++++++-----
proxmox-access-control/src/lib.rs | 2 +-
proxmox-access-control/src/user.rs | 6 +-
5 files changed, 91 insertions(+), 38 deletions(-)
diff --git a/proxmox-access-control/src/acl.rs b/proxmox-access-control/src/acl.rs
index 38cb7edf..e4c35e02 100644
--- a/proxmox-access-control/src/acl.rs
+++ b/proxmox-access-control/src/acl.rs
@@ -660,7 +660,7 @@ mod impl_feature {
use proxmox_product_config::{open_api_lockfile, replace_privileged_config, ApiLockGuard};
use crate::acl::AclTree;
- use crate::init::access_conf;
+ use crate::init::access_backend;
use crate::init::impl_feature::{acl_config, acl_config_lock};
/// Get exclusive lock
@@ -741,7 +741,7 @@ mod impl_feature {
replace_privileged_config(conf, &raw)?;
// increase cache generation so we reload it next time we access it
- access_conf().increment_cache_generation()?;
+ access_backend().increment_cache_generation()?;
Ok(())
}
diff --git a/proxmox-access-control/src/cached_user_info.rs b/proxmox-access-control/src/cached_user_info.rs
index 8db37727..81df561f 100644
--- a/proxmox-access-control/src/cached_user_info.rs
+++ b/proxmox-access-control/src/cached_user_info.rs
@@ -10,7 +10,7 @@ use proxmox_section_config::SectionConfigData;
use proxmox_time::epoch_i64;
use crate::acl::AclTree;
-use crate::init::access_conf;
+use crate::init::{access_backend, access_conf};
use crate::types::{ApiToken, User};
/// Cache User/Group/Token/Acl configuration data for fast permission tests
@@ -30,7 +30,7 @@ impl CachedUserInfo {
pub fn new() -> Result<Arc<Self>, Error> {
let now = epoch_i64();
- let cache_generation = access_conf().cache_generation();
+ let cache_generation = access_backend().cache_generation();
static CACHED_CONFIG: OnceLock<RwLock<ConfigCache>> = OnceLock::new();
let cached_config = CACHED_CONFIG.get_or_init(|| {
diff --git a/proxmox-access-control/src/init.rs b/proxmox-access-control/src/init.rs
index e64398e8..07b5df11 100644
--- a/proxmox-access-control/src/init.rs
+++ b/proxmox-access-control/src/init.rs
@@ -7,6 +7,8 @@ use proxmox_auth_api::types::{Authid, Userid};
use proxmox_section_config::SectionConfigData;
static ACCESS_CONF: OnceLock<&'static dyn AccessControlConfig> = OnceLock::new();
+#[cfg(feature = "impl")]
+static ACCESS_BACKEND: OnceLock<&'static dyn AccessControlBackend> = OnceLock::new();
/// This trait specifies the functions a product needs to implement to get ACL tree based access
/// control management from this plugin.
@@ -32,25 +34,6 @@ pub trait AccessControlConfig: Send + Sync {
false
}
- /// Returns the current cache generation of the user and acl configs. If the generation was
- /// incremented since the last time the cache was queried, the configs are loaded again from
- /// disk.
- ///
- /// Returning `None` will always reload the cache.
- ///
- /// Default: Always returns `None`.
- fn cache_generation(&self) -> Option<usize> {
- None
- }
-
- /// Increment the cache generation of user and acl configs. This indicates that they were
- /// changed on disk.
- ///
- /// Default: Does nothing.
- fn increment_cache_generation(&self) -> Result<(), Error> {
- Ok(())
- }
-
/// Optionally returns a role that has no access to any resource.
///
/// Default: Returns `None`.
@@ -65,13 +48,6 @@ pub trait AccessControlConfig: Send + Sync {
None
}
- /// Called after the user configuration is loaded to potentially re-add fixed users, such as a
- /// `root@pam` user.
- fn init_user_config(&self, config: &mut SectionConfigData) -> Result<(), Error> {
- let _ = config;
- Ok(())
- }
-
/// This is used to determined what access control list entries a user is allowed to read.
///
/// Override this if you want to use the `api` feature.
@@ -103,6 +79,53 @@ pub trait AccessControlConfig: Send + Sync {
}
}
+/// Backend hooks for loading and caching access control state.
+#[cfg(feature = "impl")]
+pub trait AccessControlBackend: Send + Sync {
+ /// Called after the user configuration is loaded to potentially re-add fixed users, such as a
+ /// `root@pam` user.
+ fn init_user_config(&self, config: &mut SectionConfigData) -> Result<(), Error> {
+ let _ = config;
+ Ok(())
+ }
+
+ /// Returns the current cache generation of the user and acl configs. If the generation was
+ /// incremented since the last time the cache was queried, the configs are loaded again from
+ /// disk.
+ ///
+ /// Returning `None` will always reload the cache.
+ ///
+ /// Default: Always returns `None`.
+ fn cache_generation(&self) -> Option<usize> {
+ None
+ }
+
+ /// Increment the cache generation of user and acl configs. This indicates that they were
+ /// changed on disk.
+ ///
+ /// Default: Does nothing.
+ fn increment_cache_generation(&self) -> Result<(), Error> {
+ Ok(())
+ }
+
+ /// Returns the current cache generation of the token shadow cache. If the generation was
+ /// incremented since the last time the cache was queried, the token shadow cache is reloaded
+ /// from disk.
+ ///
+ /// Default: Always returns `None`.
+ fn token_shadow_cache_generation(&self) -> Option<usize> {
+ None
+ }
+
+ /// Increment the cache generation of the token shadow cache and return the previous value.
+ /// This indicates that it was changed on disk.
+ ///
+ /// Default: Returns an error as token shadow generation is not supported.
+ fn increment_token_shadow_cache_generation(&self) -> Result<usize, Error> {
+ anyhow::bail!("token shadow generation not supported");
+ }
+}
+
pub fn init_access_config(config: &'static dyn AccessControlConfig) -> Result<(), Error> {
ACCESS_CONF
.set(config)
@@ -115,8 +138,24 @@ pub(crate) fn access_conf() -> &'static dyn AccessControlConfig {
.expect("please initialize the acm config before using it!")
}
+#[cfg(feature = "impl")]
+pub fn init_access_backend(config: &'static dyn AccessControlBackend) -> Result<(), Error> {
+ ACCESS_BACKEND
+ .set(config)
+ .map_err(|_| format_err!("cannot initialize access control backend twice!"))
+}
+
+#[cfg(feature = "impl")]
+pub(crate) fn access_backend() -> &'static dyn AccessControlBackend {
+ *ACCESS_BACKEND
+ .get()
+ .expect("please initialize the access control backend before using it!")
+}
+
#[cfg(feature = "impl")]
pub use impl_feature::init;
+#[cfg(feature = "impl")]
+pub use impl_feature::init_separate;
#[cfg(feature = "impl")]
pub(crate) mod impl_feature {
@@ -125,15 +164,29 @@ pub(crate) mod impl_feature {
use anyhow::{format_err, Error};
- use crate::init::{init_access_config, AccessControlConfig};
+ use crate::init::{
+ init_access_backend, init_access_config, AccessControlBackend, AccessControlConfig,
+ };
static ACCESS_CONF_DIR: OnceLock<PathBuf> = OnceLock::new();
- pub fn init<P: AsRef<Path>>(
- acm_config: &'static dyn AccessControlConfig,
+ pub fn init<T, P>(config: &'static T, config_dir: P) -> Result<(), Error>
+ where
+ T: AccessControlConfig + AccessControlBackend,
+ P: AsRef<Path>,
+ {
+ init_access_config(config)?;
+ init_access_backend(config)?;
+ init_access_config_dir(config_dir)
+ }
+
+ pub fn init_separate<P: AsRef<Path>>(
+ acl_config: &'static dyn AccessControlConfig,
+ backend: &'static dyn AccessControlBackend,
config_dir: P,
) -> Result<(), Error> {
- init_access_config(acm_config)?;
+ init_access_config(acl_config)?;
+ init_access_backend(backend)?;
init_access_config_dir(config_dir)
}
diff --git a/proxmox-access-control/src/lib.rs b/proxmox-access-control/src/lib.rs
index 9195c999..dc17da2a 100644
--- a/proxmox-access-control/src/lib.rs
+++ b/proxmox-access-control/src/lib.rs
@@ -8,7 +8,7 @@ pub mod acl;
#[cfg(feature = "api")]
pub mod api;
-#[cfg(feature = "acl")]
+#[cfg(any(feature = "acl", feature = "impl"))]
pub mod init;
#[cfg(feature = "impl")]
diff --git a/proxmox-access-control/src/user.rs b/proxmox-access-control/src/user.rs
index a4b59edc..ec5336d2 100644
--- a/proxmox-access-control/src/user.rs
+++ b/proxmox-access-control/src/user.rs
@@ -9,7 +9,7 @@ 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;
+use crate::init::access_backend;
use crate::init::impl_feature::{user_config, user_config_lock};
use crate::types::{ApiToken, User};
@@ -52,7 +52,7 @@ pub fn config() -> Result<(SectionConfigData, ConfigDigest), Error> {
let digest = ConfigDigest::from_slice(content.as_bytes());
let mut data = get_or_init_config().parse(user_config(), &content)?;
- access_conf().init_user_config(&mut data)?;
+ access_backend().init_user_config(&mut data)?;
Ok((data, digest))
}
@@ -113,7 +113,7 @@ pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
replace_privileged_config(config_file, raw.as_bytes())?;
// increase cache generation so we reload it next time we access it
- access_conf().increment_cache_generation()?;
+ access_backend().increment_cache_generation()?;
Ok(())
}
--
2.47.3
next prev parent reply other threads:[~2026-04-09 15:54 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-09 15:54 [PATCH proxmox{,-datacenter-manager} v8 0/9] token-shadow: reduce api token verification overhead Samuel Rufinatscha
2026-04-09 15:54 ` Samuel Rufinatscha [this message]
2026-04-09 15:54 ` [PATCH proxmox v8 2/6] token shadow: cache verified API token secrets Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox v8 3/6] token shadow: invalidate token-secret cache on token.shadow changes Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox v8 4/6] token shadow: add TTL window to token secret cache Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox v8 5/6] token shadow: inline set_secret fn Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox v8 6/6] token shadow: deduplicate more code into apply_api_mutation Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox-datacenter-manager v8 1/3] pdm-config: implement access control backend hooks Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox-datacenter-manager v8 2/3] pdm-config: wire user and ACL cache generation Samuel Rufinatscha
2026-04-09 15:54 ` [PATCH proxmox-datacenter-manager v8 3/3] pdm-config: wire token.shadow generation Samuel Rufinatscha
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=20260409155437.312760-2-s.rufinatscha@proxmox.com \
--to=s.rufinatscha@proxmox.com \
--cc=pbs-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.