Back-Office: globaler Platform-Admin-Zugang ueber alle Mandanten

Neue, bewusst von tenant_memberships/saas_user_has_role() komplett
getrennte Platform-Admin-Ebene (neue Tabelle platform_admins), damit die
bestehende, automatisiert getestete Mandanten-Isolation
(check-m8-tenant-isolation.php, check-m8-role-matrix.php) unangetastet
bleibt - Platform-Admin-Rechte wirken ausschliesslich auf den neuen
backoffice-*.php-Seiten.

- backoffice.php: Uebersicht aller Mandanten (Status, Teilnehmerzahl,
  Saldensumme).
- backoffice-mandant.php: reine Leseansicht eines Mandanten (Einstellungen,
  Mitglieder/Rollen, letzte Buchungen, letzte Admin-Aktionen). Bewusst
  kein Schreibzugriff von hier aus.
- backoffice-export.php: nutzt dieselbe app_export_tenant_data() wie der
  Selbstbedienungs-Export, ausgeloest durch den Platform-Admin fuer
  beliebige Mandanten.
- scripts/grant-platform-admin.php: CLI-only Bootstrap fuer den ersten
  Platform-Admin, bewusst keine Web-UI dafuer.
- Jede Back-Office-Ansicht/-Export wird im Audit-Log DES BETROFFENEN
  MANDANTEN protokolliert (Transparenzpflicht), nicht nur beim Betreiber.

Der bestehende Selbstbedienungs-Export (datenexport.php aus M8) bleibt
zusaetzlich bestehen statt ersetzt zu werden: der Mandant ist im AV-
Verhaeltnis Verantwortlicher, Art. 15/20/28 DSGVO verpflichten den
Auftragsverarbeiter zur Unterstuetzung bei Ausk''unfts-/Portabilitaets-
rechten - ein jederzeit verfuegbarer Mandanten-Export erfuellt das direkt.
Details und Begruendung in docs/backoffice.md.

