Admin roles, escalation, and "view as" ======================================= Context ------- The admin panel UI existed but was inaccessible: no user could ever receive admin-level access at runtime. access.Admin() was only used by background workers and dev seeding. This note captures the design discussion that led to the support role implementation and the broader escalation/impersonation design. What we built ------------- A "support" system role in AppRoles with three caps: - CapTenantsRead — list all tenants system-wide - CapGrantsList — list members within any tenant - CapUsersList — list all users system-wide Seeded via a system grant (system = true) anchored to the user's personal tenant. The grants table requires a tenant FK for ownership/audit even on system grants; the personal tenant is the natural anchor for user-held system grants. (The schema has a TODO noting this feels odd.) System roles (not superadmin) ------------------------------ A single "superadmin" flag was rejected in favor of narrow system roles. The key principle: hold only the caps you routinely need; escalate to get more. Roles are additive capability sets, not a privilege hierarchy. A "support" role covering read access is separate from an "ops" role covering writes. Which caps belong to which roles is application-defined. The existing grant model already supports this: system = true on a grant makes the caps apply across all tenants without a scope check. Escalation model ----------------- Core idea: grants record eligibility, not current authorization. A user who holds an "ops" grant should NOT automatically operate with ops-level caps in their normal session. System caps should not be loaded at session start. Escalation is an explicit step: 1. User requests escalation (states which caps, states why) 2. System re-authenticates or accepts a second factor 3. A short-lived credential is issued with the escalated caps 4. An audit event is written at escalation time (not just at use time) 5. Credential expires; user returns to baseline session Open question: per-operation vs. per-window - Per-operation: user escalates for a specific cap + specific action. Safest. Audit record is tightly bound to the action. More friction. - Per-window: user escalates once, gets elevated caps for N minutes. More practical for multi-step support workflows. Harder to attribute individual actions to the escalation event after the fact. Leaning toward per-operation as the default, with per-window as an opt-in for roles where the friction is genuinely too high. Not decided. Escalation threshold (what requires it): - Common metadata reads (list all tenants, list members): no escalation. - Sensitive customer content (messages, files, PII-adjacent fields): escalation required — but this is application-defined, not framework-defined. - Any write to customer data: almost certainly requires escalation. - The framework should let each cap be declared as "requires escalation"; applications configure which caps are behind that gate. Impersonation — rejected in favor of two cleaner primitives ------------------------------------------------------------ True impersonation (you become the user, hold their caps, can act as them) was considered and rejected. Problems: - Attribution: audit log either lies (says Alice did it) or is confusing (Carol-as-Alice). Alice can't prove she didn't act. - Consent: the impersonated user hasn't agreed to someone acting as them. - Social engineering vector. The use cases that motivate impersonation break into two cleaner things: "View as" (read-only inspection): Show a support agent exactly what state a user's account is in — their effective grants, their data, what they would see in the UI — without giving the agent the user's caps or writing any footprint on the user's account. This is a scoped read query under escalation, not identity substitution. "Act for" (attributed write): A support agent directly modifies a resource. The audit record says "Carol changed Alice's email" — not "Alice changed her email." The agent is the actor; the fact that they were acting on behalf of a ticket or customer request is annotation, not identity substitution. Neither requires true impersonation. Both are more auditable and less deceptive. "View as" is not yet implemented; noted as a future capability. Bootstrap / seeding ------------------- The first system-role holder has to come from somewhere. Current approach: dev seeding adds a system grant directly. For production, options discussed: - Explicit seed: a tool or migration inserts a system grant for a known email address. Simple, operator-controlled. - IdP claim mapping: an IdP group membership maps to a system grant on login/token exchange. Good for orgs that already manage groups in their IdP. Layered on top of explicit seeding, not a replacement. - Email domain matching: "anyone @yourcompany.com is support." Rejected as too coarse — company email is not sufficient to hold system caps. No decision yet on production seeding. The grants model supports all of these; seeding strategy is deployment configuration, not a code architecture question. Open questions -------------- - Per-operation vs. per-window escalation (see above) - What cap (or mechanism) signals "this cap requires escalation"? - How does "view as" surface in the UI — a separate session/tab, a modal, something else? - Should the /admin frontend route check for any system cap and redirect non-system users, rather than letting them land on the UI and get 403s? - Long-term: the grants table's tenant FK on system grants still feels odd. Is there a cleaner schema (e.g., NULL tenant for system grants, with a separate audit reference)?