Grant model and shared permission-resolution algorithm #51

Closed
opened 2026-07-04 14:52:25 +02:00 by fable-5 · 1 comment
Collaborator

Context

The heart of the security model: one algorithm, implemented once, used by API, collab, and UI (permissions.md).

Scope

Prisma model role_grants per data-model.md with the structural constraints (pond_admin only pond-scope+user-subject; personal ponds single admin). Implement the resolution algorithm in packages/shared/src/permissions/ exactly as specified in permissions.md (specificity page > label(incl. ancestors) > pond; deny wins within a level; default-closed; Site Admin bypass; trash rule) with an exhaustive table-driven unit-test suite covering every worked example from the doc plus edge cases (multi-label conflicts, label-hierarchy inheritance, anonymous/public).

Acceptance criteria

  • every worked example from permissions.md §Resolution is a passing test case
  • property test: adding a less-specific grant never changes an outcome decided at a more specific level
  • invalid grants (pond_admin on a label; second admin on a personal pond) are rejected at write time
  • the algorithm is pure (no I/O) — inputs are grants+labels+page, output is a decision (documented signature for API/collab reuse)

Technical notes

  • permissions.md (authoritative), data-model.md (role_grants).
  • This story defines the API others consume — over-invest in tests and JSDoc here.

Dependencies

Depends on #43.

Size: ~2 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context The heart of the security model: one algorithm, implemented once, used by API, collab, and UI (permissions.md). ## Scope Prisma model `role_grants` per data-model.md with the structural constraints (pond_admin only pond-scope+user-subject; personal ponds single admin). Implement the resolution algorithm in `packages/shared/src/permissions/` exactly as specified in permissions.md (specificity page > label(incl. ancestors) > pond; deny wins within a level; default-closed; Site Admin bypass; trash rule) with an exhaustive table-driven unit-test suite covering every worked example from the doc plus edge cases (multi-label conflicts, label-hierarchy inheritance, anonymous/public). ## Acceptance criteria - [ ] every worked example from permissions.md §Resolution is a passing test case - [ ] property test: adding a less-specific grant never changes an outcome decided at a more specific level - [ ] invalid grants (pond_admin on a label; second admin on a personal pond) are rejected at write time - [ ] the algorithm is pure (no I/O) — inputs are grants+labels+page, output is a decision (documented signature for API/collab reuse) ## Technical notes - permissions.md (authoritative), data-model.md (`role_grants`). - This story defines the API others consume — over-invest in tests and JSDoc here. ## Dependencies Depends on #43. **Size**: ~2 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M5 — Permissions & quotas milestone 2026-07-04 14:52:25 +02:00
fable-5 added the
auth
backend
labels 2026-07-04 14:52:25 +02:00
Collaborator

Implemented in 4d48d72 (Claude Opus 4.8). Pipeline green (CI incl. all tests; CD deploy Test → smoke → promote Int — the role_grants migration + CHECK applied and the app booted healthy on both stages). #51 has no HTTP surface yet (the GrantsService is consumed by #52/#53), so it is verified by the exhaustive test suite plus the green deploy.

Acceptance criteria

  • every worked example from permissions.md §Resolution is a passing test case (resolve.test.ts, table-driven).
  • property test: adding a less-specific grant never changes an outcome decided at a more specific level (page-decided vs added label/pond grants; label-decided vs added pond grants).
  • invalid grants rejected at write time: GrantsService.createGrant rejects pond_admin-on-label and a second admin on a personal pond (db test); a DB CHECK backs the pond_admin-scope rule.
  • the algorithm is pure (no I/O): resolvePageCapability(action, ctx) — inputs are grants + labels + page + viewer, output a boolean; documented for API/collab reuse.

Design notes

  • packages/shared/src/permissions/: resolvePageCapability (specificity page > label incl. ancestors > pond; deny wins; default-closed; Site Admin bypass) + trash helpers canAccessPage / canAccessTrashedPage (ADR 0013); grantValidationError for structural validation.
  • RoleGrant model + grant enums; unique on (pond, subject, role, scope); migration CHECK: role <> 'POND_ADMIN' OR (scope_type='POND' AND subject_type='USER').
  • GrantsService (create with validation, grantsForPond returning the shared resolver model) + DB↔shared enum mappers. Who may manage grants stays interim (owner/Site Admin) until #52.

Next M5 issue: #52 (enforce permissions in API guards + retire interim access) — a larger security-critical refactor, handled as its own focused step.

Implemented in `4d48d72` (Claude Opus 4.8). Pipeline green (CI incl. all tests; CD deploy Test → smoke → promote Int — the `role_grants` migration + CHECK applied and the app booted healthy on both stages). #51 has no HTTP surface yet (the `GrantsService` is consumed by #52/#53), so it is verified by the exhaustive test suite plus the green deploy. **Acceptance criteria** - [x] every worked example from permissions.md §Resolution is a passing test case (`resolve.test.ts`, table-driven). - [x] property test: adding a less-specific grant never changes an outcome decided at a more specific level (page-decided vs added label/pond grants; label-decided vs added pond grants). - [x] invalid grants rejected at write time: `GrantsService.createGrant` rejects pond_admin-on-label and a second admin on a personal pond (db test); a DB CHECK backs the pond_admin-scope rule. - [x] the algorithm is pure (no I/O): `resolvePageCapability(action, ctx)` — inputs are grants + labels + page + viewer, output a boolean; documented for API/collab reuse. **Design notes** - `packages/shared/src/permissions/`: `resolvePageCapability` (specificity page > label incl. ancestors > pond; deny wins; default-closed; Site Admin bypass) + trash helpers `canAccessPage` / `canAccessTrashedPage` (ADR 0013); `grantValidationError` for structural validation. - `RoleGrant` model + grant enums; unique on (pond, subject, role, scope); migration CHECK: `role <> 'POND_ADMIN' OR (scope_type='POND' AND subject_type='USER')`. - `GrantsService` (create with validation, `grantsForPond` returning the shared resolver model) + DB↔shared enum mappers. Who may manage grants stays interim (owner/Site Admin) until #52. Next M5 issue: **#52** (enforce permissions in API guards + retire interim access) — a larger security-critical refactor, handled as its own focused step.
Sign in to join this conversation.
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#51
No description provided.