dorfteich/docs/architecture/permissions.md
Claude Fable 5 b16d23297e Scaffold pnpm monorepo with lint, format, and test tooling
pnpm workspace with apps/web, apps/api, apps/collab, and
packages/shared; strict TypeScript base config, repo-wide ESLint (flat)
+ Prettier, Vitest per package, and root scripts lint/typecheck/test/
build. @dorfteich/shared ships a first health-response helper consumed
by apps/api to prove workspace linking. Existing markdown docs are
reformatted once by the new Prettier setup.

Closes #1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:06:27 +02:00

108 lines
6.2 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.