[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