Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m40s
CI / Build container images (pull_request) Successful in 4m34s
CI / Auth e2e pack (pull_request) Successful in 9m7s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
New pages take max(instance default, parent level); moving a subtree under a higher-classified parent raises every member below that level. No move-like path (reposition, trash-promote, purge-promote) lowers a level as a side effect — pinned by test. Raising is ordinary editorial work; lowering requires the dedicated capability canLowerClassification (pond-wide Pond Admin) in the central permission model. Both directions are audited (page.classification_raised/_lowered, catalogue v1.1) with old value, new value, actor and page. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
119 lines
6.9 KiB
Markdown
119 lines
6.9 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).
|
|
|
|
## 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.
|