Authorization model
Roles come exclusively from the token’s groups claim (Keycloak group
paths):
/workspaces/{workspaceId}/viewer # read dashboards/workspaces/{workspaceId}/editor # create/update/generate dashboards/workspaces/{workspaceId}/source-admin # manage sources; delete dashboards/platform-admins # global admin (single sanctioned bypass)- Highest role wins within a workspace:
viewer < editor < source-admin. - Authorization is never derived from a workspace id in a request body. The
workspace is taken from a trusted, already-scoped resource
(
source.workspaceId,dashboard.workspaceId) or, for list and create operations, checked against the identity for the requested workspace. - Unknown or malformed groups are ignored —
parseGroupsfails closed and grants no role.
Action matrix
Section titled “Action matrix”| Action | Required |
|---|---|
| Dashboard list / get | viewer |
| Dashboard create / update / generate | editor |
| Dashboard delete | owner, source-admin, or platform-admin |
| Source CRUD / test / refresh | source-admin |
| Source use (list for a picker) | viewer |
| Workspace AI limits (read) | source-admin |
Workspace AI limits (change, workspace:limits) |
platform-admin only; no workspace role grants it |
One decision point
Section titled “One decision point”can(identity, action, ctx) in src/lib/auth/authorize.ts is the only place
role decisions are made, and the only place the platform-admin bypass applies.
Every route calls requireIdentity() then assertAuthorized(...); nothing
computes authorization inline.
The source is re-resolved and re-authorized on every execution, including each poller tick — so revoking access to a source stops in-flight dashboards from reading it, rather than waiting for a page reload.
Sessions
Section titled “Sessions”A request is authenticated by a signed JWT in the session cookie. Two verification strategies are selected by environment:
- Keycloak-issued tokens (production): verified against the realm JWKS
(RS256) with issuer and audience checks. Enabled when
OIDC_JWKS_URLandOIDC_ISSUERare configured. - Locally-signed session tokens (HS256 via
SESSION_SECRET): used by the OIDC callback to mint a first-party session.
Either way, only the validated sub and groups claims are ever trusted for
authorization. name and email are carried into the session as display-only
fields for the account menu and Settings → Account; can() never reads
them, and test/account.test.ts holds it to that.
Settings → Account describes each role by asking can() about a probe
identity that holds exactly that role, so the description cannot drift from
the rule.
The session cookie is httpOnly, Secure in production, SameSite=Lax,
path /, with an 8-hour lifetime.