ADR-0056: Permission Resolution — Session-Embedded Cache (Option A)
Date: 2026-05-09 UTC
Status: Accepted
Deciders: software-architect
Refs: docs/architecture/rbac-design.md §8, docs/architecture/rbac-v2/design.md §3.4
Context
The permission resolution algorithm (user → groups → roles via DAG walk → permissions) involves multiple DB joins and a DAG traversal on every request. At v1 scale (single operator, small team), this is acceptable on every request. But as the console grows, the cost compounds. Three caching strategies were evaluated (per rbac-design.md §8):
- Option A: Session-scoped cache. On login, resolve the effective permission set and embed it as a JSONB column on the session record. Invalidated on any group membership or role assignment change for the user.
- Option B: Postgres materialized view, refreshed on writes.
- Option C: Redis permission set per user-id, TTL 5 minutes.
Decision
Option A: Session-embedded permission cache.
On login (when FLAG_RBAC_V2 is on), the permission resolution runs once and the result is stored in the session record as a JSONB column. Every subsequent request reads from the session — no DAG traversal required.
Cache invalidation: whenever rbac_user_groups or rbac_group_roles changes for a user, the post-grant service invalidates that user's session cache by clearing the JSONB column. The next request triggers a recompute.
Ticket-scoped grants are not cached in the session. They are checked live per request because their validity can change between requests (ticket closure).
Break-glass sessions maintain a separate in-memory marker; the session cache is not used for break-glass active checks.
Consequences
Positive: - No additional infra dependency (no Redis, no materialized view maintenance job). - The DAG traversal happens once per login, not per request. - Invalidation is exact: the affected user's cache is cleared immediately on any grant/revoke, not after a TTL window. - Compatible with the existing session model; the Console Postgres is already the session store.
Negative: - If the session record is in a read-only state (e.g., during a DB maintenance window), cache invalidation cannot be written. Mitigation: the grant service handles this gracefully — if cache invalidation fails, the old permission set is used until the session expires or the user logs in again. This is a degraded-but-not-broken state. - At very high console operator counts (>100), the session-embedded approach may produce stale reads in scenarios where many operators are active simultaneously and their caches need invalidating. At v1 scale (single operator), this is not a concern. Revisit at 100+ operators.
Alternatives Considered
Option B: Postgres materialized view
Requires a trigger or explicit refresh on every write to the five relevant tables. The refresh is cheap at low write rates. But adds materialized view complexity to the schema and introduces a lag between the write and the view refresh if deferred. Correct for a read-heavy multi-tenant admin console; premature for v1.
Option C: Redis TTL cache
Adds a Redis infra dependency. TTL-based invalidation means a 5-minute window where a revoked permission is still cached. For RBAC, 5-minute stale access after a revoke is a security concern. The exact invalidation of Option A is preferable.
See docs/architecture/rbac-v2/design.md §3.4 for the full resolution algorithm.