wiki/c-work/4-done/2026-04-sysadmin-authority-split.md
2026-06-02 09:42:12 +02:00

434 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: done -->
<!-- started: 2026-04-17 -->
<!-- finished: 2026-04-17 -->
<!-- component: gateway | ui-nyla | platform -->
# SysAdmin-Authority-Modell konsolidieren: Flag + Rolle → zwei klar getrennte Flags
## Beschreibung und Kontext
Das PORTA-Berechtigungsmodell kennt heute **zwei parallel gepflegte
SysAdmin-Marker**, die sich überschneiden:
1. **`User.isSysAdmin` FLAG** (Spalte in `User`-Tabelle)
- Wirkt als **RBAC-Engine-Bypass** in `rbac.py:getUserPermissions` (Zeile 88)
→ umgeht alle AccessRules und gibt vollen Zugriff.
- Geprüft via `ctx.isSysAdmin`, `requireSysAdmin`, direktes
`getattr(user, 'isSysAdmin', False)`.
- Verwendet in Routen für System-Operationen (Tokens, Logs, DB-Health,
i18n-Master, Teams-SystemBot, global-scoped Files/Sources).
2. **`sysadmin` ROLE** (UserMandateRole im Root-Mandant)
- Gedacht als normale Rolle im Root-Mandant.
- Gibt implizit **mandanten-übergreifende** Admin-Rechte, weil die zugehörigen
AccessRules auf User-/Mandate-/RBAC-Verwaltung eingestellt sind.
- Geprüft via `ctx.hasSysAdminRole`, `_hasSysAdminRole(userId)`,
`requireSysAdminRole`.
- Verwendet in Routen für cross-mandate Admin-Operationen
(User-Management, Mandate-Management, RBAC-Rules, i18n, Subscription).
### Warum der aktuelle Zustand problematisch ist
1. **Architektonisch inkonsistent:** Alle anderen Rollen sind **mandanten-scoped**.
Nur `sysadmin` ist eine Rolle im Root-Mandant, die **Autorität über andere
Mandanten** verleiht. Das durchbricht die Mandanten-Isolation konzeptionell.
2. **Doppelte Quelle der Wahrheit:** Flag und Rolle können auseinander driften.
Siehe konkreter Security-Bug (Issue 4): Admin entfernt Flag über UI, Rolle
bleibt → User behält alle Cross-Mandate-Rechte.
3. **Verwirrende Semantik:** „SysAdmin" kann zwei unterschiedliche Dinge
bedeuten — je nachdem, wer fragt.
4. **Fehlende Differenzierung:** Es ist heute nicht möglich, jemanden
auszustatten mit
- „darf die Plattform betreiben" (Logs, Tokens, DB-Health)
aber nicht mit
- „darf über alle Mandanten User und Rollen verwalten".
Oder umgekehrt. Beides hängt am selben Flag bzw. an derselben Rolle.
**Risiko bei Nicht-Umsetzung:**
- **SECURITY:** Ex-SysAdmin mit entferntem Flag behält Cross-Mandate-Admin-Rechte
(aktiver Bug).
- Aufbau technischer Schuld: jede neue Admin-Route muss zwischen Flag und
Rolle wählen — Entwickler wählen falsch.
- Audit/Compliance: schwer nachzuweisen, wer welche Autorität warum hat.
## Fokus und kritische Details
- Drei Kategorien von Autorität sind heute konzeptionell vermischt:
- **Infrastruktur-Autorität** (Kategorie A): Logs, Tokens, DB-Health,
Registry-Sync, i18n-Master, Billing-Webhooks, Teams-SystemBot.
- **Plattform-Governance-Autorität** (Kategorie BE): Cross-Mandate
User-Mgmt, Mandate-Mgmt, RBAC-Catalog, Feature-Registry,
User-Access-Overview, Mandanten-Deblockierung.
- **Mandanten-Autorität**: alles innerhalb eines spezifischen Mandanten
(inkl. Root-Mandant) — hier sind Rollen das richtige Konzept.
- Die `sysadmin`-Rolle existiert heute nur im Root-Mandant
(`interfaceBootstrap.py:_initSysAdminRole`, `isSystemRole=False`).
Ihre AccessRules regeln aber Autorität über **alle** Mandanten.
- Die RBAC-Engine hat für `isSysAdmin=True` einen **harten Bypass**
(`rbac.py:88`). Das ist kein Missbrauch, sondern eine **bewusste Design-
Entscheidung** — so funktionieren „system-ops" unabhängig vom RBAC-Schema.
- Bestehende DB-Stände können inkonsistent sein → Einmalige Migration nötig.
## Ziel und Nicht-Ziele
**Ziel (Empfehlung, siehe Entscheidungen unten)**
- Zwei klar getrennte, **einzeln zuteilbare Berechtigungs-Dimensionen**:
- `User.isSysAdmin` (umbenannt von Bedeutung her → **„Infrastructure/System
Operator"**): RBAC-Bypass für Infrastruktur-Operationen, **keine** implizite
Autorität über Mandanten-Daten oder -Verwaltung.
- `User.isPlatformAdmin` (**neu**): Cross-Mandate-Governance ohne RBAC-
Bypass. Prüfung in den jeweiligen Admin-Routen explizit.
- **Eliminierung der `sysadmin`-Rolle** aus dem Root-Mandant.
Die Autorität, die bisher an der Rolle hing, wird an `isPlatformAdmin`
gebunden.
- Beide Flags einzeln vergebbar, einzeln sichtbar im UI, einzeln auditierbar.
- Beispiele für eine klare Trennung:
- „Operations-Engineer" → `isSysAdmin=true`, `isPlatformAdmin=false`
(kann Logs und DB prüfen, aber keine User/Mandate verwalten).
- „Customer-Success-Admin" → `isSysAdmin=false`, `isPlatformAdmin=true`
(kann Mandanten deblockieren, User-Access-Overview sehen, aber nicht in
Server-Logs schauen).
- „Plattform-Super-Admin" → beide auf `true`.
**Explizit NICHT**
- Einführung einer neuen Rollen-Tabelle (`PlatformRole`) — Overkill für zwei
heute bekannte Autoritäts-Achsen; bei Bedarf später zusätzliche Flags.
- Änderung der Mandanten-scoped Rollen (admin/user/viewer etc.)
- Änderung der RBAC-Engine-Semantik — `isSysAdmin`-Bypass bleibt; er
beschränkt sich jetzt aber **eindeutig auf Infrastruktur-Zwecke**.
- Wiederverwendung des Begriffs „sysadmin" in der Namensgebung für Cross-
Mandate-Governance (explizit vermeiden — eindeutige Namen, keine
Ambiguität).
## Architektur-Analyse: verworfene Alternativen
### Alternative A (verworfen): Rolle behalten, Flag eliminieren
- Nur `sysadmin`-Rolle, Flag weg.
- **Verworfen**, weil:
- Rolle bleibt konzeptionell im Mandanten-Kontext gefangen.
- RBAC-Bypass für Infrastruktur-Operationen wäre schwerer umzusetzen
(müsste an Rolle hängen → Root-Mandant-Query bei jedem Request).
- Infrastruktur-Routen benötigen oft **keinen** Mandanten-Kontext.
### Alternative B (verworfen): Flag behalten, Rolle eliminieren, eine Flag-Dimension
- Nur `isSysAdmin` flag, Rolle weg. **Eine einzige** Dimension.
- **Verworfen**, weil:
- Keine Trennung „pure Infrastruktur" vs. „Cross-Mandate-Governance".
- Wer Logs lesen darf, kann automatisch auch Mandanten deblockieren —
das ist zu grob und macht Auditierbarkeit schwierig.
- Vom User explizit gewünschte Differenzierung geht verloren („sysadmin
flag sauber = keine Daten-Berechtigung, nur Admin-Zwecke").
### Alternative C (verworfen): Separate `PlatformRole`-Tabelle
- Neues Schema `UserPlatformRole` mit Enum `SYSTEM_OPERATOR` +
`PLATFORM_ADMIN` (+ Zukunft).
- **Verworfen für Phase 1**, weil:
- Zwei Autoritäts-Achsen heute bekannt → zwei Flags sind die einfachste,
ausreichende Lösung.
- Einführung einer neuen Tabelle = Migration + zusätzliche Query-Overhead.
- Kann **später** eingeführt werden, wenn eine dritte Achse entsteht —
die beiden Flags migrieren dann trivial in die neue Struktur.
### Alternative D (EMPFOHLEN): Zwei orthogonale Flags, Rolle eliminiert
- `isSysAdmin` behält Bedeutung + Bypass, wird aber semantisch auf
Infrastruktur eingeschränkt.
- `isPlatformAdmin` neu für Cross-Mandate-Governance.
- `sysadmin`-Rolle wird aus Root-Mandant + AccessRules entfernt.
- **Vorteile:**
- Eine Autorität = genau ein Marker (keine Sync-Pflicht).
- Einzeln vergebbar → feinere Governance.
- Keine Rollen mehr, die Mandanten-Grenzen durchbrechen.
- RBAC-Engine-Logik wird einfacher (kein root-mandate-role-lookup mehr).
- **Nachteile:**
- Migration nötig.
- Alle `ctx.hasSysAdminRole` / `requireSysAdminRole` Callsites müssen
umgeschrieben werden.
**Entscheidung: Alternative D.**
## Zielarchitektur im Detail
### Autoritäts-Matrix (nach Umsetzung)
| Autorität | Scope | Marker | Prüfung in Route |
| -------------------------------------- | ------------------------------ | ------------------------------------------- | --------------------------------------------- |
| Logs, Tokens, DB-Health, i18n-Master | global (ohne Mandant) | `isSysAdmin=true` | `requireSysAdmin` |
| RBAC-Engine-Bypass | global (Datenzugriff) | `isSysAdmin=true` | `rbac.py:getUserPermissions` (unverändert) |
| User-Mgmt cross-mandate | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` (neu) |
| Mandate-Mgmt (create/edit/block) | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` |
| RBAC-Rules-Catalog | cross-mandate (Templates) | `isPlatformAdmin=true` | `requirePlatformAdmin` |
| Feature-Registry (Admin-Features-UI) | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` |
| User-Access-Overview | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` |
| Billing-Admin-Overview (alle Mandate) | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` |
| Billing eines Mandanten | 1 Mandant | Mandanten-Rolle `admin` | `requireMandateAdmin(mandateId)` (bestehend) |
| User-Verwaltung innerhalb Mandant | 1 Mandant | Mandanten-Rolle `admin` | `requireMandateAdmin(mandateId)` |
| Feature-Instance-Verwaltung | 1 Mandant | Mandanten-Rolle `admin` | `requireMandateAdmin(mandateId)` |
### Neue Auth-Helpers (in `modules/auth/authentication.py`)
```python
@property
def isPlatformAdmin(self) -> bool:
return getattr(self.user, 'isPlatformAdmin', False)
def requirePlatformAdmin(currentUser: User = Depends(getCurrentUser)) -> User:
if not getattr(currentUser, 'isPlatformAdmin', False):
raise HTTPException(403, "Platform admin privileges required")
audit_logger.logSecurityEvent(userId=str(currentUser.id),
mandateId="system", action="platform_admin_action",
details="Cross-mandate governance operation")
return currentUser
```
`requireSysAdmin` (bestehend) wird **enger definiert** — nur noch für
Infrastruktur-Routen.
### Migration (einmalig, beim Boot, idempotent)
```
for user in alle User:
hadSysadminRole = _hasSysAdminRole_legacy(user.id)
# Alte Rolle impliziert sowohl Cross-Mandate-Admin als auch (historisch)
# impliziten Infrastruktur-Zugriff. Setze neuen Flag konservativ.
if hadSysadminRole and not user.isPlatformAdmin:
user.isPlatformAdmin = True
log.warning(f"Migration: granted isPlatformAdmin to {user.id}")
# Flag bleibt wie es ist (unabhängig).
after:
drop AccessRules WHERE roleId = sysadmin_root_role
drop UserMandateRole WHERE roleId = sysadmin_root_role
drop Role WHERE id = sysadmin_root_role
```
## Referenzen (anzupassende Stellen)
### Backend — Datamodel
| Datei | Was | Änderung |
| --------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
| `platform-core/modules/datamodels/datamodelUam.py` | `User` Pydantic + `UserInDB` | Neues Feld `isPlatformAdmin: bool = False` + Validator |
| `platform-core/modules/datamodels/datamodelUam.py` | Docstring `isSysAdmin` | Neue Semantik („Infrastructure/System Operator") |
### Backend — Auth
| Datei | Was | Änderung |
| --------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------- |
| `platform-core/modules/auth/authentication.py` | `_hasSysAdminRole`, `requireSysAdminRole`, `_getRootMandateRoleIds` | **Löschen** (bzw. als deprecated Stub behalten für transitional Phase) |
| `platform-core/modules/auth/authentication.py` | `RequestContext.hasSysAdminRole` | **Löschen** |
| `platform-core/modules/auth/authentication.py` | `RequestContext.isPlatformAdmin` (neu) | Property |
| `platform-core/modules/auth/authentication.py` | `requirePlatformAdmin` (neu) | Dependency |
| `platform-core/modules/auth/__init__.py` | Exporte | `requirePlatformAdmin` exportieren |
### Backend — Bootstrap
| Datei | Was | Änderung |
| ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------ |
| `platform-core/modules/interfaces/interfaceBootstrap.py` | `_initSysAdminRole` + `_createSysAdminAccessRules` + `_ensureSysAdminAccessRules` | **Löschen** (inkl. aller damit verbundener AccessRules) |
| `platform-core/modules/interfaces/interfaceBootstrap.py` | `_ensureAdminUser`, `_ensureEventUser` | Setze `isPlatformAdmin=True` zusätzlich zu `isSysAdmin=True` |
| `platform-core/modules/interfaces/interfaceBootstrap.py` | Neu: `_migrateSysAdminRoleToPlatformAdminFlag()` | Einmalige Migration beim Boot; idempotent |
### Backend — Routen (alle Callsites von `hasSysAdminRole` / `requireSysAdminRole`)
| Datei | Callsites | Änderung |
| --------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `platform-core/modules/routes/routeDataUsers.py` | Z.106, 159, 236, 325, 445, 517, 571, 586, 919 | `hasSysAdminRole``isPlatformAdmin` |
| `platform-core/modules/routes/routeDataUsers.py` | `_syncSysAdminRole` (aus Sync-Phase 1) | **Löschen** (nicht mehr nötig) |
| `platform-core/modules/routes/routeDataUsers.py` | `update_user`: Flag-Sync-Logik | Vereinfachen (kein Rolle-Sync mehr) |
| `platform-core/modules/routes/routeDataMandates.py` | Z.104, 226, 258, 380, 437, 510, 1054 | `hasSysAdminRole`/`requireSysAdminRole` → `isPlatformAdmin`/`requirePlatformAdmin` |
| `platform-core/modules/routes/routeDataMandates.py` | Reverse-Sync (Z.916944 aus Sync-Phase 1) | **Löschen** |
| `platform-core/modules/routes/routeAdminRbacRules.py` | 11 Stellen (Z.245, 365, 490, 537, 588, 668, 756, 839, 1020, 1079, 1140, 1204, 1360) | `hasSysAdminRole`/`requireSysAdminRole` → `isPlatformAdmin`/`requirePlatformAdmin` |
| `platform-core/modules/routes/routeAdminFeatures.py` | 22 Stellen | analog |
| `platform-core/modules/routes/routeAdminUserAccessOverview.py` | Z.78, 126, 220 | analog |
| `platform-core/modules/routes/routeAdminDatabaseHealth.py` | Z.44, 56, 68, 93 | bleibt `requireSysAdmin` (Infrastruktur) |
| `platform-core/modules/routes/routeAdminLogs.py` | Z.66, 107 | bleibt `requireSysAdmin` (Infrastruktur) |
| `platform-core/modules/routes/routeAdminDemoConfig.py` | Z.28, 44, 69 | `isPlatformAdmin` (Data-Change-Operation) |
| `platform-core/modules/routes/routeBilling.py` | Z.89, 145, 737, 1464, 1487 | Cross-Mandate-Views → `isPlatformAdmin`; Mandate-eigene → unverändert |
| `platform-core/modules/routes/routeI18n.py` | Z.829, 847, 860, 876, 914, 942 | i18n-Master/System → `requireSysAdmin`; Übersetzungs-Management → `isPlatformAdmin`|
| `platform-core/modules/routes/routeSubscription.py` | Z.49, 306, 488 | `isPlatformAdmin` |
| `platform-core/modules/routes/routeInvitations.py` | Z.189, 894 | `isPlatformAdmin` |
| `platform-core/modules/routes/routeSystem.py` | Z.481, 299, 306, 360, 370, 375, 385, 398, 408 | `isPlatformAdmin` für Navigations-Sichtbarkeit |
| `platform-core/modules/routes/routeAudit.py` | Z.134 | `isPlatformAdmin` (Cross-Mandate-Audit) |
| `platform-core/modules/routes/routeNotifications.py` | 518 | unverändert (`addRoleToUserMandate` betrifft keine sysadmin mehr) |
| `platform-core/modules/routes/routeDataFiles.py` | Z.11, 548, 850, 1044 | Global-Scope → `isSysAdmin` bleibt (Infra-Daten) |
| `platform-core/modules/routes/routeDataSources.py` | Z.10, 56 | analog |
| `platform-core/modules/routes/routeWorkflowDashboard.py` | 9 Stellen | Cross-Mandate-Übersicht → `isPlatformAdmin` |
| `platform-core/modules/features/trustee/routeFeatureTrustee.py` | Z.107, 141, 159, 161, 1814 | prüfen pro Callsite (meist `isPlatformAdmin`) |
| `platform-core/modules/features/teamsbot/routeFeatureTeamsbot.py` | 8 Stellen | System-Bot-Registrierung → `isSysAdmin` bleibt; User-Mgmt → `isPlatformAdmin` |
| `platform-core/modules/features/chatbot/routeFeatureChatbot.py` | Z.105 | `isPlatformAdmin` |
| `platform-core/modules/features/realEstate/routeFeatureRealEstate.py` | Z.119 | `isPlatformAdmin` |
| `platform-core/modules/routes/routeRealEstate.py` | Z.124 | `isPlatformAdmin` |
| `platform-core/modules/features/workspace/...` | (suchen) | pro Callsite prüfen |
| `platform-core/modules/interfaces/interfaceDbManagement.py` | Z.637643 `_isSysAdmin` | Bleibt an Flag gebunden (RBAC-Bypass in Data-Management) |
### Backend — Services / Demo / Tests
| Datei | Was | Änderung |
| -------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------------- |
| `platform-core/modules/demoConfigs/investorDemo2026.py` | Z.188, 222 | `isPlatformAdmin=True` statt Rolle; `isSysAdmin` wie gehabt |
| `platform-core/modules/serviceCenter/services/serviceAgent/...` | Z.568 | Liest Flag — kein Change |
| `platform-core/tests/**` | alle `hasSysAdminRole`/`requireSysAdminRole` Mentions | Tests anpassen |
| `platform-core/tests/integration/rbac/test_sysadmin_sync.py` (geplant Phase 1) | **entfällt** bzw. wird zu `test_platform_admin_flag.py` | |
### Frontend
| Datei | Was | Änderung |
| --------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `ui-nyla/src/types/mandate.ts` | User-Typ Z.143 | `isPlatformAdmin: boolean` ergänzen |
| `ui-nyla/src/api/userApi.ts` / `authApi.ts` | User-Schema | `isPlatformAdmin?: boolean` ergänzen |
| `ui-nyla/src/utils/userCache.ts` | User-Cache | `isPlatformAdmin` ergänzen |
| `ui-nyla/src/pages/admin/AdminUsersPage.tsx` | Edit-Formular | Zwei separate Toggles: "Systemadmin" und "Plattformadmin"; Tooltip mit Erklärung |
| `ui-nyla/src/pages/admin/AdminUserAccessOverviewPage.tsx` | User-Grid, Detail-Ansicht | Beide Flags anzeigen; Spalten + Filter |
| `ui-nyla/src/pages/billing/BillingAdmin.tsx` | Z.449, 505, 508, 669 | `isSysAdmin``isPlatformAdmin` für Cross-Mandate-View |
| `ui-nyla/src/pages/views/teamsbot/*.tsx` | Z.23, 24, 446, 329 | prüfen; meist `isPlatformAdmin` für Admin-UI, `isSysAdmin` für SystemBot-Config |
| `ui-nyla/src/hooks/useUsers.ts` | Z.35, 39, 74, 88, 92, 218, 222 | Beide Flags mitgeben |
## Entscheidungen
| Datum | Entscheidung | Begründung |
| ---------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| 2026-04-17 | `sysadmin`-Rolle im Root-Mandant wird eliminiert | Eine Rolle im Mandanten-Kontext, die Autorität über andere Mandanten gibt, ist architektonisch falsch |
| 2026-04-17 | Zwei orthogonale Flags: `isSysAdmin` (Infrastruktur) + `isPlatformAdmin` (Cross-Mandate-Governance) | Deckt die heute bekannten zwei Autoritäts-Achsen sauber ab; einzeln vergebbar, einzeln auditierbar |
| 2026-04-17 | Keine neue `PlatformRole`-Tabelle | Zwei Achsen → zwei Flags reichen; Tabelle wäre Over-Engineering; spätere Migration trivial möglich |
| 2026-04-17 | `isSysAdmin`-RBAC-Bypass bleibt erhalten | Bewusste Engine-Design-Entscheidung; Infrastruktur-Ops brauchen Unabhängigkeit vom RBAC-Schema |
| 2026-04-17 | `requirePlatformAdmin` prüft Flag **ohne** RBAC-Bypass | Plattform-Governance ist Admin-UI-Recht, nicht Daten-Bypass-Recht |
| 2026-04-17 | Migration idempotent beim Boot, nicht als separates Migrations-Skript | Konsistent mit bisherigem Bootstrap-Stil; kein Down-Time-Risiko |
| 2026-04-17 | `isPlatformAdmin` gibt **keinen** impliziten Daten-Zugriff auf Mandante | Separation of concerns: Governance ≠ Daten |
| 2026-04-17 | Phase-1-Sync-Code (Flag↔Rolle) wird zurückgerollt | Wird durch finale Lösung obsolet; aktuell committete Helper `_syncSysAdminRole` etc. entfernen |
## Umsetzungs-Checkliste
### Phase A — Datamodel + Auth
- [ ] `User.isPlatformAdmin` Feld in Pydantic + DB-Schema
- [ ] `_normalizePlatformAdmin` Validator
- [ ] `RequestContext.isPlatformAdmin` Property
- [ ] `requirePlatformAdmin` Dependency + Audit
- [ ] Export in `modules/auth/__init__.py`
- [ ] Docstring-Update `isSysAdmin` Field: neue enge Semantik
### Phase B — Migration + Bootstrap
- [ ] `_migrateSysAdminRoleToPlatformAdminFlag()` in `interfaceBootstrap.py`
- [ ] Aufruf nach `_initSysAdminRole` (vor dessen Entfernung)
- [ ] Test: vorhandene sysadmin-Rolle + User mit UserMandateRole → Flag=True
- [ ] `_ensureAdminUser`, `_ensureEventUser`: `isPlatformAdmin=True` setzen
- [ ] `investorDemo2026.py`: analog
### Phase C — Routen umstellen
- [ ] Alle Callsites laut Referenz-Tabelle bearbeiten
- [ ] Nach Umstellung: Code-Such nach `hasSysAdminRole` / `requireSysAdminRole`
→ muss 0 Treffer ergeben (ausser deprecated-Stub)
- [ ] Navigation (`routeSystem.py`): Menü-Sichtbarkeit korrekt anpassen
### Phase D — Phase-1-Sync-Code zurückrollen
- [ ] `routeDataUsers.py:_syncSysAdminRole` entfernen
- [ ] `routeDataUsers.py:update_user` Sync-Aufruf entfernen
- [ ] `routeDataUsers.py:create_user` Sync-Aufruf entfernen
- [ ] `routeDataMandates.py:update_user_roles_in_mandate` Reverse-Sync entfernen
### Phase E — sysadmin-Rolle entfernen
- [ ] `_initSysAdminRole` entfernen
- [ ] `_createSysAdminAccessRules`, `_ensureSysAdminAccessRules` entfernen
- [ ] Migration: sysadmin-Role + AccessRules + UserMandateRole-Einträge löschen
- [ ] `_hasSysAdminRole`, `requireSysAdminRole` entfernen (oder als deprecated
für 1 Release behalten)
### Phase F — Frontend
- [ ] Typen + userCache erweitern
- [ ] `AdminUsersPage`: zwei separate Toggles + Tooltip
- [ ] `AdminUserAccessOverviewPage`: Flag-Spalten + Filter
- [ ] Callsites Frontend (BillingAdmin, Teamsbot-Pages etc.)
### Querschnitt
- [ ] **RBAC / Permissions:** Keine neuen AccessRules. Alte sysadmin-AccessRules gelöscht.
- [ ] **Neutralisierung:** User-Neutralisierung setzt beide Flags auf False.
- [ ] **Navigation / Routing:** Admin-Menü-Sichtbarkeit via `isPlatformAdmin`.
- [ ] **Billing-Impact:** keiner.
- [ ] **Tests:** Unit-Tests für `requirePlatformAdmin`, Integrations-Tests für
Migration.
## Akzeptanzkriterien
| # | Kriterium (Given-When-Then) | Prio |
| -- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| 1 | Given User X mit `isSysAdmin=true`, `isPlatformAdmin=false`, When X ruft `GET /api/admin/mandates` auf, Then 403 | must |
| 2 | Given User X mit `isSysAdmin=false`, `isPlatformAdmin=true`, When X ruft `GET /api/admin/database-health` auf, Then 403 | must |
| 3 | Given User X mit `isPlatformAdmin=true`, When X ruft `GET /api/admin/mandates` auf, Then 200 mit allen Mandanten | must |
| 4 | Given DB-Migration: User Y hatte `sysadmin`-Rolle in Root-Mandant, When Gateway bootet, Then Y hat `isPlatformAdmin=true`; sysadmin-Rolle/AccessRules sind weg | must |
| 5 | Given Admin entzieht User X den `isPlatformAdmin`-Flag, When X im nächsten Request `GET /api/admin/rbac-rules` aufruft, Then 403 (ohne Token-Refresh, nächster Request prüft live DB) | must |
| 6 | Given Admin entzieht User X den `isSysAdmin`-Flag, When X im nächsten Request `GET /api/admin/logs` aufruft, Then 403 | must |
| 7 | Given `Admin > USER`-Formular, When Admin bearbeitet User, Then zwei separate Toggles „Systemadmin" und „Plattformadmin" sind sichtbar + dokumentiert | must |
| 8 | Given User X versucht sich selbst `isSysAdmin` oder `isPlatformAdmin` zu verändern, Then 403 (Self-Protection für beide Flags) | must |
| 9 | Given `_hasSysAdminRole` / `requireSysAdminRole` werden importiert, When Codebase gescannt wird, Then 0 Treffer (oder nur deprecated-Stub mit Logger-Warning) | should |
| 10 | Given `sysadmin`-Rolle existiert noch in DB, When Migration läuft, Then Rolle + zugehörige UserMandateRole + AccessRules werden gelöscht; Flag der betroffenen User ist konsistent gesetzt | must |
| 11 | Given „Operations-Engineer"-User mit `isSysAdmin=true`, `isPlatformAdmin=false`, When er die Admin-UI öffnet, Then sieht er System-Menüpunkte (Logs, DB-Health), aber keine Mandate-/User-Verwaltung | should |
| 12 | Given „Customer-Success-Admin" mit `isSysAdmin=false`, `isPlatformAdmin=true`, When er die Admin-UI öffnet, Then sieht er User-/Mandate-Verwaltung, aber keine Logs | should |
## Testplan
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
| --- | ---- | ------------------------ | ------------- | ------------------------------------------------------------------ | ------- |
| T1 | 1 | api-integration | ja | `platform-core/tests/integration/rbac/test_platform_admin_flag.py` | done |
| T2 | 2 | api-integration | ja | dito | done |
| T3 | 3 | api-integration | ja | dito | done |
| T4 | 4,10 | migration-unit | ja | `platform-core/tests/unit/rbac/test_sysadmin_migration.py` | done |
| T5 | 5,6 | api-integration | ja | `platform-core/tests/integration/rbac/test_platform_admin_flag.py` | done |
| T6 | 7 | frontend (Playwright) | optional | `ui-nyla/e2e/admin-users-flags.spec.ts` (neu) | open |
| T7 | 8 | api-unit | ja | `platform-core/tests/integration/rbac/test_platform_admin_flag.py` | done |
| T8 | 9 | codebase-scan | ja (CI-Check) | `scripts/check_no_sysadmin_role.py` (CI-Gate) | done |
| T9 | 11,12| manuelle UI-Regression | manuell | Siehe Security-Regression-Manual | open |
**Security-Regression-Manual (T9)**
1. User A: `isSysAdmin=true`, `isPlatformAdmin=false`. Login.
- `Admin > Logs` → ok.
- `Admin > DB-Health` → ok.
- `Admin > Mandate` → 403/nicht sichtbar im Menü.
- `Admin > User` → 403/nicht sichtbar.
2. User B: `isSysAdmin=false`, `isPlatformAdmin=true`. Login.
- `Admin > Mandate` → ok.
- `Admin > User` → ok.
- `Admin > User Access Overview` → ok.
- `Admin > Logs` → 403.
- `Admin > DB-Health` → 403.
3. User C: beide `false`. Login.
- Keine Admin-Menüpunkte sichtbar.
4. Flag-Entzug im laufenden Betrieb: Admin entzieht B das `isPlatformAdmin`-Flag.
B's nächster Request auf `Admin > Mandate` → 403 (ohne Re-Login).
5. sysadmin-Rolle-Check: kein User hat mehr eine sysadmin-Rolle in DB
(`SELECT COUNT(*) FROM "Role" WHERE "roleLabel" = 'sysadmin'` = 0).
## Links
- RBAC-Referenz: `wiki/b-reference/platform/rbac.md`
- Auth-Modul: `platform-core/modules/auth/authentication.py`
- Ausgangspunkt Testing PORTA 2026-04-17 (Issue 4: Security-Bug)
## Abschluss
- [x] `wiki/b-reference/platform/rbac.md`: Abschnitt „Platform-Governance-Autoritaet" ergaenzt
- [ ] `wiki/TOPICS.md`: Topic „Authority-Modell: System/Platform/Mandate" anlegen
- [x] Dieses Dokument nach Abschluss → `wiki/c-work/4-done/` verschoben
- [x] CI-Gate `platform-core/scripts/check_no_sysadmin_role.py` (Acceptance T8 / AC#9)
### Umsetzungs-Status
- Phase A (Datamodel + Auth) : done
- Phase B (Migration + Bootstrap) : done
- Phase C (Routen umstellen) : done — 0 Treffer fuer hasSysAdminRole/requireSysAdminRole/_hasSysAdminRole
- Phase D (Phase-1-Sync zurueckgerollt) : done
- Phase E (sysadmin-Rolle entfernt) : done — Stubs entfernt, Migration entfernt Rolle/AccessRules/Memberships
- Phase F (Frontend) : done — Typen + AdminUserAccessOverview + AdminMandates + BillingAdmin + useUsers
- Tests + CI-Gate : done — T1-T5, T7, T8 automatisiert (13 grün); T6 (Playwright) + T9 (manuell) optional/offen