[pbs-devel] [PATCH proxmox 2/3] proxmox-access-control: invalidate token-secret cache on token.shadow changes
Samuel Rufinatscha
s.rufinatscha at proxmox.com
Fri Dec 5 14:25:58 CET 2025
Previously the in-memory token-secret cache was only updated via
set_secret() and delete_secret(), so manual edits to token.shadow were
not reflected.
This patch adds file change detection to the cache. It tracks the mtime
and length of token.shadow and clears the in-memory token secret cache
whenever these values change.
Note, this patch fetches file stats on every request. An TTL-based
optimization will be covered in a subsequent patch of the series.
This patch is a partly-fix.
Signed-off-by: Samuel Rufinatscha <s.rufinatscha at proxmox.com>
---
proxmox-access-control/src/token_shadow.rs | 35 ++++++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/proxmox-access-control/src/token_shadow.rs b/proxmox-access-control/src/token_shadow.rs
index 2dcd117d..d08fb06a 100644
--- a/proxmox-access-control/src/token_shadow.rs
+++ b/proxmox-access-control/src/token_shadow.rs
@@ -1,5 +1,8 @@
use std::collections::HashMap;
+use std::fs;
+use std::io::ErrorKind;
use std::sync::{OnceLock, RwLock};
+use std::time::SystemTime;
use anyhow::{bail, format_err, Error};
use serde_json::{from_value, Value};
@@ -38,12 +41,38 @@ fn write_file(data: HashMap<Authid, String>) -> Result<(), Error> {
replace_config(token_shadow(), &json)
}
+fn refresh_cache_if_file_changed() -> Result<(), Error> {
+ let mut cache = token_secret_cache().write().unwrap();
+
+ // Fetch the current token.shadow metadata
+ let (new_mtime, new_len) = match fs::metadata(token_shadow().as_path()) {
+ Ok(meta) => (meta.modified().ok(), Some(meta.len())),
+ Err(e) if e.kind() == ErrorKind::NotFound => (None, None),
+ Err(e) => return Err(e.into()),
+ };
+
+ // Fast path: file did not change, keep the cache
+ if cache.file_mtime == new_mtime && cache.file_len == new_len {
+ return Ok(());
+ }
+
+ // File changed, drop all cached secrets
+ cache.secrets.clear();
+ cache.file_mtime = new_mtime;
+ cache.file_len = new_len;
+
+ Ok(())
+}
+
/// Verifies that an entry for given tokenid / API token secret exists
pub fn verify_secret(tokenid: &Authid, secret: &str) -> Result<(), Error> {
if !tokenid.is_token() {
bail!("not an API token ID");
}
+ // Ensure cache is in sync with on-disk token.shadow file
+ refresh_cache_if_file_changed()?;
+
// Fast path
if let Some(cached) = token_secret_cache().read().unwrap().secrets.get(tokenid) {
// Compare cached secret with provided one using constant time comparison
@@ -117,12 +146,18 @@ struct ApiTokenSecretCache {
/// `generate_and_set_secret`. Used to avoid repeated
/// password-hash computation on subsequent authentications.
secrets: HashMap<Authid, String>,
+ // shadow file mtime to detect changes
+ file_mtime: Option<SystemTime>,
+ // shadow file length to detect changes
+ file_len: Option<u64>,
}
fn token_secret_cache() -> &'static RwLock<ApiTokenSecretCache> {
TOKEN_SECRET_CACHE.get_or_init(|| {
RwLock::new(ApiTokenSecretCache {
secrets: HashMap::new(),
+ file_mtime: None,
+ file_len: None,
})
})
}
--
2.47.3
More information about the pbs-devel
mailing list