all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox v8 4/6] token shadow: add TTL window to token secret cache
Date: Thu,  9 Apr 2026 17:54:24 +0200	[thread overview]
Message-ID: <20260409155437.312760-5-s.rufinatscha@proxmox.com> (raw)
In-Reply-To: <20260409155437.312760-1-s.rufinatscha@proxmox.com>

cached_secret_valid() currently stats the file on every request, which
performs a metadata() call on token.shadow each time. Under load this
adds unnecessary overhead, considering also the file usually should
rarely change.

This patch introduces a TTL boundary, controlled by
TOKEN_SECRET_CACHE_TTL_SECS. File metadata is only re-loaded once the
TTL has expired.

Signed-off-by: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
---
Changes from v7 to v8:
* TTL fast path (read lock) and write-lock re-check now return
  cache.secret_matches(tokenid, secret) instead of just true, following
  the cached_secret_valid() merge from the previous patch
* Adjusted commit message

Changes from v6 to v7:
* Rebased

Changes from v5 to v6:
* Rebased

Changes from v4 to v5:
* Rebased
* Introduce shadow_check_within_ttl() helper

Changes from v3 to v4:
* Adjusted commit message

Changes from v2 to v3:
* Refactored refresh_cache_if_file_changed TTL logic.
* Remove had_prior_state check (replaced by last_checked logic).
* Improve TTL bound checks.
* Reword documentation warning for clarity.

Changes from v1 to v2:
* Add TOKEN_SECRET_CACHE_TTL_SECS and last_checked.
* Implement double-checked TTL: check with try_read first; only attempt
  refresh with try_write if expired/unknown.
* Fix TTL bookkeeping: update last_checked on the “file unchanged” path
  and after API mutations.
* Add documentation warning about TTL-delayed effect of manual
  token.shadow edits.

 proxmox-access-control/src/token_shadow.rs | 33 +++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/proxmox-access-control/src/token_shadow.rs b/proxmox-access-control/src/token_shadow.rs
index 810ff0c3..4185351e 100644
--- a/proxmox-access-control/src/token_shadow.rs
+++ b/proxmox-access-control/src/token_shadow.rs
@@ -27,6 +27,8 @@ static TOKEN_SECRET_CACHE: LazyLock<RwLock<ApiTokenSecretCache>> = LazyLock::new
         file_info: None,
     })
 });
+/// Max age in seconds of the token secret cache before checking for file changes.
+const TOKEN_SECRET_CACHE_TTL_SECS: i64 = 60;
 
 // Get exclusive lock
 fn lock_config() -> Result<ApiLockGuard, Error> {
@@ -57,15 +59,31 @@ fn write_file(data: HashMap<Authid, String>) -> Result<(), Error> {
 /// tokens for in-flight requests may still validate against the previous
 /// generation.
 ///
+/// If the cache file metadata's TTL has expired, will revalidate and invalidate the cache if
+/// needed.
+///
 /// Returns true if secret is cached and cache is still valid
 fn cached_secret_valid(tokenid: &Authid, secret: &str) -> bool {
     let now = epoch_i64();
 
-    // Best-effort refresh under write lock.
+    // Fast path: cache is fresh if generation matches and TTL not expired.
+    if let (Some(cache), Some(read_gen)) =
+        (TOKEN_SECRET_CACHE.try_read(), token_shadow_generation())
+    {
+        if cache.cached_gen == read_gen && cache.shadow_check_within_ttl(now) {
+            return cache.secret_matches(tokenid, secret);
+        }
+        // read lock drops here
+    } else {
+        return false;
+    }
+
+    // Slow path: best-effort refresh under write lock.
     let Some(mut cache) = TOKEN_SECRET_CACHE.try_write() else {
         return false;
     };
 
+    // Re-read generation after acquiring the lock (may have changed meanwhile).
     let Some(current_gen) = token_shadow_generation() else {
         return false;
     };
@@ -75,6 +93,12 @@ fn cached_secret_valid(tokenid: &Authid, secret: &str) -> bool {
         cache.reset_and_set_gen(current_gen);
     }
 
+    // TTL check again after acquiring the lock
+    let now = epoch_i64();
+    if cache.shadow_check_within_ttl(now) {
+        return cache.secret_matches(tokenid, secret);
+    }
+
     // Stat the file to detect manual edits.
     let Ok((new_mtime, new_len)) = shadow_mtime_len() else {
         return false;
@@ -224,6 +248,13 @@ impl ApiTokenSecretCache {
         self.cached_gen = new_gen;
     }
 
+    /// Returns true if cached token.shadow metadata exists and was checked within the TTL window.
+    fn shadow_check_within_ttl(&self, now: i64) -> bool {
+        self.file_info.as_ref().is_some_and(|cached| {
+            now >= cached.last_checked && (now - cached.last_checked) < TOKEN_SECRET_CACHE_TTL_SECS
+        })
+    }
+
     /// Returns true if there is a matching cached entry
     fn secret_matches(&self, tokenid: &Authid, secret: &str) -> bool {
         let Some(entry) = self.secrets.get(tokenid) else {
-- 
2.47.3





  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 ` [PATCH proxmox v8 1/6] token shadow: split AccessControlConfig and add token.shadow generation Samuel Rufinatscha
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 ` Samuel Rufinatscha [this message]
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-5-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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal