kern/09c — Multi-Rolle + Rollen-Switch
In Planung
kern/09c — Multi-Rolle + Rollen-Switch
Abschnitt betitelt „kern/09c — Multi-Rolle + Rollen-Switch“Status: Draft (2026-05-05) — Anstoss vom Gruender. Ergaenzt
09-auth-self-service.md§5.4 (Rollen) und09b-auth-me-extended.md(/v1/auth/me).
1. Motivation
Abschnitt betitelt „1. Motivation“Heute hat jeder User genau eine Rolle (users.role). In der Praxis ist
das aber zu eng:
- Der Admin im 5-Mann-Betrieb arbeitet selbst auf der Baustelle und braucht parallel die Mitarbeiter-Sicht (Stempeluhr, eigene Stunden, eigene Spesen).
- Ein Bauleiter ist gleichzeitig auch Manager fuer sein Team.
- Wir wollen nicht zwei Logins / zwei E-Mails pro Person.
2. Ziel V1
Abschnitt betitelt „2. Ziel V1“- Ein User darf eine zusaetzliche Rolle zur primaeren Rolle haben (V1: max. 2 Rollen pro User; Tabelle ist vorwaerts-kompatibel auf n).
- Im Web kann der User in der Topbar zwischen seinen Rollen umschalten.
Die aktive Rolle steckt in der Session (
active_role). RLS / RBAC verwendet ab dann die aktive Rolle, nicht mehr die primaere. - In der App ist die Admin-Rolle niemals aktiv. Wenn die primaere
Rolle
administ und es eine zweite gibt, wird in der App die zweite verwendet. Hat ein Admin keine Zweitrolle, sieht er in der App nichts ausser einem Hinweis „App-Nutzung erfordert eine Mitarbeiter-Rolle — bitte im Web → Profil eine zweite Rolle aktivieren.“
Default: keine Zweitrolle. Bestehende User bleiben unveraendert.
3. Datenmodell
Abschnitt betitelt „3. Datenmodell“3.1 Neue Tabelle user_roles
Abschnitt betitelt „3.1 Neue Tabelle user_roles“Multi-Tenant-Invariante (ADR-0001): tenant_id NOT NULL + uniformes RLS-
Predikat.
CREATE TABLE user_roles ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, role text NOT NULL CHECK (role IN ('admin','manager','bauleiter', 'buchhaltung','monteur','azubi')), granted_at timestamptz NOT NULL DEFAULT now(), granted_by uuid REFERENCES users(id) ON DELETE SET NULL, PRIMARY KEY (user_id, role));- Eine Zeile = der User darf diese Rolle annehmen.
- Die primaere Rolle (
users.role) wird beim Migrieren als erste Zeile inuser_rolesgespiegelt — Backfill in derselben Migration. - Ab V1: V1-Code liest beide; das Frontend/Middleware nutzt
effective_roles = users.role ∪ user_roles.role. - V2 (Roadmap):
users.rolewird zur Bequemlichkeitsspalte (= Default- Rolle); die echte Wahrheit liegt dann nur noch inuser_roles.
3.2 Sessions: active_role
Abschnitt betitelt „3.2 Sessions: active_role“ALTER TABLE sessions ADD COLUMN active_role text CHECK (active_role IN ('admin','manager','bauleiter','buchhaltung','monteur','azubi'));- Beim Login auf den ersten Wert aus
effective_rolesgesetzt (=users.role). - Web-PATCH
/v1/auth/me/active-roleschaltet um; muss ineffective_rolesenthalten sein, sonst 422 ROLE-NOT-GRANTED. - Mobile-Client darf nicht auf
adminschalten — siehe §5.
4. Endpoints
Abschnitt betitelt „4. Endpoints“4.1 Self-Service — User schaltet selbst um
Abschnitt betitelt „4.1 Self-Service — User schaltet selbst um“| Method | Path | Auth | Body |
|---|---|---|---|
| GET | /v1/auth/me/roles |
session | — |
| PATCH | /v1/auth/me/active-role |
session + Idempotency-Key | { role: 'manager' } |
GET /v1/auth/me/roles → { primaryRole, additionalRoles[], activeRole, allowedToSwitchTo[] }.
allowedToSwitchTo ist auf der Server-Seite gefiltert nach Client-Kind:
- Web/Desktop: alle effective_roles
- Mobile: alle ausser
admin
4.2 Admin — Sekundaerrolle vergeben/zurueckziehen
Abschnitt betitelt „4.2 Admin — Sekundaerrolle vergeben/zurueckziehen“| Method | Path | Auth | Body |
|---|---|---|---|
| POST | /v1/admin/kern/auth/users/:id/additional-roles |
admin + Idempotency-Key | { role: 'monteur' } |
| DELETE | /v1/admin/kern/auth/users/:id/additional-roles/:role |
admin + Idempotency-Key | — |
Audit-Events:
admin.kern.auth.user.additional_role.grantedadmin.kern.auth.user.additional_role.revokedauth.session.active_role.switched
Validierung:
:rolemuss inuserRoleValuessein.- Die primaere Rolle (
users.role) wird inuser_rolesimmer mitgefuehrt — sie kann nicht via DELETE entfernt werden (425 PRIMARY-ROLE-IMMUTABLE oder 422; Codewahl im Detail-Refinement).
4.3 Client-Kind-Erkennung
Abschnitt betitelt „4.3 Client-Kind-Erkennung“Header x-client-kind: web|mobile|desktop. Mobile-Apps senden mobile
zwingend (Flutter setzt es im Dio-Interceptor). Default = web. Server-
seitige Whitelist; alles andere → web.
5. App-Verhalten (Flutter)
Abschnitt betitelt „5. App-Verhalten (Flutter)“- Auf Login wird
GET /v1/auth/me/rolesaufgerufen. WennactiveRole == admin, ruft die App automatischPATCH /v1/auth/me/active-rolemit der ersten Nicht-Admin-Rolle ausadditionalRolesauf. - Hat der User keine Nicht-Admin-Rolle, zeigt die App den Block-Screen „Diese App ist fuer Mitarbeiter — bitte im Web eine Mitarbeiter-Rolle aktivieren oder hinzufuegen lassen.“
- Der App-State (Riverpod) liest
activeRoleaus dem Auth-Provider; alle RBAC-Gates basieren darauf, nicht mehr aufuser.role.
6. Web-UI
Abschnitt betitelt „6. Web-UI“6.1 Topbar — Role-Picker
Abschnitt betitelt „6.1 Topbar — Role-Picker“- Sichtbar nur wenn
effective_roles.length > 1. - Dropdown rechts neben dem User-Avatar mit den verfuegbaren Rollen.
- Auswahl triggert
PATCH /v1/auth/me/active-roleund reload der Seite (oder Soft-Reload des SWR-Caches; in V1: harter Reload reicht). - Optisches Tag in der Topbar: „Aktive Rolle: Manager“ — damit der User jederzeit weiss, in welcher Rolle er gerade unterwegs ist.
6.2 Admin-UI — User-Detail
Abschnitt betitelt „6.2 Admin-UI — User-Detail“- In der bestehenden User-Verwaltung pro User-Karte ein neuer Block „Zusaetzliche Rollen“. Buttons „+ Rolle hinzufuegen“ / „— Rolle entfernen“. Liste der bereits vergebenen Sekundaerrollen.
7. Migration & Backwards-Kompatibilitaet
Abschnitt betitelt „7. Migration & Backwards-Kompatibilitaet“- Migration
0158_kern_09_user_roles.sqllegt die Tabelle an + befuellt sie aususers.role(Backfill:INSERT INTO user_roles (user_id, tenant_id, role) SELECT id, tenant_id, role FROM users ON CONFLICT DO NOTHING;). - Migration
0159_kern_09_sessions_active_role.sqlfuegtsessions.active_rolehinzu (ADD COLUMN IF NOT EXISTS) und backfillt fuer aktive Sessions aufusers.role. - Bestehende
requireRole(c, ...allowed)-Aufrufe in Routes bleiben unveraendert — die Middleware liest ab V1session.active_rolestattsession.role.session.rolebleibt als legacy-alias parallel bestehen. - Bestehende Tests, die
x-test-role: adminsetzen, bleiben gruen — die Test-Harness setztactive_roleauf den gleichen Wert.
8. RBAC-Matrix V1
Abschnitt betitelt „8. RBAC-Matrix V1“- Vergabe Sekundaerrolle: nur
admin. - Aktivieren/Deaktivieren der eigenen aktiven Rolle: jeder eingeloggte
User (
requireSession). - Vergabe der Rolle
adminals Sekundaerrolle ist nur moeglich, wenn der ausfuehrende Admin selbst nicht der Ziel-User ist (kein Self-Promotion-Loop) — Audit-Event traegt das ausdruecklich.
9. Roadmap-Schichtung
Abschnitt betitelt „9. Roadmap-Schichtung“| Anforderung-ID | V1 | V1.5 | V2 |
|---|---|---|---|
| F-MR-01 user_roles-Tabelle + Backfill | ✅ | ||
| F-MR-02 effective_roles in Middleware | ✅ | ||
F-MR-03 Self-Service /v1/auth/me/active-role |
✅ | ||
F-MR-04 Admin-Endpoints additional-roles |
✅ | ||
| F-MR-05 Web-Topbar Role-Picker | ✅ | ||
| F-MR-06 App clamp to non-admin | ✅ | ||
| F-MR-07 User-Detail-Admin-UI fuer Sekundaerrollen | ✅ | ||
| F-MR-08 >2 Rollen pro User | ✅ | ||
| F-MR-09 Auto-Switch bei Tenant-Kontext-Wechsel | ✅ |
10. Risiken
Abschnitt betitelt „10. Risiken“| Risiko | W | I | Massnahme |
|---|---|---|---|
Bestehende Routes verlassen sich auf session.role direkt statt requireRole |
mittel | mittel | grep-Audit aller session.role-Lese-Stellen vor Migration; Fallback-Alias bleibt fuer 1 Release |
Mobile-App gibt x-client-kind nicht mit (alte Versionen) |
hoch | niedrig | Server defaultet auf web → Admin-Switch im Mobile bleibt moeglich. Akzeptiert fuer V1 — die App-Versionen koennen gezwungen geupdated werden via min-version-gate |
| Audit-Volumen explodiert wenn User staendig switchen | niedrig | niedrig | Rate-Limit 1 Switch / 5 s pro User |
11. Definition of Done
Abschnitt betitelt „11. Definition of Done“- Migration 0158 + 0159 angewandt + RLS gruen
-
users+user_roles+sessions.active_roletypecheck-clean -
requireRoleliesteffective_roles -
GET /v1/auth/me/roles+PATCH /v1/auth/me/active-rolelive - Admin-Endpoints
additional-rolesPOST/DELETE live - Web-Topbar Role-Picker live
- Flutter-App clamped auf non-admin
- Tests: API CRUD, RBAC (manager kann nicht vergeben), RLS Cross-Tenant, Mobile-Switch zu admin → 422
- Audit-Events landen in audit_events
- Doku in
/admin/sicherheit(oder bestehender Auth-Doku)