fix(authz): invalidate role cache on change
This commit is contained in:
@@ -283,6 +283,62 @@ impl PgAclEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop the `owner_cache` entry for `resource`. Called after any
|
||||
/// operation that changes which drive a file/folder belongs to —
|
||||
/// the pre-D6 comment on `owner_cache` ("a resource's owner is
|
||||
/// immutable") stopped being true when cross-drive MOVE landed.
|
||||
///
|
||||
/// Without this call, admin (or any other role holder) on the
|
||||
/// destination drive gets `authz.denied` when acting on the moved
|
||||
/// resource: the cached (stale) `Resource → src_drive_id` lookup
|
||||
/// steers the drive-role precheck at `check_inner` toward the
|
||||
/// SOURCE drive where the caller has no role, and the fallback
|
||||
/// per-resource cascade doesn't cover drive-level grants. TTL
|
||||
/// backstops eventually (5 min), but every write path that MOVEs
|
||||
/// content across drives MUST invalidate here so authz observes
|
||||
/// the new drive on the next check.
|
||||
pub async fn invalidate_owner_cache_for_resource(&self, resource: Resource) {
|
||||
self.owner_cache.invalidate(&resource).await;
|
||||
}
|
||||
|
||||
/// Bulk cousin of [`Self::invalidate_owner_cache_for_resource`] —
|
||||
/// clears the entire `owner_cache`. Called by folder cross-drive
|
||||
/// MOVE where the moved subtree's descendants each carry their
|
||||
/// own stale entry, and we don't (yet) walk the subtree to
|
||||
/// invalidate them individually. The cache repopulates lazily on
|
||||
/// next access; the overhead is a single JOIN per file/folder
|
||||
/// touched in the following minute or two, versus a stale-authz
|
||||
/// bug that returned `NotFound` for legitimate Delete.
|
||||
pub async fn invalidate_owner_cache_all(&self) {
|
||||
self.owner_cache.invalidate_all();
|
||||
}
|
||||
|
||||
/// Sibling of [`Self::invalidate_drive_role_cache_for_drive`] keyed by
|
||||
/// subject rather than drive. Used by the user-deleted lifecycle hook
|
||||
/// to reap every cached "user X → drive Y = role R" entry after the
|
||||
/// user row (and its DB-cascade-cleared role_grants) is gone. Without
|
||||
/// this call the entry lingers until TTL; in practice auth rejection
|
||||
/// on the deleted user's tokens fires first, but leaving stale
|
||||
/// authorisation rows in the cache is poor hygiene and would surface
|
||||
/// as an issue if a session survived (e.g. long-lived Basic Auth via
|
||||
/// app password) or if a same-uuid user were ever recreated.
|
||||
pub async fn invalidate_drive_role_cache_for_subject(&self, subject: Subject) {
|
||||
if let Err(err) = self
|
||||
.drive_role_cache
|
||||
.invalidate_entries_if(move |key, _v| key.0 == subject)
|
||||
{
|
||||
tracing::error!(
|
||||
target: "oxicloud::authz",
|
||||
event = "authz.cache_invalidation_failed",
|
||||
cache = "drive_role_cache",
|
||||
subject = ?subject,
|
||||
error = %err,
|
||||
"drive_role_cache cannot be bulk-invalidated by subject — \
|
||||
cache builder is missing support_invalidation_closures()",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand a user subject into the set of subject UUIDs that should match
|
||||
/// in `access_grants`: the user's own UUID, every group the user is
|
||||
/// transitively a member of, and (for internal users only) the implicit
|
||||
@@ -2099,8 +2155,19 @@ impl UserLifecycleHook for AuthzCacheLifecycleHook {
|
||||
_tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), DomainError> {
|
||||
// No DB writes here — just memory invalidation. `_tx` is
|
||||
// intentionally ignored.
|
||||
// intentionally ignored. The DB cascade
|
||||
// (`trg_cleanup_role_grants_user`) already dropped every
|
||||
// role_grants row for this subject; we mirror that cleanup on
|
||||
// both authz caches:
|
||||
// 1. `user_groups_cache` — recomputed group expansion.
|
||||
// 2. `drive_role_cache` — cached "user X → drive Y = role R"
|
||||
// entries seeded by prior authz checks. Without this
|
||||
// the deleted user's role stays visible in-process for
|
||||
// up to the cache TTL (~30 s).
|
||||
self.engine.invalidate_user_groups_cache(user.id()).await;
|
||||
self.engine
|
||||
.invalidate_drive_role_cache_for_subject(Subject::User(user.id()))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user