From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id A6F8E1FF13F for ; Thu, 09 Apr 2026 17:54:11 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E7403A7CA; Thu, 9 Apr 2026 17:54:52 +0200 (CEST) From: Samuel Rufinatscha 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 Message-ID: <20260409155437.312760-2-s.rufinatscha@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260409155437.312760-1-s.rufinatscha@proxmox.com> References: <20260409155437.312760-1-s.rufinatscha@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1775750013573 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.227 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 2J2SOYJNIRJIGH2CEFEQJPPZRQ2Y3N2R X-Message-ID-Hash: 2J2SOYJNIRJIGH2CEFEQJPPZRQ2Y3N2R X-MailFrom: s.rufinatscha@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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, Error> { let now = epoch_i64(); - let cache_generation = access_conf().cache_generation(); + let cache_generation = access_backend().cache_generation(); static CACHED_CONFIG: OnceLock> = 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 { - 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 { + 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 { + 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 { + 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 = OnceLock::new(); - pub fn init>( - acm_config: &'static dyn AccessControlConfig, + pub fn init(config: &'static T, config_dir: P) -> Result<(), Error> + where + T: AccessControlConfig + AccessControlBackend, + P: AsRef, + { + init_access_config(config)?; + init_access_backend(config)?; + init_access_config_dir(config_dir) + } + + pub fn init_separate>( + 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