Bündel 3: Security-Helper + Demo-Migration, SW by-v1115

NEUE HELPER in auth.py:

require_moderator(user=Depends(get_current_user))
  Konsequente Dependency statt inline
  'if user["rolle"] not in ("admin", "moderator")'

require_breeder(user=Depends(get_current_user))
  Konsequente Dependency statt inline
  'if user["subscription_tier"] not in ("breeder", "breeder_test")'

require_owner(row, user, owner_field='user_id',
              not_found_msg, forbidden_msg) -> row
  Zentralisiert das häufigste Pattern (54 Stellen im Audit):
  Statt:
    row = conn.execute(...).fetchone()
    if not row: raise HTTPException(404, ...)
    if row['user_id'] != user['id']: raise HTTPException(403, ...)
  Jetzt:
    row = require_owner(conn.execute(...).fetchone(), user,
                        not_found_msg='Ort nicht gefunden.')

is_owner_or_admin(row, user, owner_field='user_id') -> bool
  True wenn Owner ODER Admin/Moderator (Admin-Override für
  Moderations-Endpoints)

DEMO-MIGRATION:
places.py PATCH /places/{id} + DELETE /places/{id} migriert auf
require_owner() — als Style-Referenz für künftige Migrationen.

KEINE Massen-Migration der 54 Stellen — bewusste Entscheidung
weil security-kritisch. Helper sind bereitgestellt, neuer Code
nutzt sie, bestehender bleibt funktional identisch.

Tests 19/19 grün.

Hinweis: Massen-Migration der Owner-Checks ist eigener Sprint mit
sehr sorgfältigem Testing — bei jeder migrierten Route muss die
404→403→Cascade durchgeprüft werden, dass Owner+Non-Owner+Admin
sich identisch zum Vorher verhalten.
This commit is contained in:
rene 2026-05-27 11:27:00 +02:00
parent 297bd22f96
commit 35937ed51b
7 changed files with 68 additions and 27 deletions

View file

@ -212,6 +212,49 @@ def require_admin(user=Depends(get_current_user)):
return user
def require_moderator(user=Depends(get_current_user)):
"""Dependency: Admin oder Moderator. Konsequente Nutzung statt
Inline-`if user['rolle'] not in (...):` in den Routen."""
if user["rolle"] not in ("admin", "moderator") and not user.get("is_moderator"):
raise HTTPException(status.HTTP_403_FORBIDDEN, "Moderator-Zugriff erforderlich.")
return user
def require_breeder(user=Depends(get_current_user)):
"""Dependency: Admin oder Züchter (breeder/breeder_test)."""
if user["rolle"] == "admin":
return user
if user.get("subscription_tier") in ("breeder", "breeder_test"):
return user
raise HTTPException(status.HTTP_403_FORBIDDEN, "Züchter-Zugriff erforderlich.")
# ------------------------------------------------------------------
# Owner-Checks — zentral, statt 54x inline `if row['user_id'] != user['id']: 403`
# ------------------------------------------------------------------
def require_owner(row, user: dict, owner_field: str = "user_id",
not_found_msg: str = "Nicht gefunden",
forbidden_msg: str = "Kein Zugriff"):
"""Wirft 404 wenn row None/falsy ist, 403 wenn User nicht Besitzer.
Returns row für chainability:
dog = require_owner(conn.execute(...).fetchone(), user, 'user_id', 'Hund nicht gefunden')
"""
if not row:
raise HTTPException(status.HTTP_404_NOT_FOUND, not_found_msg)
if row[owner_field] != user["id"]:
raise HTTPException(status.HTTP_403_FORBIDDEN, forbidden_msg)
return row
def is_owner_or_admin(row, user: dict, owner_field: str = "user_id") -> bool:
"""True wenn User Owner ist oder Admin/Moderator."""
if not row:
return False
if user["rolle"] in ("admin", "moderator") or user.get("is_moderator"):
return True
return row[owner_field] == user["id"]
def has_pro_access(user: dict) -> bool:
"""True wenn User Pro-Features nutzen darf."""
if not user: