- approval_payload: kanonischer, deterministischer Approval-Payload - approval_signature: Ed25519 sign/verify (Christian=Private Key, Executor=Public Key only) - approval_verifier: verify-only, alle Bindings fail-closed - approval_state: Lifecycle CREATED->CONSUMED, Single-Use, Reservation, OUTCOME_UNKNOWN - test_approval_auth3d: T1-T30 + adversarial (50 Tests) - test_approval_helpers: synthetische Test-Keypairs - sensitivity_proof: Mutationen A-J machen Tests ROT Nur synthetische Test-Keypairs. Kein produktives Deployment. DELETE_OPERATION_ACTIVATION bleibt BLOCKED bis AUTH.3D geprueft.
165 lines
5.9 KiB
Python
165 lines
5.9 KiB
Python
"""
|
|
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())
|