# Compliance-Implementierung – Umsetzungsbericht **Erstellt:** 2026-05-09 **Zuletzt aktualisiert:** 2026-05-10 **Audit-Basis:** `docs/compliance-audit.md` **App-Version nach Umsetzung:** 0.8.67 --- ## Freigegebene Pakete und Umsetzungsstatus ### 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:** Nicht umgesetzt — weiterhin offen (MITT-05) **Befund:** `logout()` in `frontend/src/context/AuthContext.jsx` löscht nur `localStorage`-Einträge: ```javascript const logout = () => { setUser(null) localStorage.removeItem('authToken') localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY) // sessionStorage wird NICHT geleert } ``` `TrainingCoachPage` schreibt folgende Schlüssel in `sessionStorage`: - `storageStepKey(unitId)` – aktueller Trainingsschritt (Zahl) - `storageDeltasKey(unitId)` – Trainingsdeltas (JSON) - `storageDebriefKey(unitId)` – Debrief-Status (Boolean) Nach einem Logout bleiben diese Daten im `sessionStorage` des Tabs erhalten. Bei einem Nutzerwechsel im selben Tab (geteilter Rechner) kann der neue Nutzer Trainingsfortschritt des Vorgängers sehen, bis der Tab geschlossen wird. **Risikoeinstufung:** MITT-05 (mittleres Risiko; sessionStorage ist tab-lokal und wird beim Tab-Schließen gelöscht; erfordert physischen Zugang zum selben offenen Tab) **Fix (nicht durchgeführt, ca. 15 Minuten Aufwand):** ```javascript // AuthContext.jsx logout(): const logout = () => { setUser(null) localStorage.removeItem('authToken') localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY) sessionStorage.clear() // oder gezielt nach 'shinkan_coach_' prefix } ``` --- ### 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.67) ``` 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: 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) ``` --- ## Nicht umgesetzte Pakete | Paket | Status | Begründung | |-------|--------|------------| | P-01 | offen | Rechtstexte — Scope ausgeschlossen | | P-02 | offen | Self-Service-Löschworkflow — Scope ausgeschlossen | | P-06 | offen | HSTS-Header — Scope ausgeschlossen | | P-09 | offen | Kein Einwilligungsdialog Recht am eigenen Bild — Scope ausgeschlossen | | P-10 | offen | DSA-Meldeverfahren — Scope ausgeschlossen | | P-11 | offen | HttpOnly-Cookie-Migration — Scope ausgeschlossen | | P-12 | offen | sessionStorage bei Logout — nicht freigegeben, Aufwand ca. 15 min | | P-13–P-16 | offen | Scope ausgeschlossen | --- ## Re-Audit-Empfehlung Operativ nach Deployment 0.8.67 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).