fix(security): F03 (Astra P1) — Sitzungswiderruf in beiden Session-Stores durchsetzen

Vorher (Astra-Finding): Widerruf war inkonsistent ueber vier Pfade:
- Deaktivierung invalidierte nur den Berechtigungscache — Sessions
  liefen bis TTL (8h) weiter
- Loeschung invalidierte GAR NICHTS
- Passwortwechsel loeschte nur Redis-Sessions — PostgreSQL-Fallback-
  Sessions ueberlebten jeden Redis-Ausfall
- Fehlende UserTenant-Mitgliedschaft wurde durchgewinkt statt
  abgewiesen

Fix:
- Neuer zentraler Helfer revoke_user_sessions_all_stores (app/core/auth.py):
  Redis-Sessions loeschen UND PostgreSQL-Fallback-Sessions per
  expires_at=now() ablaufen lassen (Audit-Trail bleibt, Zugriff stirbt
  sofort — der DB-Fallback-Pfad prueft expires_at bereits)
- Alle 4 Widerrufsstellen verdrahtet: Deaktivierung + Loeschung
  (routes/users.py), Passwortwechsel (user_service.py), Passwort-Reset
  (auth_service.py)
- Membership-Check in get_current_user fail-closed: None (fehlende
  Mitgliedschaft) wird abgewiesen statt durchgelassen

Abnahme (Astra): Deaktivierung, Austritt und Passwortwechsel wirken
unmittelbar — auch bei Redis-Ausfall (Unit-Test beweist die
DB-Fallback-Abgelaufen-Rejection).

Tests: test_s1_security_guards.py 10/10 (3 neue F03-Tests) +
test_auth.py 11/11 + ruff clean.
This commit is contained in:
Agent Zero
2026-09-18 07:59:31 +02:00
parent b8a556091e
commit 47432651f1
6 changed files with 195 additions and 8 deletions
+15
View File
@@ -339,6 +339,15 @@ async def update_user(
except Exception:
pass # Best-effort — don't fail the update if Redis is down
# F03 (Astra): deactivation must take effect IMMEDIATELY in BOTH session
# stores (Redis runtime + PostgreSQL fallback) — not just the permission
# cache. Otherwise a deactivated user keeps working until the session TTL
# (8h) expires, and DB-fallback sessions survive Redis outages entirely.
if body.is_active is False:
from app.core.auth import revoke_user_sessions_all_stores
await revoke_user_sessions_all_stores(uid)
return {
"id": str(user.id),
"email": user.email,
@@ -379,6 +388,12 @@ async def delete_user(
if not success:
raise HTTPException(404, detail={"detail": "User not found", "code": "not_found"})
# F03 (Astra): deletion must revoke all sessions immediately — in BOTH
# stores (Redis runtime + PostgreSQL fallback).
from app.core.auth import revoke_user_sessions_all_stores
await revoke_user_sessions_all_stores(uid)
await log_audit(
db,
tenant_id,