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>
5.1 KiB
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_admingrants are rejected there. Shared ponds may have several. pond_admingrants 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), orpublic(everyone, including anonymous visitors). - scope: the whole pond, one label, or one page.
- effect:
allowordeny.denyexpresses 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?
- If U is Site Admin → allow.
- 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.
- Collect all grants in P's pond whose subject matches U
(their user id;
authenticatedif logged in;publicalways). - Keep grants whose role covers action A
(write needs
editor/pond_admin; read is covered by any role). - Evaluate by descending specificity; the first level that contains
any matching grant decides:
- Page scope — grants on P itself.
- 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.
- Pond scope — grants on the pond.
- Within the deciding level: if any matching grant is
deny→ deny, else allow. (Deny wins ties at the same specificity; a more specificallowstill beats a less specificdeny— that is the point of "most specific wins".) - 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.