All checks were successful
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 32s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 32s
- compliance-implementation.md: P-01-Abschnitt hinzugefügt (technisch teilweise umgesetzt), Testergebnisse, Version 0.8.69 - compliance-package-register.md: P-01 ❌ open → ⚠️ partially implemented; Fortschrittsblock aktualisiert; App-Version 0.8.69 - compliance-roadmap.md: P-01 in teilweise umgesetzte Pakete; Blocker 1 aktualisiert; §6 nächste Freigabe auf P-06 / Etappe B; Anhang Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
302 lines
13 KiB
Markdown
302 lines
13 KiB
Markdown
# Compliance-Implementierung – Umsetzungsbericht
|
||
|
||
**Erstellt:** 2026-05-09
|
||
**Zuletzt aktualisiert:** 2026-05-10
|
||
**Audit-Basis:** `docs/compliance-audit.md`
|
||
**App-Version nach Umsetzung:** 0.8.69
|
||
|
||
---
|
||
|
||
## Freigegebene Pakete und Umsetzungsstatus
|
||
|
||
### P-01 – Rechtstexte ⚠️ (technischer Teil umgesetzt; juristische Inhalte offen)
|
||
|
||
**Status:** Technisch teilweise umgesetzt (2026-05-10, Version 0.8.69)
|
||
|
||
**Betroffene Dateien:**
|
||
- `frontend/src/pages/LegalPage.jsx` (neu) — Platzhalter-Komponente für alle vier Seiten
|
||
- `frontend/src/App.jsx` — 4 neue öffentliche Routen
|
||
- `frontend/src/pages/LoginPage.jsx` — Rechtstext-Links im Card-Footer
|
||
- `frontend/src/components/DesktopSidebar.jsx` — Rechtstext-Links im Sidebar-Footer
|
||
|
||
**Technische Änderung:**
|
||
Vier öffentliche Routen angelegt, kein Auth erforderlich, kein Redirect für nicht eingeloggte Nutzer:
|
||
- `/impressum` → `<LegalPage type="impressum" />`
|
||
- `/datenschutz` → `<LegalPage type="datenschutz" />`
|
||
- `/nutzungsbedingungen` → `<LegalPage type="nutzungsbedingungen" />`
|
||
- `/medienrichtlinie` → `<LegalPage type="medienrichtlinie" />`
|
||
|
||
Jede Seite enthält:
|
||
- Deutlich sichtbaren Platzhalterhinweis: „MUSTER / PLATZHALTER – Inhalt wird vor Produktivbetrieb juristisch geprüft und durch den Betreiber ergänzt."
|
||
- Strukturierte Pflichtfelder je Rechtstext (Betreiber, Rechtsgrundlagen, Betroffenenrechte etc.)
|
||
- Querlinks zu den anderen drei Rechtstextseiten
|
||
|
||
Links zu allen vier Seiten sind in der Login-/Registrierungsseite (Card-Footer) und im Desktop-Sidebar-Footer sichtbar ohne Anmeldung erreichbar.
|
||
|
||
**Nicht umgesetzt (außerhalb Freigabe):**
|
||
Juristische Texte, inhaltliche Überprüfung, Admin-konfigurierbare Inhalte, Einwilligungs-Checkboxen.
|
||
|
||
**KRIT-01 Blocking-Status:**
|
||
Der Blocker KRIT-01 bleibt **offen** bis juristisch geprüfte Texte durch den Betreiber eingepflegt sind. Die technische Voraussetzung (Routen und Seitenstruktur) ist erfüllt.
|
||
|
||
**Tests:** 5 Playwright-Tests, alle grün:
|
||
- Impressum ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
|
||
- Datenschutz ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
|
||
- Nutzungsbedingungen ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
|
||
- Medienrichtlinie ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
|
||
- Login-Seite enthält alle vier Rechtstext-Links
|
||
|
||
---
|
||
|
||
### P-03 – Papierkorb-Retention-Job aktivieren ✅
|
||
|
||
**Status:** Umgesetzt
|
||
|
||
**Betroffene Dateien:**
|
||
- `docker-compose.yml` – neuer Service `retention-cron`
|
||
|
||
**Technische Änderung:**
|
||
Neuer Docker-Service `retention-cron` nutzt dasselbe Backend-Image und führt `scripts/media_retention_job.py` täglich um 03:00 Uhr aus. Der Service startet beim ersten Hochfahren sofort und schläft bis zum nächsten 3 AM (Python-basierter Loop ohne externe Cron-Abhängigkeit). Zugriff auf DB und Media-Volume identisch zur Backend-Konfiguration.
|
||
|
||
**Tests:** Keine automatisierten Tests möglich (Runtime-Verhalten); operativ über Container-Logs (`docker logs shinkan-retention-cron`) überprüfbar.
|
||
|
||
---
|
||
|
||
### P-03b – Retention-Zeiten mit Löschkonzept abgleichen ✅
|
||
|
||
**Status:** Umgesetzt (2026-05-10)
|
||
|
||
**Betroffene Dateien:**
|
||
- `backend/media_lifecycle.py:24` – Default `HIDDEN_TO_PURGE_DAYS`
|
||
- `docker-compose.yml` – explizite Env-Variable im `retention-cron`-Service
|
||
|
||
**Befund des Re-Audits:**
|
||
Das fachliche Löschkonzept (Audit §6.4) sieht 30 + 30 Tage vor:
|
||
- Stufe 1 → Stufe 2 (Soft → Hidden): 30 Tage ✓ (war bereits korrekt)
|
||
- Stufe 2 → Purge (Hidden → gelöscht): **weitere 30 Tage** → Code-Default war 90 Tage ❌
|
||
|
||
**Technische Änderung:**
|
||
```python
|
||
# media_lifecycle.py Zeile 24 (vorher "90", jetzt "30"):
|
||
HIDDEN_TO_PURGE_DAYS = max(1, int(os.getenv("MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS", "30")))
|
||
```
|
||
|
||
`docker-compose.yml` enthält jetzt explizit `MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS: "${...:-30}"` mit Kommentar, der das 30+30-Konzept dokumentiert. Der Wert kann per Env-Variable in einzelnen Deployments überschrieben werden.
|
||
|
||
---
|
||
|
||
### P-04 – Copyright-Pflicht bei Archiv-Promotion vereinheitlichen ✅
|
||
|
||
**Status:** Umgesetzt; Tests nachgehärtet (2026-05-10)
|
||
|
||
**Betroffene Dateien:**
|
||
- `backend/routers/media_assets.py` – `patch_media_asset()` und `bulk_media_patch()`
|
||
- `backend/tests/test_media_assets_copyright_promotion.py`
|
||
|
||
**Technische Änderung:**
|
||
Beide Endpoints prüfen, ob `copyright_notice` vorhanden ist, wenn `visibility` auf `club` oder `official` gewechselt wird. Priorität: Wert aus dem Request-Body > bestehender Wert in der DB. Ist beides leer, wird HTTP 400 zurückgegeben.
|
||
|
||
Fehlermeldung: `"Für Vereins- oder offizielle Medien ist eine Urheberrechtsangabe (copyright_notice) Pflicht."` (Umlaut korrekt, 2026-05-10 korrigiert)
|
||
|
||
**Tests:** 7 Tests, alle grün:
|
||
- Promotion zu `club` ohne Copyright → **400** (exakt)
|
||
- Promotion zu `official` ohne Copyright → **400** (exakt)
|
||
- Promotion zu `club` mit Copyright im Body → **200** + Payload-Prüfung (gehärtet)
|
||
- Promotion zu `club`, Copyright bereits auf Asset → **200** + Payload-Prüfung (gehärtet)
|
||
- Kein Visibility-Wechsel → keine Copyright-Prüfung → **200** (exakt)
|
||
- Bulk: ohne Copyright → in `failed`, `updated_count == 0` (exakt)
|
||
- Bulk: mit Copyright → in `updated`, `updated_count == 1` (exakt)
|
||
|
||
---
|
||
|
||
### P-05 – Passwort-Mindestlänge angleichen ⚠️ (Teil 1 von 2 – Re-Audit-Auflage offen)
|
||
|
||
**Status:** Teilweise umgesetzt
|
||
|
||
**Betroffene Dateien (initialer Fix 2026-05-09):**
|
||
- `backend/routers/auth.py:101` – `PUT /api/auth/pin`: `< 4` → `< 8`
|
||
- `frontend/src/pages/LoginPage.jsx:175` – `minLength="6"` → `minLength="8"`
|
||
- `frontend/src/pages/AccountSettingsPage.jsx:403` – `minLength={4}` → `minLength={8}`
|
||
|
||
**Verbleibende Lücke identifiziert im Re-Audit 2026-05-09:** `POST /api/auth/reset-password` hatte kein Mindestlängen-Limit → siehe P-05b.
|
||
|
||
---
|
||
|
||
### P-05b – reset-password Mindestlänge 8 Zeichen ✅
|
||
|
||
**Status:** Umgesetzt (2026-05-10)
|
||
|
||
**Betroffene Dateien:**
|
||
- `backend/models.py:29` – `PasswordResetConfirm.new_password`
|
||
- `backend/tests/test_auth_password_reset_minlength.py` – 7 neue Tests
|
||
|
||
**Technische Änderung:**
|
||
```python
|
||
# models.py (vorher):
|
||
class PasswordResetConfirm(BaseModel):
|
||
token: str
|
||
new_password: str
|
||
|
||
# models.py (nachher):
|
||
class PasswordResetConfirm(BaseModel):
|
||
token: str
|
||
new_password: str = Field(min_length=8, max_length=128)
|
||
```
|
||
|
||
FastAPI lehnt Requests mit `new_password < 8 Zeichen` nun mit HTTP **422** (Pydantic Validation Error) ab, bevor der Endpoint-Handler ausgeführt wird. Kein DB-Zugriff erfolgt für unvalide Requests.
|
||
|
||
**Tests:** 7 Tests, alle grün:
|
||
- Leer-String → 422
|
||
- 1-Zeichen-Passwort → 422
|
||
- 7-Zeichen-Passwort (`"1234567"`) → 422
|
||
- Exakt 8 Zeichen (`"12345678"`) → **200** ✓
|
||
- Langes Passwort → **200** ✓
|
||
- Fehlendes `new_password`-Feld → 422
|
||
- Gültiges Passwort, ungültiger Token → 400
|
||
|
||
**P-05 vollständig geschlossen?** Ja — alle passwortverarbeitenden Backend-Endpoints erzwingen jetzt Mindestlänge 8:
|
||
|
||
| Endpoint | Mindestlänge | Mechanismus |
|
||
|---|---|---|
|
||
| `POST /api/auth/register` | 8 | `if len(password) < 8` im Handler |
|
||
| `PUT /api/auth/pin` | 8 | `if len(new_pin) < 8` im Handler |
|
||
| `POST /api/auth/reset-password` | 8 | Pydantic `Field(min_length=8)` |
|
||
| Management-Reset (profiles.py) | 8 | Pydantic `Field(min_length=8)` |
|
||
| Frontend LoginPage | 8 | `minLength="8"` |
|
||
| Frontend AccountSettingsPage | 8 | `minLength={8}` |
|
||
|
||
---
|
||
|
||
### P-07 – ALLOW_PUBLIC_MEDIA_STATIC Release-Test ✅
|
||
|
||
**Status:** Umgesetzt
|
||
|
||
**Betroffene Dateien:**
|
||
- `backend/tests/test_security_release.py` – 2 neue Tests
|
||
|
||
**Tests:** Beide grün.
|
||
|
||
---
|
||
|
||
### P-12 – sessionStorage bei Logout bereinigen ✅
|
||
|
||
**Status:** Umgesetzt (2026-05-10, Version 0.8.68)
|
||
|
||
**Betroffene Dateien:**
|
||
- `frontend/src/context/AuthContext.jsx` – `logout()`
|
||
- `tests/dev-smoke-test.spec.js` – neuer Playwright-Test
|
||
|
||
**Befund (war offen):**
|
||
`logout()` löschte nur `localStorage`-Einträge. `TrainingCoachPage` schrieb sessionStorage-Schlüssel mit Präfix `sj_coach_` (`sj_coach_step_{unitId}`, `sj_coach_deltas_{unitId}`, `sj_coach_debrief_{unitId}`), die nach Logout im Tab erhalten blieben. Bei Nutzerwechsel im selben Tab (geteilter Rechner) konnte der neue Nutzer Trainingsfortschritt des Vorgängers sehen.
|
||
|
||
**Technische Änderung:**
|
||
Gezielte Präfix-Löschung aller `sj_coach_*`-Schlüssel beim Logout (kein `sessionStorage.clear()`). Fremde sessionStorage-Schlüssel (Browser-Extensions o. ä.) bleiben erhalten.
|
||
|
||
```javascript
|
||
// AuthContext.jsx logout() — Ergänzung:
|
||
for (const key of Object.keys(sessionStorage)) {
|
||
if (key.startsWith('sj_coach_')) {
|
||
sessionStorage.removeItem(key)
|
||
}
|
||
}
|
||
```
|
||
|
||
**Begründung gezielte statt vollständiger Löschung:**
|
||
Alle Shinkan-spezifischen sessionStorage-Schlüssel sind eindeutig über den Präfix `sj_coach_` identifizierbar (definiert in `TrainingCoachPage.jsx` Zeilen 15–25). Ein `sessionStorage.clear()` würde auch Schlüssel fremder Quellen im selben Tab löschen; die Präfix-Löschung ist spezifischer und sicherer.
|
||
|
||
**Tests:** Playwright E2E-Test `tests/dev-smoke-test.spec.js` (Test „P-12: sessionStorage wird bei Logout bereinigt"):
|
||
- Setzt drei `sj_coach_*`-Schlüssel und einen fremden Schlüssel
|
||
- Klickt „Abmelden"
|
||
- Prüft: `sj_coach_*` → null (entfernt), fremder Schlüssel → erhalten, `authToken` → null (localStorage weiterhin korrekt bereinigt)
|
||
|
||
**Hinweis Tests:** Das Projekt verfügt über kein Frontend-Unit-Test-Framework (kein Vitest/Jest in package.json). Der Test ist als Playwright E2E-Test implementiert, der einen laufenden Dev-Server voraussetzt. Automatisierte Ausführung der Playwright-Tests erfordert `npx playwright test` mit gesetzter `PLAYWRIGHT_BASE_URL`.
|
||
|
||
---
|
||
|
||
### P-23 – LoginPage: minLength + Versionsstring ✅
|
||
|
||
**Status:** Umgesetzt
|
||
|
||
**Betroffene Dateien:**
|
||
- `frontend/src/pages/LoginPage.jsx`
|
||
|
||
**Technische Änderung:**
|
||
- `minLength="6"` → `minLength="8"`
|
||
- Hardcodierter Versionsstring `v0.1.0 • Development` entfernt
|
||
|
||
---
|
||
|
||
### P-24 – CORS allow_methods und allow_headers einschränken ✅
|
||
|
||
**Status:** Umgesetzt
|
||
|
||
**Betroffene Dateien:**
|
||
- `backend/main.py:85-86`
|
||
|
||
**Technische Änderung:**
|
||
```python
|
||
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||
allow_headers=["Content-Type", "X-Auth-Token", "X-Active-Club-Id"],
|
||
```
|
||
|
||
---
|
||
|
||
## Test-Zusammenfassung (Stand 0.8.69)
|
||
|
||
```
|
||
tests/test_auth_password_reset_minlength.py 7 passed (neu, P-05b)
|
||
tests/test_media_assets_copyright_promotion.py 7 passed (gehärtet, P-04)
|
||
tests/test_security_release.py 9 passed (inkl. 2 P-07-Tests)
|
||
Weitere bestehende Tests: 81 passed, 6 skipped
|
||
Gesamt (Backend): 104 passed, 6 skipped, 1 failed
|
||
|
||
Fehlgeschlagener Test: test_list_media_assets_invalid_lifecycle_400
|
||
→ Pre-existing: benötigt laufenden PostgreSQL-Container (Hostname "postgres")
|
||
→ Bestand bereits vor allen Compliance-Änderungen (verifiziert per git stash)
|
||
|
||
Playwright E2E (dev-smoke-test.spec.js): 14 passed (inkl. 5 neue P-01-Tests)
|
||
→ P-01: 4× Route ohne Auth + Platzhalterhinweis + Reload, 1× Login-Links
|
||
→ P-12: sessionStorage-Bereinigung (grün)
|
||
```
|
||
|
||
---
|
||
|
||
## Nicht umgesetzte Pakete
|
||
|
||
> Paket-IDs und -Titel gemäß kanonischem Register `docs/compliance-package-register.md`.
|
||
> Abweichende Beschreibungen in der Ursprungsversion dieses Abschnitts wurden am 2026-05-10 korrigiert (P-06, P-08, P-09, P-10, P-11, P-18 — Details im Konsistenzbericht des Registers).
|
||
|
||
| Paket | Kanonischer Titel | Status | Begründung |
|
||
|-------|------------------|--------|------------|
|
||
| P-01 | Rechtstexte | offen | Scope ausgeschlossen (juristischer Inhalt) |
|
||
| P-02 | Self-Service-Kontolöschung + Datenexport | offen | Scope ausgeschlossen |
|
||
| P-06 | Upload-Einwilligungsdialog (Recht am eigenen Bild) | offen | Scope ausgeschlossen |
|
||
| P-08 | HSTS / externe Proxy-Sicherheit dokumentieren | offen | Scope ausgeschlossen (außerhalb Repo — Reverse-Proxy) |
|
||
| P-09 | Admin-Audit-Log | offen | Scope ausgeschlossen |
|
||
| P-10 | Mindestalter-Abfrage | offen | Scope ausgeschlossen |
|
||
| P-11 | Legal-Hold Lifecycle-Status | offen | Scope ausgeschlossen |
|
||
| P-12 | sessionStorage bei Logout bereinigen | ✅ umgesetzt | Version 0.8.68 — siehe §P-12 oben |
|
||
| P-13 | Content-Melde-Backend | offen | Scope ausgeschlossen (erst juristisch klären) |
|
||
| P-14 | Moderations-UI | offen | Scope ausgeschlossen |
|
||
| P-15 | Uploader-Benachrichtigung bei Sperrung | offen | Scope ausgeschlossen |
|
||
| P-16 | Beschwerdeverfahren | offen | Scope ausgeschlossen |
|
||
| P-17 | MFA für Superadmins (TOTP) | offen | Scope ausgeschlossen |
|
||
| P-18 | HttpOnly-Cookie als Auth-Alternative | offen | Scope ausgeschlossen |
|
||
| P-19 | Anti-Virus-Scan (ClamAV) | offen | Scope ausgeschlossen |
|
||
| P-20 | VVT erstellen | offen | Scope ausgeschlossen (Betreiber-Aufgabe) |
|
||
| P-21 | AV-Verträge abschließen | offen | Scope ausgeschlossen (Betreiber-Aufgabe) |
|
||
| P-22 | HTML-Sanitizer für Rich-Text-Felder | offen | Scope ausgeschlossen |
|
||
|
||
---
|
||
|
||
## Re-Audit-Empfehlung
|
||
|
||
Operativ nach Deployment 0.8.69 prüfen:
|
||
|
||
1. **P-03/P-03b**: `docker logs shinkan-retention-cron` — Job läuft täglich 03:00 Uhr; Retention-Zeiten: 30 → 30 Tage
|
||
2. **P-04**: Manuell: PATCH privates Medium auf `official` ohne `copyright_notice` → muss 400 liefern
|
||
3. **P-05b**: Manuell: Reset-Link mit 7-Zeichen-Passwort → muss mit Fehler abgewiesen werden
|
||
4. **P-24**: Browser DevTools Preflight → `Access-Control-Allow-Headers: content-type, x-auth-token, x-active-club-id`
|
||
|
||
Nächster vollständiger Re-Audit nach Umsetzung der kritischen Findings (P-01: Impressum/Datenschutz, P-02: DSGVO-Löschanfragen).
|