Initial deliverable of the architecture phase: 16 ADRs (stack, CRDT collaboration, plugin sandbox, import/export, backups, CI/CD), data model, permission model, real-time collaboration and plugin concepts, deployment/operations/security documentation, and the milestone roadmap that the implementation issues are derived from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
108 lines
5.1 KiB
Markdown
108 lines
5.1 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.
|
|
|
|
## 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.
|