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

6.2 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_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 denydeny, 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.