Zum Inhalt springen

kern/09c — Multi-Rolle + Rollen-Switch

In Planung

Status: Draft (2026-05-05) — Anstoss vom Gruender. Ergaenzt 09-auth-self-service.md §5.4 (Rollen) und 09b-auth-me-extended.md (/v1/auth/me).

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.
  1. Ein User darf eine zusaetzliche Rolle zur primaeren Rolle haben (V1: max. 2 Rollen pro User; Tabelle ist vorwaerts-kompatibel auf n).
  2. 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.
  3. In der App ist die Admin-Rolle niemals aktiv. Wenn die primaere Rolle admin ist 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.

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 in user_roles gespiegelt — Backfill in derselben Migration.
  • Ab V1: V1-Code liest beide; das Frontend/Middleware nutzt effective_roles = users.role ∪ user_roles.role.
  • V2 (Roadmap): users.role wird zur Bequemlichkeitsspalte (= Default- Rolle); die echte Wahrheit liegt dann nur noch in user_roles.
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_roles gesetzt (= users.role).
  • Web-PATCH /v1/auth/me/active-role schaltet um; muss in effective_roles enthalten sein, sonst 422 ROLE-NOT-GRANTED.
  • Mobile-Client darf nicht auf admin schalten — siehe §5.
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.granted
  • admin.kern.auth.user.additional_role.revoked
  • auth.session.active_role.switched

Validierung:

  • :role muss in userRoleValues sein.
  • Die primaere Rolle (users.role) wird in user_roles immer mitgefuehrt — sie kann nicht via DELETE entfernt werden (425 PRIMARY-ROLE-IMMUTABLE oder 422; Codewahl im Detail-Refinement).

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.

  • Auf Login wird GET /v1/auth/me/roles aufgerufen. Wenn activeRole == admin, ruft die App automatisch PATCH /v1/auth/me/active-role mit der ersten Nicht-Admin-Rolle aus additionalRoles auf.
  • 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 activeRole aus dem Auth-Provider; alle RBAC-Gates basieren darauf, nicht mehr auf user.role.
  • Sichtbar nur wenn effective_roles.length > 1.
  • Dropdown rechts neben dem User-Avatar mit den verfuegbaren Rollen.
  • Auswahl triggert PATCH /v1/auth/me/active-role und 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.
  • In der bestehenden User-Verwaltung pro User-Karte ein neuer Block „Zusaetzliche Rollen“. Buttons „+ Rolle hinzufuegen“ / „— Rolle entfernen“. Liste der bereits vergebenen Sekundaerrollen.
  • Migration 0158_kern_09_user_roles.sql legt die Tabelle an + befuellt sie aus users.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.sql fuegt sessions.active_role hinzu (ADD COLUMN IF NOT EXISTS) und backfillt fuer aktive Sessions auf users.role.
  • Bestehende requireRole(c, ...allowed)-Aufrufe in Routes bleiben unveraendert — die Middleware liest ab V1 session.active_role statt session.role. session.role bleibt als legacy-alias parallel bestehen.
  • Bestehende Tests, die x-test-role: admin setzen, bleiben gruen — die Test-Harness setzt active_role auf den gleichen Wert.
  • Vergabe Sekundaerrolle: nur admin.
  • Aktivieren/Deaktivieren der eigenen aktiven Rolle: jeder eingeloggte User (requireSession).
  • Vergabe der Rolle admin als Sekundaerrolle ist nur moeglich, wenn der ausfuehrende Admin selbst nicht der Ziel-User ist (kein Self-Promotion-Loop) — Audit-Event traegt das ausdruecklich.
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
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
  • Migration 0158 + 0159 angewandt + RLS gruen
  • users + user_roles + sessions.active_role typecheck-clean
  • requireRole liest effective_roles
  • GET /v1/auth/me/roles + PATCH /v1/auth/me/active-role live
  • Admin-Endpoints additional-roles POST/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)