fix(security): F11 (Astra P1) — Freigaben an Entscheider, Ablauf und Atomizitaet binden
Vorher: resolve_approval_request pruefte nur Mandant + pending — NICHT
Ablaufdatum, NICHT den vorgesehenen Genehmiger, und ueberschrieb
approver_id mit dem tatsaechlichen Entscheider (Zuordnung verloren).
Astra-Repro: Eine abgelaufene Anfrage konnte von einem anderen
Entscheider genehmigt werden; konkurrierende Entscheidungen waren
moeglich.
Fix:
- Migration 0146: neue Spalte resolved_by (Zuordnung vs. Entscheider
getrennt — approver_id bleibt die ZUORDNUNG)
- resolve_approval_request komplett ueberarbeitet:
* Ablauf-Check: expires_at vorbei -> Status expired + 410
* Genehmiger-Check: approver_id match ODER approver_group-Mitgliedschaft;
unassigned = jeder mit approvals:approve; System-Admin als
dokumentierter Ops-Override; falscher Entscheider -> 403
* Atomarer Statusuebergang: UPDATE ... WHERE status=pending —
konkurrierende Entscheidung -> 409
* approver_id wird NIE ueberschrieben; resolved_by dokumentiert den
Entscheider
- ApprovalDecisionError mit HTTP-Status-Codes; approve/reject-Routen
fangen sie sauber ab (404/409/410/403 statt Flat-404)
- ApprovalResponse + Mapper um resolved_by ergaenzt
Abnahme (Astra): Falscher Entscheider, abgelaufene Anfrage und doppelte
Entscheidung werden abgewiesen — erfuellt (6 Tests).
Hinweis: workflows.py approve/reject-Aufrufer waren bereits kaputt
(F12, S2-Welle: approval[id] auf ORM-Objekt) und werden dort gefixt.
Tests: test_s1_security_guards.py 18/18 (6 neue F11-Tests). ruff clean.
Damit ist S1 — ALLE 11 Sicherheits-Findings der Astra-Welle 1 gefixt.
This commit is contained in:
+38
-18
@@ -64,6 +64,7 @@ class ApprovalResponse(BaseModel):
|
||||
requested_by_type: str
|
||||
approver_id: str | None = None
|
||||
approver_group: str | None = None
|
||||
resolved_by: str | None = None
|
||||
status: str
|
||||
comment: str | None = None
|
||||
created_at: str | None = None
|
||||
@@ -93,6 +94,7 @@ def _to_response(r: ApprovalRequest) -> ApprovalResponse:
|
||||
requested_by_type=r.requested_by_type,
|
||||
approver_id=str(r.approver_id) if r.approver_id else None,
|
||||
approver_group=r.approver_group,
|
||||
resolved_by=str(r.resolved_by) if r.resolved_by else None,
|
||||
status=r.status,
|
||||
comment=r.comment,
|
||||
created_at=r.created_at.isoformat() if r.created_at else None,
|
||||
@@ -242,16 +244,25 @@ async def approve_approval(
|
||||
"""Approve a pending approval request."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
rid = _parse_uuid(request_id, "request_id")
|
||||
req = await resolve_approval_request(
|
||||
db,
|
||||
tenant_id,
|
||||
rid,
|
||||
decision="approved",
|
||||
approver_id=uuid.UUID(current_user["user_id"]),
|
||||
comment=body.comment,
|
||||
)
|
||||
from app.core.approval import ApprovalDecisionError
|
||||
|
||||
try:
|
||||
req = await resolve_approval_request(
|
||||
db,
|
||||
tenant_id,
|
||||
rid,
|
||||
decision="approved",
|
||||
approver_id=uuid.UUID(current_user["user_id"]),
|
||||
comment=body.comment,
|
||||
is_system_admin=bool(current_user.get("is_system_admin", False)),
|
||||
)
|
||||
except ApprovalDecisionError as exc:
|
||||
raise HTTPException(
|
||||
status_code=exc.http_status,
|
||||
detail={"detail": str(exc), "code": exc.code},
|
||||
) from exc
|
||||
if req is None:
|
||||
raise HTTPException(status_code=404, detail="Approval request not found or not pending")
|
||||
raise HTTPException(status_code=404, detail="Approval request not found")
|
||||
await db.commit()
|
||||
return _to_response(req)
|
||||
|
||||
@@ -270,16 +281,25 @@ async def reject_approval(
|
||||
"""Reject a pending approval request."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
rid = _parse_uuid(request_id, "request_id")
|
||||
req = await resolve_approval_request(
|
||||
db,
|
||||
tenant_id,
|
||||
rid,
|
||||
decision="rejected",
|
||||
approver_id=uuid.UUID(current_user["user_id"]),
|
||||
comment=body.comment,
|
||||
)
|
||||
from app.core.approval import ApprovalDecisionError
|
||||
|
||||
try:
|
||||
req = await resolve_approval_request(
|
||||
db,
|
||||
tenant_id,
|
||||
rid,
|
||||
decision="rejected",
|
||||
approver_id=uuid.UUID(current_user["user_id"]),
|
||||
comment=body.comment,
|
||||
is_system_admin=bool(current_user.get("is_system_admin", False)),
|
||||
)
|
||||
except ApprovalDecisionError as exc:
|
||||
raise HTTPException(
|
||||
status_code=exc.http_status,
|
||||
detail={"detail": str(exc), "code": exc.code},
|
||||
) from exc
|
||||
if req is None:
|
||||
raise HTTPException(status_code=404, detail="Approval request not found or not pending")
|
||||
raise HTTPException(status_code=404, detail="Approval request not found")
|
||||
await db.commit()
|
||||
return _to_response(req)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user