feat(dsgvo): Art. 17 Anonymisierung von Mitarbeiterkonten

Statt Hard-Delete (kollidiert mit Lohn-/Steuer-Aufbewahrung) werden bei
DSGVO-Löschanfrage die personenbezogenen Felder der bestehenden User-Zeile
überschrieben; referenzierte Daten (Zeit/Abwesenheit/Auszahlung/AuditLog)
bleiben entkoppelt erhalten.

- users.anonymized_at (Migration 0040); UserOut.anonymized_at.
- user_service.anonymize_user: überschreibt Name/E-Mail (eindeutig anon-<id>@deleted.local),
  entfernt Auth-Material (Passwort/PIN/QR/NFC/TOTP/LDAP-DN), is_active=False, löscht Sessions,
  AuditLog 'user_anonymized'. Personalnummer bleibt reserviert (kein Personenbezug mehr).
  Selbst-Anonymisierung (400), Doppelanonymisierung/Reaktivierung anonymisierter Konten (409).
- POST /users/{id}/anonymize (COMPANY_ADMIN/SUPER_ADMIN, mandantengeprüft).
- UsersPage: "Anonymisieren"-Button mit Sicherheitsabfrage + "Anonymisiert"-Badge.

193/193 Tests grün. Deployed 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 14:31:14 +02:00
co-authored by Claude Opus 4.8
parent 3034c6c55a
commit c122ab6fd7
7 changed files with 153 additions and 2 deletions
+35
View File
@@ -126,3 +126,38 @@ async def test_kiosk_pin_must_be_numeric(client: AsyncClient, registered_user):
json={"pin": "abcd"},
)
assert resp.status_code == 422
async def test_anonymize_user(client: AsyncClient, registered_user):
"""DSGVO Art. 17: Anonymisierung entfernt den Personenbezug, Konto wird inaktiv."""
inv = await client.post(INVITE_URL, headers=auth(registered_user["tokens"]), json={
"email": "gdpr@test.de", "first_name": "Max", "last_name": "Muster",
"role": "EMPLOYEE", "initial_password": "Secret123",
})
user_id = inv.json()["id"]
resp = await client.post(f"{USERS_URL}/{user_id}/anonymize", headers=auth(registered_user["tokens"]))
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["anonymized_at"] is not None
assert body["first_name"] == "Anonymisiert"
assert body["email"] == f"anon-{user_id}@deleted.local"
assert body["is_active"] is False
# Login mit alter E-Mail unmöglich
login = await client.post("/api/v1/auth/login", json={"email": "gdpr@test.de", "password": "Secret123"})
assert login.status_code == 401
# Reaktivierung eines anonymisierten Kontos wird abgelehnt
react = await client.post(f"{USERS_URL}/{user_id}/reactivate", headers=auth(registered_user["tokens"]))
assert react.status_code == 409
# Erneutes Anonymisieren wird abgelehnt
again = await client.post(f"{USERS_URL}/{user_id}/anonymize", headers=auth(registered_user["tokens"]))
assert again.status_code == 409
async def test_cannot_anonymize_self(client: AsyncClient, registered_user):
user_id = registered_user["user"]["id"]
resp = await client.post(f"{USERS_URL}/{user_id}/anonymize", headers=auth(registered_user["tokens"]))
assert resp.status_code == 400