Add a read-only role per app for calendar, feedback and tickets
A grant can now be `read` instead of `access`: requireAppAccess lets it through for GET/HEAD/OPTIONS and answers 403 to every other method, so new write routes are covered without being marked. `read` and `access` on the same app are rejected together; user management has no read role. /tickets/admin/me and /feedback/admin/me report `readOnly`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
9 files changed
+377
-10
No files matched your search
@@ -0,0 +1,73 @@
|
||||
# Admin roles: full access and read-only
|
||||
|
||||
Added 2026-10-29. A user's permission is a list of **(app, role)** grants in
|
||||
`user_app_permissions` (see `src/models/admin/admin.schema.ts`). Each app can now be granted at
|
||||
one of two levels:
|
||||
|
||||
| Role | UI label (admin app) | Apps | Meaning |
|
||||
|----------|----------------------|-------------------------------|--------------------------------------------|
|
||||
| `access` | Vollzugriff | calendar, feedback, tickets, admin | Everything in that app, as before. |
|
||||
| `read` | Nur lesen | calendar, feedback, tickets | See everything, change nothing. |
|
||||
|
||||
Rules:
|
||||
|
||||
- **`access` and `read` on the same app are mutually exclusive.** `toPermissions` returns `null`
|
||||
for a list holding both, so `PUT /admin/users/:id/permissions` and `POST /admin/invitations`
|
||||
answer 400. There is no "higher one wins".
|
||||
- **`admin` (user management) has no `read` role.** Viewing it means viewing other people's
|
||||
email addresses, IPs and sessions; that is a different trust level from viewing concerts.
|
||||
- Existing grants were not migrated: every pre-existing row is `access` and still means full
|
||||
access.
|
||||
|
||||
## Enforcement
|
||||
|
||||
`requireAppAccess(app)` (no role argument - which is how every app router uses it) does, after
|
||||
the existing session/disabled/has-any-grant checks:
|
||||
|
||||
- `GET`, `HEAD`, `OPTIONS` - allowed for any grant on the app.
|
||||
- anything else - allowed only with `access` (`canWrite`). A `read`-only user gets
|
||||
`403 {status: 'FORBIDDEN', message: 'Du hast für diesen Bereich nur Lesezugriff.'}`.
|
||||
|
||||
This is by method on purpose: a write route added later is covered without being marked. It
|
||||
relies on one convention that **must stay true**: admin routes read with GET and change with
|
||||
POST/PUT/PATCH/DELETE. When this was built, every GET handler in tickets, feedback and calendar
|
||||
was checked to call only read functions (no SQL writes, no mail). A GET with a side effect, or a
|
||||
POST that only reads, breaks the read-only role in one direction or the other.
|
||||
|
||||
Consequences worth knowing:
|
||||
|
||||
- "Resend confirmation email" (tickets) is a POST and therefore not available read-only - by
|
||||
decision: it has an effect outside the system.
|
||||
- CSV exports and reports are GETs and therefore available read-only - also by decision.
|
||||
Read-only means all data, including personal data. A restricted variant would be a new role.
|
||||
- `requireAppAccess(app, role)` with an explicit role does **not** apply the method rule: the
|
||||
caller has already named exactly the grant it needs.
|
||||
|
||||
## Telling the frontend
|
||||
|
||||
- `GET /tickets/admin/me` and `GET /feedback/admin/me` return `readOnly: boolean` alongside
|
||||
`email` and `fullName`. Frontends hide write controls when it is `true`; the API refuses the
|
||||
writes regardless.
|
||||
- `GET /admin/me` returns the full `permissions` list; `isReadOnly(permissions, app)` in
|
||||
`admin.schema.ts` is the same rule.
|
||||
|
||||
## Calendar
|
||||
|
||||
The API side is done: calendar writes sit behind `requireAppAccess('calendar')` and so refuse a
|
||||
`read`-only user, and the optional-cookie reads (`/:calendar/json`, `/ical`) check
|
||||
`access.apps.includes('calendar')`, which a `read` grant satisfies - read-only users see the
|
||||
restricted calendars.
|
||||
|
||||
The current Angular frontend (`Calendar_Frontend`) was deliberately **not** changed, because it
|
||||
is being rewritten in Next.js. Until then a read-only calendar user sees the edit buttons and
|
||||
gets the 403 above when using them. Contract for the rewrite: there is no `/calendar/.../me`
|
||||
with `readOnly`; read `GET /admin/me` and compute `isReadOnly(permissions, 'calendar')` - or add
|
||||
a calendar `/me` mirroring tickets/feedback.
|
||||
|
||||
## Adding a finer role later
|
||||
|
||||
`canWrite` checks for `access` specifically, so a new role (e.g. `tickets:redeem`) grants no
|
||||
writes through the method rule. Watch out: tickets and feedback apply the role-less
|
||||
`requireAdminAuth` once at the top of their admin router, so that gate would already refuse the
|
||||
new role's POST before any route-level `requireAppAccess(app, 'redeem')` runs. A finer role
|
||||
therefore needs `canWrite` (or the top-level gate) taught about it, not just a route-level check.
|
||||
Reference in new issue
Block a user