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 JSONB rule, not a membership row
Section titled “A JSONB rule, not a membership row”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.
Granting a role
Section titled “Granting a role”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.