scripts/check-m8-tenant-isolation.php: legt zwei frische, isolierte Test-Mandanten mit je einem Teilnehmer, einer Ledger-Buchung, einem Hinweis und einem Audit-Log-Eintrag an und prueft 10 Faelle - Lesezugriffe (Teilnehmerlisten, Einzelabruf, letzte Buchungen, aktive Hinweise, Audit-Log) und Schreibzugriffe (Buchung, Zugangsvergabe, Storno) sind strikt auf den jeweils richtigen Mandanten beschraenkt. Raeumt sich selbst auf. 10/10 gruen. scripts/check-m8-role-matrix.php: legt einen Test-Mandanten mit je einem Nutzer pro Rolle an (owner/admin/treasurer/member/viewer), loggt sich per echtem HTTP-Request ein (manueller Cookie-Jar ueber file_get_contents, da diese PHP-Installation keine curl-Extension hat) und prueft alle elf rollen-geschuetzten Seiten gegen die erwartete Rollenliste. 55/55 gruen (5 Rollen x 11 Seiten) - bestaetigt, dass die Rollenpruefungen ueberall konsistent mit dem im Plan dokumentierten Rollenmodell durchgesetzt sind. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
142 lines
5.7 KiB
Markdown
142 lines
5.7 KiB
Markdown
# M8 Härtung, Datenschutz und Betrieb
|
||
|
||
Stand: 2026-07-15
|
||
|
||
M8 macht die SaaS-App für den mehrmandantenfähigen Betrieb sicherer und
|
||
nachvollziehbarer: Security-Headers, Rate-Limits, ein zentrales Audit-Log,
|
||
sowie (folgend) Mandanten-Isolation, Rollenmatrix, Datenexport und
|
||
Löschung/Anonymisierung.
|
||
|
||
## Security-Headers
|
||
|
||
Umgesetzte Dateien:
|
||
|
||
```text
|
||
app/bootstrap.php
|
||
landing.php
|
||
```
|
||
|
||
- `app_send_security_headers()` setzt `X-Content-Type-Options: nosniff`,
|
||
`X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`,
|
||
`Permissions-Policy` (Geolocation/Mikrofon/Kamera aus) sowie `Strict-
|
||
Transport-Security` bei HTTPS. Der Aufruf steht am Ende von
|
||
`app/bootstrap.php`, das transitiv von jeder dynamischen Seite geladen
|
||
wird, und läuft damit automatisch einmal pro Request.
|
||
- Bewusst **kein** Content-Security-Policy-Header: Die bestehenden
|
||
Templates nutzen durchgängig `style=""`-Inline-Attribute (Banner,
|
||
Tabellenzellen, Status-Einfärbung). Ein CSP-Lockdown würde diese
|
||
brechen und bräuchte einen eigenen Template-Durchgang – als offener
|
||
Punkt vermerkt, nicht in diesem Schritt umgesetzt.
|
||
- `landing.php` war die einzige Seite ganz ohne PHP (reines HTML). Ein
|
||
minimaler `require_once app/bootstrap.php`-Aufruf am Dateianfang sorgt
|
||
dafür, dass auch sie die Header bekommt, ohne das Markup zu verändern.
|
||
|
||
## Rate-Limits
|
||
|
||
Umgesetzte Dateien:
|
||
|
||
```text
|
||
database/migrations/0010_saas_rate_limits.sql
|
||
app/rate-limit.php
|
||
login.php
|
||
register.php
|
||
passwort-vergessen.php
|
||
```
|
||
|
||
- Neue Tabelle `rate_limit_attempts` (Bucket + Zeitstempel) mit
|
||
`app_rate_limit_check()`: schreibt einen Versuch, räumt alte Einträge
|
||
für denselben Bucket auf und meldet, ob das Limit noch eingehalten ist.
|
||
- Login: 10 Versuche / 15 Minuten je E-Mail-Adresse **und** 20 Versuche /
|
||
15 Minuten je IP (beide müssen greifen, damit ein einzelnes Konto nicht
|
||
gezielt von verteilten IPs aus brute-forced werden kann und eine IP
|
||
nicht viele Konten gleichzeitig durchprobieren kann).
|
||
- Registrierung: 5 Versuche / Stunde je IP (gegen Spam-Registrierungen).
|
||
- Passwort-Reset-Anfrage: 5 / Stunde je E-Mail, 10 / Stunde je IP. Bei
|
||
überschrittenem Limit erscheint bewusst dieselbe generische
|
||
Erfolgsmeldung wie im echten Erfolgsfall, damit ein ausgereiztes Limit
|
||
nicht zusätzlich verrät, ob ein Konto existiert.
|
||
- Live getestet: 11 aufeinanderfolgende Fehlversuche gegen `login.php` –
|
||
die ersten 10 zeigen „ungültig“, der 11. wird korrekt mit „Zu viele
|
||
Anmeldeversuche“ abgewiesen.
|
||
|
||
## Audit-Log
|
||
|
||
Umgesetzte Dateien:
|
||
|
||
```text
|
||
database/migrations/0011_saas_audit_log.sql
|
||
app/audit.php
|
||
mitarbeiterverwalten.php
|
||
letzteneintraege.php
|
||
mandant-einstellungen.php
|
||
hinweise.php
|
||
csvupload.php
|
||
jahresauswertung.php
|
||
mailversenden.php
|
||
```
|
||
|
||
- Neue Tabelle `audit_log` (Mandant, ausführender Nutzer, Aktion,
|
||
betroffener Datensatz, Metadaten als JSON, IP, Zeitpunkt) gemäß
|
||
Kernschema aus dem Umstrukturierungsplan.
|
||
- `app_audit_log()` schreibt einen Eintrag, `app_fetch_audit_log()` liest
|
||
die letzten N Einträge eines Mandanten.
|
||
- Protokollierte Aktionen: Mitglied anlegen/bearbeiten/(de)aktivieren,
|
||
Zugang gewähren/entziehen, Storno von Einzahlung/Strich, Mandant-
|
||
Einstellungen ändern, Hinweis anlegen/löschen, CSV-Import bestätigen,
|
||
Jahresbonus-Verteilung, Live-Mailversand.
|
||
- Sichtbar für Owner/Admin auf `mandant-einstellungen.php` unter
|
||
„Protokoll" (letzte 50 Einträge, mit Namen des ausführenden Nutzers).
|
||
- Live getestet: Hinweis anlegen erzeugt einen Log-Eintrag mit korrekten
|
||
Metadaten; über einen eigens angelegten Test-Mandanten geprüft, dass
|
||
eine Einstellungsänderung im UI-Protokoll mit korrektem Nutzernamen
|
||
erscheint. Testdaten anschließend entfernt.
|
||
|
||
## Mandanten-Isolation (automatisiert)
|
||
|
||
Umgesetzte Datei: `scripts/check-m8-tenant-isolation.php`
|
||
|
||
Legt zwei frische, isolierte Test-Mandanten mit je einem Teilnehmer, einer
|
||
Ledger-Buchung, einem Hinweis und einem Audit-Log-Eintrag an und prüft
|
||
10 Fälle: Teilnehmerlisten, Einzelabruf, letzte Buchungen, aktive Hinweise,
|
||
Schreibversuche (Buchung, Zugangsvergabe, Storno) und Audit-Log sind
|
||
strikt auf den jeweils richtigen Mandanten beschränkt; ein Zugriff über den
|
||
falschen Mandanten liefert nichts beziehungsweise schlägt sauber fehl statt
|
||
fremde Daten zurückzugeben. Räumt die Testdaten am Ende selbst auf.
|
||
|
||
Ergebnis: grün mit 10 Assertions.
|
||
|
||
## Rollenmatrix (automatisiert)
|
||
|
||
Umgesetzte Datei: `scripts/check-m8-role-matrix.php`
|
||
|
||
Legt einen Test-Mandanten mit je einem Nutzer pro Rolle (`owner`, `admin`,
|
||
`treasurer`, `member`, `viewer`) an, loggt sich für jede Rolle per echtem
|
||
HTTP-Request ein (manueller Cookie-Jar über `file_get_contents`, da diese
|
||
PHP-Installation keine curl-Extension hat) und ruft alle rollen-geschützten
|
||
Seiten auf: `kaffeeliste.php`, `mitarbeiterverwalten.php`, `hinweise.php`,
|
||
`stricheintragen.php`, `einzahlung.php`, `letzteneintraege.php`,
|
||
`csvupload.php`, `exportKaffeeliste.php`, `mailversenden.php`,
|
||
`jahresauswertung.php`, `mandant-einstellungen.php`. Für jede
|
||
Rolle-Seite-Kombination wird geprüft, ob der tatsächliche Zugriff (anhand
|
||
der „Kein Zugriff"/„keine Berechtigung"-Marker in der Antwort) mit der
|
||
erwarteten Rollenliste übereinstimmt.
|
||
|
||
Ergebnis: grün mit 55 Assertions (5 Rollen × 11 Seiten). Räumt die
|
||
Testdaten am Ende selbst auf.
|
||
|
||
## Prüfstatus
|
||
|
||
- Golden Master: grün mit 104 Assertions.
|
||
- M4 Ledger-Migration: grün mit 73 Assertions.
|
||
- M3 Settings-Flow: grün mit 13 Assertions.
|
||
- HTTP-Smoke: grün mit 26 geprüften Seiten.
|
||
- M8 Mandanten-Isolation: grün mit 10 Assertions.
|
||
- M8 Rollenmatrix: grün mit 55 Assertions.
|
||
|
||
## Noch offen in M8
|
||
|
||
- Content-Security-Policy (braucht Template-Bereinigung der Inline-Styles).
|
||
- Datenexport pro Mandant.
|
||
- Lösch-/Anonymisierungsprozess für Teilnehmer und Kunden.
|
||
- Backup-/Restore-Prozess und Monitoring/Fehlerlogging dokumentieren.
|