Live getestet: Back-Office zeigt alle Mandanten korrekt (inkl. echter
Bestandsmandanten), Detail/Export fuer Test-Mandant funktioniert,
Audit-Log korrekt geschrieben. Kritischer Test bestanden: derselbe
Platform-Admin sieht auf normalen Mandanten-Seiten weiterhin nur seinen
eigenen Mandanten; ein eingeloggter Nicht-Platform-Admin bekommt 403.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 22:18:48 +02:00
co-authored by Claude Sonnet 5
parent 8b0dcd2c70
commit cbe6547f3a
8 changed files with 494 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
# Back-Office (Platform-Admin)
Stand: 2026-07-16
Ergänzung außerhalb der ursprünglichen M0M9-Meilensteine: ein
Betreiber-Zugang, der mandantenübergreifend alle Kunden einsehen kann
getrennt von der normalen, streng mandantengebundenen Rollenlogik.
## Design-Entscheidung: getrennt von `tenant_memberships`
Platform-Admin-Rechte sind bewusst **kein** Teil von
`saas_user_has_role()`/`tenant_memberships`, sondern eine eigene, komplett
separate Tabelle `platform_admins` (`user_id``users.id`). Das ist
kein Zufall: Alle bestehenden Mandanten-Seiten und die automatisierten
Isolationstests (`scripts/check-m8-tenant-isolation.php`,
`scripts/check-m8-role-matrix.php`) verlassen sich darauf, dass ein
Login niemals automatisch mandantenübergreifenden Zugriff bekommt. Ein
Platform-Admin-Flag direkt in `tenant_memberships` oder `users` einzubauen
hätte dieses Fundament unterlaufen können. Stattdessen prüft
ausschließlich der neue Code-Pfad in `app/platform-admin.php`
(`app_require_platform_admin()`) auf Back-Office-Seiten die
Platform-Admin-Eigenschaft; alle bestehenden Seiten sind unverändert und
bleiben strikt mandantengebunden.
## Umgesetzte Dateien
```text
database/migrations/0013_saas_platform_admins.sql
app/platform-admin.php
scripts/grant-platform-admin.php
backoffice.php
backoffice-mandant.php
backoffice-export.php
```
## Erster Platform-Admin
Es gibt bewusst **keine Weboberfläche**, um den ersten Platform-Admin zu
setzen das wäre ein öffentlich erreichbarer "werde Admin"-Endpunkt und
ein erhebliches Risiko. Stattdessen: Person registriert sich normal als
Mandant (oder nutzt einen bestehenden Login), danach per Shell-Zugriff auf
dem Server:
```bash
php scripts/grant-platform-admin.php person@example.com
```
Das Skript prüft, dass der Account existiert, und legt nur dann den
`platform_admins`-Eintrag an. Zugang entziehen aktuell nur per direktem
`DELETE FROM platform_admins WHERE user_id = ?` (keine UI dafür aus
demselben Grund wie beim Setzen).
## Funktionsumfang
- `backoffice.php`: Übersicht aller Mandanten mit Kürzel, Status,
Erstelldatum, Teilnehmerzahl (aktiv/gesamt) und Saldensumme.
- `backoffice-mandant.php?tenant_id=X`: reine Leseansicht eines einzelnen
Mandanten Einstellungen, Mitglieder mit Rolle, letzte 20 Buchungen,
letzte 20 Admin-Aktionen. Absichtlich **kein** Schreibzugriff auf
Mandantendaten von hier aus, um das Risiko einer versehentlichen
Fremdänderung auszuschließen.
- `backoffice-export.php`: nutzt dieselbe `app_export_tenant_data()`-
Funktion wie der Selbstbedienungs-Export, aber ausgelöst durch den
Platform-Admin für einen beliebigen Mandanten.
## Verhältnis zum Selbstbedienungs-Export (`datenexport.php`)
Bewusste Entscheidung: Der bestehende Selbstbedienungs-Export für
Mandanten-Owner/Admin (`datenexport.php`, aus M8) bleibt zusätzlich
bestehen, statt ihn durch das Back-Office zu ersetzen. Begründung: Der
Mandant ist im Auftragsverarbeitungs-Verhältnis der Verantwortliche
(Controller), der Betreiber dieser App der Auftragsverarbeiter
(Processor). Art. 15/20 DSGVO geben den betroffenen Personen ein
Auskunfts-/Portabilitätsrecht, und Art. 28 DSGVO verpflichtet den
Auftragsverarbeiter, den Verantwortlichen bei der Erfüllung dieser
Rechte zu unterstützen sowie Daten am Vertragsende zurückzugeben ein
jederzeit verfügbarer Selbstbedienungs-Export erfüllt genau das. Das
Back-Office ergänzt das um einen Betreiber-seitigen Zugriff für Support,
Migrationen oder eigene Nachweispflichten, ersetzt den
Mandanten-Selbstexport aber nicht.
## Audit-Trail
Jede Back-Office-Ansicht und jeder Back-Office-Export wird im Audit-Log
**des betroffenen Mandanten** protokolliert
(`backoffice.tenant_viewed`, `backoffice.tenant_exported`, mit dem
Platform-Admin als `actor_user_id`). Ein Mandant sieht damit über sein
eigenes Protokoll (`mandant-einstellungen.php`), wann der Betreiber auf
seine Daten zugegriffen hat Transparenzpflicht statt stiller
Einsicht.
## Prüfstatus
- `scripts/check-m8-tenant-isolation.php`: weiterhin grün mit 10
Assertions das Back-Office berührt die geprüften Pfade nicht.
- `scripts/check-m8-role-matrix.php`: weiterhin grün mit 55 Assertions.
- HTTP-Smoke: grün mit 30 geprüften Seiten (zwei neue Login-Schutz-Checks
für `backoffice.php`/`backoffice-mandant.php`).
- Golden Master: grün mit 104 Assertions.
Live getestet: eigens angelegter Test-Mandant, per CLI-Skript zum
Platform-Admin gemacht, Back-Office-Übersicht zeigt korrekt alle
Mandanten (inklusive der echten Bestandsmandanten), Detailansicht und
Export für den eigenen Test-Mandanten funktionieren, Audit-Log-Einträge
korrekt geschrieben. Kritischer Isolationstest bestätigt: derselbe
Platform-Admin sieht auf regulären Mandanten-Seiten (`kaffeeliste.php`)
weiterhin ausschließlich seinen eigenen Mandanten. Ein zweiter,
eingeloggter, aber nicht privilegierter Testnutzer bekommt auf
`backoffice.php` korrekt `403 Forbidden`. Alle Testdaten anschließend
vollständig entfernt.