Cut recurring Argon2 cost and remove per-lock WebDAV timers

Two hot-path fixes for DAV sync clients, which poll continuously:

1. App-password Basic Auth cache TTL 30s → 300s. Every cache miss costs
   a full Argon2id verification (~50-100ms CPU) plus two DB queries; at
   30s a continuously-syncing client re-paid that every 30s. 5 min cuts
   it ~10x. Safe because revoke() already invalidates the user's cached
   entries immediately; expiry/deactivation are only re-checked on a
   miss, so they now have a <=5 min grace window (comparable to a JWT
   access-token lifetime) — documented on the constant.

2. WebDAV lock store: replace the tokio::spawn + sleep scheduled per
   acquire/refresh with Moka's per-entry Expiry policy. Office clients
   refresh locks constantly, leaving thousands of orphaned sleeping
   timers pinned in the runtime that were never cancelled. by_path now
   carries the exact per-lock TTL via LockExpiry (no background tasks);
   by_token keeps a 24h backstop and resolves through by_path, so a
   lingering reverse-index entry can never resurrect an expired lock.
   Adds unit tests for the expiry policy and acquire/refresh/release
   (the module had none).

https://claude.ai/code/session_0193Hff42gaA962wThxMGSd1
This commit is contained in:
Claude
2026-06-11 09:38:35 +00:00
parent 6a27c742d4
commit a3a2d2f1cf
2 changed files with 214 additions and 57 deletions
@@ -30,9 +30,25 @@ const NC_APP_PASSWORD_GROUP_LEN: usize = 5;
const NC_PREFIX_LEN: usize = 8;
/// TTL for cached Basic Auth verification results.
/// Balances performance (avoids repeated Argon2id + DB queries) with security
/// (limits the window during which a revoked app password remains usable).
const BASIC_AUTH_CACHE_TTL_SECS: u64 = 30;
///
/// DAV sync clients (Nautilus, Windows Explorer, Apple Calendar, …) poll
/// continuously, and every cache miss costs a full Argon2id verification
/// (~50–100 ms of CPU) plus two DB round-trips. A 30 s TTL re-paid that
/// cost every 30 s per client; 5 min cuts it ~10× under steady sync load.
///
/// Security envelope of this window:
/// - **Revocation is immediate**: `revoke()` calls `invalidate_entries_if`
/// on this cache for the user, so a revoked password never survives in
/// cache regardless of TTL.
/// - **Expiry / deactivation are bounded by the TTL**: `expires_at` and
/// `user.is_active()` are only re-checked on a cache *miss* (the DB
/// query filters them), so an app password that expires — or a user
/// deactivated via `set_user_active` — may keep authenticating from
/// cache for at most this long. 5 min is comparable to a typical JWT
/// access-token lifetime, so the grace window is consistent across
/// auth surfaces. Lengthen with care; shorten if a tighter bound on
/// post-deactivation access is required.
const BASIC_AUTH_CACHE_TTL_SECS: u64 = 300;
/// Maximum number of cached Basic Auth verifications.
/// Each entry is ~160 bytes (32-byte key + 4 small strings), so 10 000
@@ -61,9 +77,9 @@ pub struct AppPasswordService {
///
/// **Value**: the authenticated identity (user_id, username, email, role).
///
/// **Eviction**: TTL-based (30 s) + capacity-based (10 000 entries).
/// Failed verifications are *never* cached, so brute-force attackers
/// always pay the full Argon2id cost.
/// **Eviction**: TTL-based (see `BASIC_AUTH_CACHE_TTL_SECS`) +
/// capacity-based (10 000 entries). Failed verifications are *never*
/// cached, so brute-force attackers always pay the full Argon2id cost.
auth_cache: Cache<[u8; 32], CachedBasicAuthResult>,
}