# 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 B–E): 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 | | --------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------ | | `gateway/modules/datamodels/datamodelUam.py` | `User` Pydantic + `UserInDB` | Neues Feld `isPlatformAdmin: bool = False` + Validator | | `gateway/modules/datamodels/datamodelUam.py` | Docstring `isSysAdmin` | Neue Semantik („Infrastructure/System Operator") | ### Backend — Auth | Datei | Was | Änderung | | --------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------- | | `gateway/modules/auth/authentication.py` | `_hasSysAdminRole`, `requireSysAdminRole`, `_getRootMandateRoleIds` | **Löschen** (bzw. als deprecated Stub behalten für transitional Phase) | | `gateway/modules/auth/authentication.py` | `RequestContext.hasSysAdminRole` | **Löschen** | | `gateway/modules/auth/authentication.py` | `RequestContext.isPlatformAdmin` (neu) | Property | | `gateway/modules/auth/authentication.py` | `requirePlatformAdmin` (neu) | Dependency | | `gateway/modules/auth/__init__.py` | Exporte | `requirePlatformAdmin` exportieren | ### Backend — Bootstrap | Datei | Was | Änderung | | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------ | | `gateway/modules/interfaces/interfaceBootstrap.py` | `_initSysAdminRole` + `_createSysAdminAccessRules` + `_ensureSysAdminAccessRules` | **Löschen** (inkl. aller damit verbundener AccessRules) | | `gateway/modules/interfaces/interfaceBootstrap.py` | `_ensureAdminUser`, `_ensureEventUser` | Setze `isPlatformAdmin=True` zusätzlich zu `isSysAdmin=True` | | `gateway/modules/interfaces/interfaceBootstrap.py` | Neu: `_migrateSysAdminRoleToPlatformAdminFlag()` | Einmalige Migration beim Boot; idempotent | ### Backend — Routen (alle Callsites von `hasSysAdminRole` / `requireSysAdminRole`) | Datei | Callsites | Änderung | | --------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | `gateway/modules/routes/routeDataUsers.py` | Z.106, 159, 236, 325, 445, 517, 571, 586, 919 | `hasSysAdminRole` → `isPlatformAdmin` | | `gateway/modules/routes/routeDataUsers.py` | `_syncSysAdminRole` (aus Sync-Phase 1) | **Löschen** (nicht mehr nötig) | | `gateway/modules/routes/routeDataUsers.py` | `update_user`: Flag-Sync-Logik | Vereinfachen (kein Rolle-Sync mehr) | | `gateway/modules/routes/routeDataMandates.py` | Z.104, 226, 258, 380, 437, 510, 1054 | `hasSysAdminRole`/`requireSysAdminRole` → `isPlatformAdmin`/`requirePlatformAdmin` | | `gateway/modules/routes/routeDataMandates.py` | Reverse-Sync (Z.916–944 aus Sync-Phase 1) | **Löschen** | | `gateway/modules/routes/routeAdminRbacRules.py` | 11 Stellen (Z.245, 365, 490, 537, 588, 668, 756, 839, 1020, 1079, 1140, 1204, 1360) | `hasSysAdminRole`/`requireSysAdminRole` → `isPlatformAdmin`/`requirePlatformAdmin` | | `gateway/modules/routes/routeAdminFeatures.py` | 22 Stellen | analog | | `gateway/modules/routes/routeAdminUserAccessOverview.py` | Z.78, 126, 220 | analog | | `gateway/modules/routes/routeAdminDatabaseHealth.py` | Z.44, 56, 68, 93 | bleibt `requireSysAdmin` (Infrastruktur) | | `gateway/modules/routes/routeAdminLogs.py` | Z.66, 107 | bleibt `requireSysAdmin` (Infrastruktur) | | `gateway/modules/routes/routeAdminDemoConfig.py` | Z.28, 44, 69 | `isPlatformAdmin` (Data-Change-Operation) | | `gateway/modules/routes/routeBilling.py` | Z.89, 145, 737, 1464, 1487 | Cross-Mandate-Views → `isPlatformAdmin`; Mandate-eigene → unverändert | | `gateway/modules/routes/routeI18n.py` | Z.829, 847, 860, 876, 914, 942 | i18n-Master/System → `requireSysAdmin`; Übersetzungs-Management → `isPlatformAdmin`| | `gateway/modules/routes/routeSubscription.py` | Z.49, 306, 488 | `isPlatformAdmin` | | `gateway/modules/routes/routeInvitations.py` | Z.189, 894 | `isPlatformAdmin` | | `gateway/modules/routes/routeSystem.py` | Z.481, 299, 306, 360, 370, 375, 385, 398, 408 | `isPlatformAdmin` für Navigations-Sichtbarkeit | | `gateway/modules/routes/routeAudit.py` | Z.134 | `isPlatformAdmin` (Cross-Mandate-Audit) | | `gateway/modules/routes/routeNotifications.py` | 518 | unverändert (`addRoleToUserMandate` betrifft keine sysadmin mehr) | | `gateway/modules/routes/routeDataFiles.py` | Z.11, 548, 850, 1044 | Global-Scope → `isSysAdmin` bleibt (Infra-Daten) | | `gateway/modules/routes/routeDataSources.py` | Z.10, 56 | analog | | `gateway/modules/routes/routeWorkflowDashboard.py` | 9 Stellen | Cross-Mandate-Übersicht → `isPlatformAdmin` | | `gateway/modules/features/trustee/routeFeatureTrustee.py` | Z.107, 141, 159, 161, 1814 | prüfen pro Callsite (meist `isPlatformAdmin`) | | `gateway/modules/features/teamsbot/routeFeatureTeamsbot.py` | 8 Stellen | System-Bot-Registrierung → `isSysAdmin` bleibt; User-Mgmt → `isPlatformAdmin` | | `gateway/modules/features/chatbot/routeFeatureChatbot.py` | Z.105 | `isPlatformAdmin` | | `gateway/modules/features/realEstate/routeFeatureRealEstate.py` | Z.119 | `isPlatformAdmin` | | `gateway/modules/routes/routeRealEstate.py` | Z.124 | `isPlatformAdmin` | | `gateway/modules/features/workspace/...` | (suchen) | pro Callsite prüfen | | `gateway/modules/interfaces/interfaceDbManagement.py` | Z.637–643 `_isSysAdmin` | Bleibt an Flag gebunden (RBAC-Bypass in Data-Management) | ### Backend — Services / Demo / Tests | Datei | Was | Änderung | | -------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------------- | | `gateway/modules/demoConfigs/investorDemo2026.py` | Z.188, 222 | `isPlatformAdmin=True` statt Rolle; `isSysAdmin` wie gehabt | | `gateway/modules/serviceCenter/services/serviceAgent/...` | Z.568 | Liest Flag — kein Change | | `gateway/tests/**` | alle `hasSysAdminRole`/`requireSysAdminRole` Mentions | Tests anpassen | | `gateway/tests/integration/rbac/test_sysadmin_sync.py` (geplant Phase 1) | **entfällt** bzw. wird zu `test_platform_admin_flag.py` | | ### Frontend | Datei | Was | Änderung | | --------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- | | `frontend_nyla/src/types/mandate.ts` | User-Typ Z.143 | `isPlatformAdmin: boolean` ergänzen | | `frontend_nyla/src/api/userApi.ts` / `authApi.ts` | User-Schema | `isPlatformAdmin?: boolean` ergänzen | | `frontend_nyla/src/utils/userCache.ts` | User-Cache | `isPlatformAdmin` ergänzen | | `frontend_nyla/src/pages/admin/AdminUsersPage.tsx` | Edit-Formular | Zwei separate Toggles: "Systemadmin" und "Plattformadmin"; Tooltip mit Erklärung | | `frontend_nyla/src/pages/admin/AdminUserAccessOverviewPage.tsx` | User-Grid, Detail-Ansicht | Beide Flags anzeigen; Spalten + Filter | | `frontend_nyla/src/pages/billing/BillingAdmin.tsx` | Z.449, 505, 508, 669 | `isSysAdmin` → `isPlatformAdmin` für Cross-Mandate-View | | `frontend_nyla/src/pages/views/teamsbot/*.tsx` | Z.23, 24, 446, 329 | prüfen; meist `isPlatformAdmin` für Admin-UI, `isSysAdmin` für SystemBot-Config | | `frontend_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 | `gateway/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 | `gateway/tests/unit/rbac/test_sysadmin_migration.py` | done | | T5 | 5,6 | api-integration | ja | `gateway/tests/integration/rbac/test_platform_admin_flag.py` | done | | T6 | 7 | frontend (Playwright) | optional | `frontend_nyla/e2e/admin-users-flags.spec.ts` (neu) | open | | T7 | 8 | api-unit | ja | `gateway/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: `gateway/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 `gateway/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