""" AUTH.3D — approval_state.py Approval Lifecycle + Single-Use + Reservation (TOCTOU/Race Safety). Lifecycle: CREATED → VERIFIED → RESERVED → EXECUTED → CONSUMED - RESERVED: atomare Reservation vor dem Tolaria-DELETE (verhindert Race). - CONSUMED: endgültig, niemals erneut ausführbar. - OUTCOME_UNKNOWN: kein blinder Retry; manuelle Klärung. NUR isolierter Code + Tests. KEIN produktives Deployment. """ from __future__ import annotations import threading import uuid from typing import Any, Dict, Optional # Approval States ST_CREATED = "CREATED" ST_VERIFIED = "VERIFIED" ST_RESERVED = "RESERVED" ST_EXECUTED = "EXECUTED" ST_CONSUMED = "CONSUMED" ST_INVALID = "INVALID" ST_EXPIRED = "EXPIRED" ST_REVOKED = "REVOKED" ST_FAILED = "FAILED" ST_OUTCOME_UNKNOWN = "OUTCOME_UNKNOWN" # Terminal (nicht mehr ausführbar) States _TERMINAL = {ST_CONSUMED, ST_INVALID, ST_EXPIRED, ST_REVOKED, ST_FAILED} # Übergänge, die eine Ausführung erlauben _EXECUTABLE = {ST_CREATED, ST_VERIFIED, ST_RESERVED} class ApprovalStateError(Exception): """Ungültiger State-Übergang (FAIL CLOSED).""" class ApprovalStateStore: """ In-Memory State Store für isolierte Tests. In Produktion würde dies die C5-DB (delete_approvals) erweitern. Hier: reine, thread-sichere In-Memory-Implementierung für Tests. """ def __init__(self): self._lock = threading.RLock() self._approvals: Dict[str, Dict[str, Any]] = {} self._used_nonces: set = set() def create(self, approval_id: str, nonce: str) -> Dict[str, Any]: with self._lock: if approval_id in self._approvals: raise ApprovalStateError(f"Approval {approval_id} existiert bereits") rec = { "approval_id": approval_id, "nonce": nonce, "state": ST_CREATED, "attempt_id": None, "idempotency_key": None, "consumed_at": None, "result": None, } self._approvals[approval_id] = rec return dict(rec) def get(self, approval_id: str) -> Optional[Dict[str, Any]]: with self._lock: rec = self._approvals.get(approval_id) return dict(rec) if rec else None def transition(self, approval_id: str, new_state: str, **fields) -> Dict[str, Any]: with self._lock: rec = self._approvals.get(approval_id) if rec is None: raise ApprovalStateError(f"Approval {approval_id} existiert nicht") if new_state == ST_CONSUMED: if rec["state"] not in _EXECUTABLE: raise ApprovalStateError( f"Approval {approval_id} in State {rec['state']} kann nicht CONSUMED werden" ) rec["state"] = new_state rec.update(fields) if new_state == ST_CONSUMED: rec["consumed_at"] = fields.get("consumed_at") return dict(rec) def reserve( self, approval_id: str, attempt_id: str, idempotency_key: str ) -> Dict[str, Any]: """ Atomare Reservation: APPROVED/RESERVED → RESERVED (nur wenn noch ausführbar). Verhindert TOCTOU-Race: nur EINE Reservation pro Approval. """ with self._lock: rec = self._approvals.get(approval_id) if rec is None: raise ApprovalStateError(f"Approval {approval_id} existiert nicht") if rec["state"] not in _EXECUTABLE: raise ApprovalStateError( f"Approval {approval_id} in State {rec['state']} kann nicht reserviert werden" ) # Idempotenz: gleicher Attempt wird nicht doppelt reserviert. if rec.get("idempotency_key") == idempotency_key: return dict(rec) # Bereits reserviert (anderer Attempt) -> kein zweiter Reservation. if rec["state"] == ST_RESERVED: raise ApprovalStateError( f"Approval {approval_id} ist bereits reserviert (Attempt {rec.get('attempt_id')})" ) rec["state"] = ST_RESERVED rec["attempt_id"] = attempt_id rec["idempotency_key"] = idempotency_key return dict(rec) def mark_used_nonce(self, nonce: str) -> None: with self._lock: self._used_nonces.add(nonce) def is_nonce_used(self, nonce: str) -> bool: with self._lock: return nonce in self._used_nonces def consume(self, approval_id: str, result: str) -> Dict[str, Any]: """Markiert als CONSUMED (endgültig).""" return self.transition(approval_id, ST_CONSUMED, result=result) def mark_outcome_unknown(self, approval_id: str) -> Dict[str, Any]: """Markiert als OUTCOME_UNKNOWN (kein blinder Retry).""" return self.transition(approval_id, ST_OUTCOME_UNKNOWN) def recover_from_unknown(self, approval_id: str, target_absent: bool) -> Dict[str, Any]: """ Recovery aus OUTCOME_UNKNOWN: - target_absent=True -> CONSUMED (Delete war erfolgreich) - target_absent=False -> zurück zu RESERVED (Delete nicht ausgeführt) """ with self._lock: rec = self._approvals.get(approval_id) if rec is None: raise ApprovalStateError(f"Approval {approval_id} existiert nicht") if rec["state"] != ST_OUTCOME_UNKNOWN: raise ApprovalStateError( f"Approval {approval_id} ist nicht OUTCOME_UNKNOWN (State: {rec['state']})" ) if target_absent: rec["state"] = ST_CONSUMED rec["result"] = "DELETE_OK" else: rec["state"] = ST_RESERVED rec["result"] = None return dict(rec) def new_attempt_id() -> str: return str(uuid.uuid4()) def new_idempotency_key() -> str: return str(uuid.uuid4())