All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m55s
CI / Build container images (pull_request) Successful in 3m0s
CI / Auth e2e pack (pull_request) Successful in 8m49s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 21s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m28s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m15s
CI / Import/export fidelity gate (push) Successful in 59s
Declarative instance setting idpMapping.rules turns ID-token claims into pond roles and the site-admin flag on every OIDC login — configuration, not code. Mapped grants travel through the SAME GrantsService path as manual ones (permission cache invalidated, collab access notify fires so live sessions revalidate — asserted by test), never raw rows. Ownership makes precedence explicit: role_grants.origin marks mapped rows, users.is_site_admin_managed marks a mapping-set admin flag. The mapping only creates and revokes what it owns — manual wins: hand-made grants and hand-promoted admins are never revoked by a missing claim (a manual toggle clears the marker and takes ownership). Removal of a claim revokes the mapped grant and the managed flag on the next login. Every mapping-driven change is audited with origin idp_mapping. Failure containment: unknown pond slugs and the last-Pond-Admin protection log-and-skip — a mapping problem must never become a login lockout. Tests drive real OIDC logins against the fake IdP with group claims: grant + working access, revocation incl. notify, manual-wins, managed site-admin promote/demote/hands-off. Documented in permissions.md (own section), ADR 0021, data-model.md and the hardening guide (care rule: same PR). Refs #217. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
154 lines
8.5 KiB
Markdown
154 lines
8.5 KiB
Markdown
# Permission model — "the most specific setting wins"
|
|
|
|
This document defines the authoritative semantics of roles and grants. The
|
|
resolution algorithm is implemented **once** in
|
|
`packages/shared/src/permissions/` and used by the API (guards), the collab
|
|
server (token issuance), and the frontend (UI affordances only — the client
|
|
never enforces security).
|
|
|
|
## Roles
|
|
|
|
| Role | Scope of existence | Capabilities |
|
|
| -------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Site Admin** | instance (user flag) | everything: instance settings, users, quotas, plugins (install + set `disabled`/`optional`/`required`), all ponds' administration, legal pages, backups |
|
|
| **Pond Admin** | per pond (grant) | manage the pond: settings/fonts, labels, members and their grants, enable/disable optional plugins, trash, quotas view; implies Editor everywhere in the pond |
|
|
| **Editor** | per pond/label/page (grant) | create/edit/delete pages within the granted scope; view history, restore versions, use trash for pages they may edit |
|
|
| **Reader** | per pond/label/page (grant) | read pages within the granted scope; no history access |
|
|
| **Public** | pseudo-subject | what non-authenticated visitors may read (never write) |
|
|
|
|
Additional structural rules:
|
|
|
|
- Personal ponds (self-signup) have exactly **one** Pond Admin — the owner;
|
|
additional `pond_admin` grants are rejected there. Shared ponds may have
|
|
several.
|
|
- `pond_admin` grants exist only at pond scope. Editor/Reader grants exist
|
|
at pond, label, or page scope.
|
|
- Site Admin bypasses resolution entirely.
|
|
|
|
## Grants
|
|
|
|
A grant is `(subject, role, scope, effect)` inside one pond
|
|
(table `role_grants`, see `data-model.md`):
|
|
|
|
- **subject**: a specific user, `authenticated` (any logged-in user), or
|
|
`public` (everyone, including anonymous visitors).
|
|
- **scope**: the whole pond, one label, or one page.
|
|
- **effect**: `allow` or `deny`. `deny` expresses the vision's "all pages
|
|
_except_ label X" (pond-scope allow + label-scope deny).
|
|
- **origin**: `manual` (admin-created) or `idp` (written by the claim
|
|
mapping below). Resolution ignores the column — it only exists so the
|
|
mapping can tell its own rows apart.
|
|
|
|
## IdP claim mapping (issue #217, ADR 0021)
|
|
|
|
With external authentication (#214), the instance setting
|
|
`idpMapping.rules` maps ID-token claims declaratively onto this model —
|
|
configuration, not code:
|
|
|
|
```json
|
|
[
|
|
{ "claim": "groups", "value": "wiki-editors", "role": "editor", "pondSlug": "team-wiki" },
|
|
{ "claim": "groups", "value": "wiki-admins", "role": "site_admin" }
|
|
]
|
|
```
|
|
|
|
Semantics, decided and tested:
|
|
|
|
- **Applied on every OIDC login** (fresh claims exist only there).
|
|
Matching is string equality; array claims match by containment. Several
|
|
rules for one pond: the strongest role wins.
|
|
- **Same service path as manual grants** (`GrantsService`) — the
|
|
permission cache is invalidated and the collab access notify fires, so
|
|
live sessions revalidate exactly as on a manual change. Never raw rows.
|
|
- **Removal of a claim revokes the mapped grant on the next login.** The
|
|
mapping only ever touches rows with `origin = 'idp'` — **manual wins**:
|
|
hand-made grants and hand-promoted site admins are never revoked by a
|
|
missing claim (`users.is_site_admin_managed` marks a mapping-set flag;
|
|
a manual toggle clears the marker and takes ownership).
|
|
- **Every mapping-driven change is audited** (`grant.created` /
|
|
`grant.deleted` / `user.site_admin_set` with `origin: idp_mapping`).
|
|
- Configuration errors (unknown pond slug) and refusals (last-Pond-Admin
|
|
protection) are logged and skipped — a mapping problem must never
|
|
become a login lockout.
|
|
|
|
## Resolution algorithm
|
|
|
|
Question: _may user U perform action A (read / write) on page P?_
|
|
|
|
1. If U is Site Admin → **allow**.
|
|
2. If the page or its pond is in trash → only roles that could edit it may
|
|
see it in trash views; regular access is denied.
|
|
3. Collect all grants in P's pond whose subject matches U
|
|
(their user id; `authenticated` if logged in; `public` always).
|
|
4. Keep grants whose role covers action A
|
|
(write needs `editor`/`pond_admin`; read is covered by any role).
|
|
5. Evaluate by **descending specificity**; the first level that contains
|
|
any matching grant decides:
|
|
1. **Page scope** — grants on P itself.
|
|
2. **Label scope** — grants on any label assigned to P, **including
|
|
inherited labels**: a grant on label L applies to L and all its
|
|
descendants in the label hierarchy.
|
|
3. **Pond scope** — grants on the pond.
|
|
6. Within the deciding level: if any matching grant is `deny` → **deny**,
|
|
else **allow**. (Deny wins ties at the same specificity; a more specific
|
|
`allow` still beats a less specific `deny` — that is the point of
|
|
"most specific wins".)
|
|
7. No matching grant at any level → **deny** (default-closed).
|
|
|
|
### Worked examples
|
|
|
|
Pond "Handbook", user Uma has pond-scope `editor` (allow):
|
|
|
|
| Extra grants | Uma edits page "Salaries"? | Why |
|
|
| ------------------------------------------------------------------------ | -------------------------- | -------------------------------------------- |
|
|
| — | yes | pond-scope allow |
|
|
| label `confidential` on the page + label-scope `editor` **deny** for Uma | no | label level is more specific than pond level |
|
|
| additionally page-scope `editor` **allow** for Uma on "Salaries" | yes | page level beats label level |
|
|
| page has labels `confidential` (deny Uma) and `hr` (allow Uma) | no | same level → deny wins |
|
|
|
|
"Only pages with label Y" (vision) = no pond-scope editor grant + label-Y
|
|
`editor` allow. "All except label X" = pond-scope allow + label-X deny.
|
|
|
|
### Non-page objects
|
|
|
|
- **Attachments** inherit the permissions of their page (or pond for
|
|
pond-level files): read requires read on the page, upload/delete requires
|
|
write.
|
|
- **Comments** (later milestone): reading follows page read; writing
|
|
comments requires page read + the pond setting "who may comment"
|
|
(readers-and-up or editors-only).
|
|
- **History & trash**: require write permission on the affected page
|
|
(ADR 0013).
|
|
- **Search** results are filtered through the same resolution (ADR 0010).
|
|
- **Plugin API** calls execute with the viewing user's permissions
|
|
(ADR 0008) — the API enforces this server-side.
|
|
- **Classification lowering** (issue #205, ADR 0022): _raising_ a page's
|
|
VS-NfD classification is ordinary editorial work and needs only write
|
|
permission; _lowering_ it is the sensitive direction (content escapes
|
|
marking through it) and requires the dedicated capability
|
|
`canLowerClassification` — pond-wide **Pond Admin** (the owner's
|
|
standing grant; Site Admin bypasses). The capability lives in the
|
|
shared model (`permissions/pond.ts`), never as an ad-hoc check; every
|
|
raise and lower is audited (`page.classification_raised` /
|
|
`page.classification_lowered`, see `audit-events.md`). The
|
|
classification itself changes **no** read/write decision — it is a
|
|
marking, not a protection mechanism.
|
|
|
|
## Performance
|
|
|
|
Resolution needs the page's labels (+ label ancestors) and the pond's
|
|
grants — a handful of indexed queries, cacheable per (user, pond) with
|
|
event-based invalidation on grant/label changes. The collab server resolves
|
|
once at token issuance (token TTL ≤ 60 s) and re-checks on reconnect;
|
|
revoking write access closes live sessions via a pond-level notification
|
|
(`LISTEN/NOTIFY`).
|
|
|
|
## UI obligations
|
|
|
|
- The frontend hides actions the user lacks (buttons, routes) but every
|
|
denied server response renders a proper i18n-ed error — client checks are
|
|
convenience, not security.
|
|
- Pond Admin UI must make effective permissions inspectable: "show effective
|
|
access for user X / for Public" per page — this transparency feature is a
|
|
story of its own and load-bearing for admin trust.
|