Skip to content

Authorization

Access is role-based and scoped to a single workspace:

Role Can
READER Read documents, links, events, jobs, and see who else has access.
MEMBER Everything READER can, plus create/update/delete documents, links, and jobs.
MANAGER Everything MEMBER can, plus manage workspace membership itself.

The one instance-wide right is a superuser flag, checked first and bypassing the role grid entirely. Everyone else’s access is computed fresh on every request from the workspace’s roles, never baked into the session token — so revoking a role takes effect on the next request, not on the session’s expiry.

A role isn’t granted to one person at a time. Each grant stores a role plus a grant_match — a small JSONB document, matched against every field an OIDC login populated on the principal (its id, its type, and every claim your provider returned):

{"groups": "finance"}

matches any principal whose groups claim includes "finance"; {"id": "alice"} matches exactly one principal. A rule with several keys matches on any of them (OR), and a key whose value on the principal’s side is a list matches by membership rather than equality — groups today, but the same logic applies to any future list-shaped claim.

That matching runs in plain Python at request time, deliberately not through Postgres’s own JSONB containment operator (@>): the same check needs to run over an in-memory list of principals too (to answer “who does this rule currently cover,” for the Members page), and a check that’s implemented once, in one language, is easier to reason about — and to unit test exhaustively, list-vs-scalar cases included — than trusting @>’s own containment semantics for something this security-sensitive.

The practical upshot: adding someone to the finance group in your identity provider grants them every role matching {"groups": "finance"} on their very next login, with nothing to update inside Atlas. The trade-off is the mirror image of a membership table’s own — “who currently has access” is computed by evaluating every rule against the current principals, not read back from a stored list.

Managing membership itself is kept out of the generic read/write/delete grid above: every role can read who has access (so a READER isn’t locked out of the Members page for their own workspace), but only MANAGER can write it. Folding that into the same grid as documents/links/events would have silently handed MEMBER the right to change membership just because MEMBER already has generic write access.

Terminal window
curl -b cookies.txt -X POST http://localhost:8000/api/v1/workspaces/{workspace_id}/roles \
-H "Content-Type: application/json" \
-d '{"role": "MEMBER", "grant_match": {"groups": "finance"}}'
Goal grant_match
One specific person {"id": "alice"}
An entire OIDC group {"groups": "finance"}
Every API key (service accounts) {"type": "API"}

Matching on a claim like groups only works if your OIDC scope actually returns it — see Authentication’s claims section and ATLAS_AUTH_OIDC_SCOPE in CORS & Authentication.