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:
parent
297bd22f96
commit
35937ed51b
7 changed files with 68 additions and 27 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue