shinkan-jinkendo/docs/compliance-implementation.md

248 lines
9.3 KiB
Markdown
Raw Normal View History

# 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).