_roleDefaultsCache in trpc.ts is process-local with 60 s TTL. After permission revocation, a compromised user keeps old permissions for up to 60 s. In multi-instance deployments, invalidateRoleDefaultsCache() only clears on the invoking node — other nodes remain stale.
Window of 60 s (single node) or longer (multi-node) where revoked permissions remain active. Acceptable in low-privilege revocations, risky for admin demotions.
Proposed Fix
(1) Publish cache-invalidation event via Redis pub/sub on permission changes — already have Redis. All nodes subscribe and clear local cache. (2) Reduce TTL to 10 s for security-sensitive keys. (3) For admin demotions specifically, also invalidate all active sessions of the target user.
Acceptance Criteria
Redis pub/sub invalidation in place
Multi-instance revocation test: 2nd node sees update within 1 s
## Problem
`_roleDefaultsCache` in trpc.ts is process-local with 60 s TTL. After permission revocation, a compromised user keeps old permissions for up to 60 s. In multi-instance deployments, `invalidateRoleDefaultsCache()` only clears on the invoking node — other nodes remain stale.
## Evidence
- `packages/api/src/trpc.ts:25-50 — ROLE_DEFAULTS_TTL = 60_000, module-local`
## Impact
Window of 60 s (single node) or longer (multi-node) where revoked permissions remain active. Acceptable in low-privilege revocations, risky for admin demotions.
## Proposed Fix
(1) Publish cache-invalidation event via Redis pub/sub on permission changes — already have Redis. All nodes subscribe and clear local cache. (2) Reduce TTL to 10 s for security-sensitive keys. (3) For admin demotions specifically, also invalidate all active sessions of the target user.
## Acceptance Criteria
- [ ] Redis pub/sub invalidation in place
- [ ] Multi-instance revocation test: 2nd node sees update within 1 s
- [ ] Admin demotion forces user re-login
---
Parent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (A-19)
Resolved in e2dddd3 on branch security/audit-2026-04-17.
Changes
packages/api/src/trpc.ts — shrink ROLE_DEFAULTS_TTL from 60s to 10s as fail-safe; publish/subscribe on capakraken:rbac-invalidate so peer instances drop their local cache on mutation (lazy-init ioredis client so modules that never touch RBAC do not open a connection)
packages/api/src/router/user-procedure-support.ts — after updateUserRole (when role actually changes), setUserPermissions, resetUserPermissions: delete every ActiveSession row for the affected user so the next request re-auths through the tRPC jti check, and call invalidateRoleDefaultsCache() to fan out the invalidation
Tests
rbac-cache-redis-pubsub.test.ts — FakeRedis pub/sub fan-out: receiving a message on the channel clears the cache so next loadRoleDefaults() re-reads from DB; invalidateRoleDefaultsCache() publishes exactly once on the channel
user-role-permission-session-invalidation.test.ts — mutation side-effects assert session deletion + cache invalidation on each privileged mutation path, and verify the no-op path (unchanged role) does neither
Acceptance: within at most 10s (and typically sub-second via pub/sub) a role/permission change invalidates the role-defaults cache on every node; any existing JWT for the affected user is rejected at the next request because its jti no longer matches an ActiveSession row.
Resolved in e2dddd3 on branch `security/audit-2026-04-17`.
**Changes**
- `packages/api/src/trpc.ts` — shrink `ROLE_DEFAULTS_TTL` from 60s to 10s as fail-safe; publish/subscribe on `capakraken:rbac-invalidate` so peer instances drop their local cache on mutation (lazy-init ioredis client so modules that never touch RBAC do not open a connection)
- `packages/api/src/router/user-procedure-support.ts` — after `updateUserRole` (when role actually changes), `setUserPermissions`, `resetUserPermissions`: delete every `ActiveSession` row for the affected user so the next request re-auths through the tRPC `jti` check, and call `invalidateRoleDefaultsCache()` to fan out the invalidation
**Tests**
- `rbac-cache-redis-pubsub.test.ts` — FakeRedis pub/sub fan-out: receiving a message on the channel clears the cache so next `loadRoleDefaults()` re-reads from DB; `invalidateRoleDefaultsCache()` publishes exactly once on the channel
- `user-role-permission-session-invalidation.test.ts` — mutation side-effects assert session deletion + cache invalidation on each privileged mutation path, and verify the no-op path (unchanged role) does neither
Acceptance: within at most 10s (and typically sub-second via pub/sub) a role/permission change invalidates the role-defaults cache on every node; any existing JWT for the affected user is rejected at the next request because its `jti` no longer matches an `ActiveSession` row.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
_roleDefaultsCachein trpc.ts is process-local with 60 s TTL. After permission revocation, a compromised user keeps old permissions for up to 60 s. In multi-instance deployments,invalidateRoleDefaultsCache()only clears on the invoking node — other nodes remain stale.Evidence
packages/api/src/trpc.ts:25-50 — ROLE_DEFAULTS_TTL = 60_000, module-localImpact
Window of 60 s (single node) or longer (multi-node) where revoked permissions remain active. Acceptable in low-privilege revocations, risky for admin demotions.
Proposed Fix
(1) Publish cache-invalidation event via Redis pub/sub on permission changes — already have Redis. All nodes subscribe and clear local cache. (2) Reduce TTL to 10 s for security-sensitive keys. (3) For admin demotions specifically, also invalidate all active sessions of the target user.
Acceptance Criteria
Parent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (A-19)
Resolved in
e2dddd3on branchsecurity/audit-2026-04-17.Changes
packages/api/src/trpc.ts— shrinkROLE_DEFAULTS_TTLfrom 60s to 10s as fail-safe; publish/subscribe oncapakraken:rbac-invalidateso peer instances drop their local cache on mutation (lazy-init ioredis client so modules that never touch RBAC do not open a connection)packages/api/src/router/user-procedure-support.ts— afterupdateUserRole(when role actually changes),setUserPermissions,resetUserPermissions: delete everyActiveSessionrow for the affected user so the next request re-auths through the tRPCjticheck, and callinvalidateRoleDefaultsCache()to fan out the invalidationTests
rbac-cache-redis-pubsub.test.ts— FakeRedis pub/sub fan-out: receiving a message on the channel clears the cache so nextloadRoleDefaults()re-reads from DB;invalidateRoleDefaultsCache()publishes exactly once on the channeluser-role-permission-session-invalidation.test.ts— mutation side-effects assert session deletion + cache invalidation on each privileged mutation path, and verify the no-op path (unchanged role) does neitherAcceptance: within at most 10s (and typically sub-second via pub/sub) a role/permission change invalidates the role-defaults cache on every node; any existing JWT for the affected user is rejected at the next request because its
jtino longer matches anActiveSessionrow.