""" AUTH.3E — delete_executor_core.py ================================== DELETE-Executor-Logik (DELETE-only, AUTH.3D-Validierung). Eigenschaften: * DELETE-only: claimt NUR C5_DELETE_OBJECT-Jobs * Der DELETE-Job selbst ist KEINE Approval * AUTH.3D-Composition: VALID_JOB AND VALID_STATE AND VALID_AUTH3D_SIGNATURE AND VALID_NONCE AND VALID_EXPIRY AND NOT_CONSUMED AND VALID_OBJECT AND VALID_PATH AND VALID_COMMIT AND VALID_PROVENANCE AND DELETE_CREDENTIAL * Kein Faktor ersetzt einen anderen * Fail-closed: kein HTTP ohne DELETE-Credential * OUTCOME_UNKNOWN bei unklarem HTTP-Ergebnis (kein blinder Retry) * Ein gestarteter DELETE wird bei Lease-Expiry NICHT blind wiederholt Isoliert implementiert (KEIN produktiver Container). Nutzt job_store + job_schema. """ from __future__ import annotations from typing import Any, Callable, Dict, Optional from job_schema import JOB_TYPE_DELETE from job_store import JobStore # --------------------------------------------------------------------------- # Result-Codes # --------------------------------------------------------------------------- RC_OK = "OK" RC_CREDENTIAL_MISSING = "CREDENTIAL_MISSING" RC_APPROVAL_MISSING = "APPROVAL_MISSING" RC_APPROVAL_INVALID = "APPROVAL_INVALID" RC_APPROVAL_EXPIRED = "APPROVAL_EXPIRED" RC_APPROVAL_CONSUMED = "APPROVAL_CONSUMED" RC_APPROVAL_MISMATCH = "APPROVAL_MISMATCH" RC_OBJECT_MISMATCH = "OBJECT_MISMATCH" RC_PATH_MISMATCH = "PATH_MISMATCH" RC_COMMIT_MISMATCH = "COMMIT_MISMATCH" RC_PROVENANCE_MISMATCH = "PROVENANCE_MISMATCH" RC_MISSION_MISMATCH = "MISSION_MISMATCH" RC_DELETE_DENIED = "DELETE_DENIED" RC_OUTCOME_UNKNOWN = "OUTCOME_UNKNOWN" RC_REJECTED = "REJECTED" class DeleteExecutorError(Exception): pass class DeleteExecutorCore: """ DELETE-Executor. worker_scope="DELETE" (nur DELETE-Jobs claimbar). approval_loader: Callable[[str], Optional[Dict[str, Any]]] — lädt die AUTH.3D-Approval (approval_id) -> dict oder None. approval_verify: Callable[[Dict[str, Any]], Dict[str, Any]] — verifiziert die AUTH.3D-Signatur (Ed25519 PUBLIC KEY ONLY) -> {"valid": bool, "reason": str}. tolaria_delete: Callable[[str], Dict[str, Any]] — führt den Tolaria-DELETE aus (vault_path) -> {"status": "ok"|"error", "uncertain": bool}. Muss fail-closed sein (kein HTTP ohne Credential). """ def __init__( self, store: JobStore, approval_loader: Callable[[str], Optional[Dict[str, Any]]], approval_verify: Callable[[Dict[str, Any]], Dict[str, Any]], tolaria_delete: Callable[[str], Dict[str, Any]], ): if store.worker_scope != "DELETE": raise DeleteExecutorError("DeleteExecutorCore requires worker_scope='DELETE'") self.store = store self.approval_loader = approval_loader self.approval_verify = approval_verify self.tolaria_delete = tolaria_delete # -- Hauptverarbeitung -------------------------------------------------- def process_job(self, job_id: str, worker_id: str) -> Dict[str, Any]: """ Verarbeitet einen DELETE-Job durch die State Machine. Ablauf: 1. Job laden (muss existieren) 2. Job-Type-Scope prüfen (nur DELETE) 3. READY -> CLAIMED (atomarer Claim) 4. CLAIMED -> EXECUTING 5. AUTH.3D-Composition validieren (Job + Approval exakt zusammenpassen) 6. Tolaria-DELETE ausführen 7. Ergebnis: SUCCEEDED / FAILED / OUTCOME_UNKNOWN / REJECTED """ job = self.store.get_job(job_id) if job is None: raise DeleteExecutorError(f"job not found: {job_id}") # Scope: nur DELETE-Jobs if job["job_type"] != JOB_TYPE_DELETE: self.store.mark_rejected(job_id, RC_REJECTED) return self.store.get_job(job_id) # Atomarer Claim try: self.store.claim_job(job_id, worker_id) except Exception: # Nicht claimbar (bereits geclaimt) -> kein Doppel-Delete return self.store.get_job(job_id) self.store.begin_execution(job_id, worker_id) # AUTH.3D-Composition (alle Faktoren, keiner ersetzt einen anderen). # Job NACH dem Claim neu lesen, damit eine Payload-Manipulation nach # dem Claim (TOCTOU) erkannt wird — Defense in depth. job = self.store.get_job(job_id) if job is None: self.store.mark_failed(job_id, RC_DELETE_DENIED, worker_id) return self.store.get_job(job_id) gate = self._validate_auth3d_composition(job) if gate["valid"] is not True: self.store.mark_failed(job_id, gate["code"], worker_id) return self.store.get_job(job_id) # Audit: Executor hat Job + AUTH.3D-Approval validiert (AUTHORIZED). # Ein REQUESTED-Eintrag ist KEINE Autorisierung — nur AUTHORIZED # (nach AUTH.3D-Validierung) autorisiert die Mutation. self.store.record_audit_event(job_id, "AUTHORIZED", worker_id=worker_id) # Tolaria-DELETE ausführen (fail-closed) result = self.tolaria_delete(job["vault_path"]) status = result.get("status") if status == "ok": self.store.mark_succeeded(job_id, worker_id) # Audit: Mutation ausgeführt (EXECUTED). self.store.record_audit_event(job_id, "EXECUTED", worker_id=worker_id) elif status == "error" and result.get("uncertain"): self.store.mark_outcome_unknown(job_id, worker_id) else: self.store.mark_failed(job_id, result.get("code", RC_DELETE_DENIED), worker_id) return self.store.get_job(job_id) # -- AUTH.3D-Composition ------------------------------------------------- def _validate_auth3d_composition(self, job: Dict[str, Any]) -> Dict[str, Any]: """ Validiert die AUTH.3D-Composition. Alle Faktoren müssen gelten: VALID_JOB AND VALID_STATE AND VALID_AUTH3D_SIGNATURE AND VALID_NONCE AND VALID_EXPIRY AND NOT_CONSUMED AND VALID_OBJECT AND VALID_PATH AND VALID_COMMIT AND VALID_PROVENANCE AND DELETE_CREDENTIAL Rückgabe: {"valid": True} oder {"valid": False, "code": RC_...} """ # VALID_JOB (Schema bereits beim Erzeugen validiert) # VALID_STATE (EXECUTING — bereits durch begin_execution) # Approval laden approval = self.approval_loader(job["approval_id"]) if approval is None: return {"valid": False, "code": RC_APPROVAL_MISSING} # VALID_AUTH3D_SIGNATURE (Ed25519 PUBLIC KEY ONLY) sig = self.approval_verify(approval) if sig.get("valid") is not True: return {"valid": False, "code": RC_APPROVAL_INVALID} # VALID_NONCE / VALID_EXPIRY / NOT_CONSUMED if approval.get("expired"): return {"valid": False, "code": RC_APPROVAL_EXPIRED} if approval.get("consumed"): return {"valid": False, "code": RC_APPROVAL_CONSUMED} # VALID_OBJECT if approval.get("object_id") != job["object_id"]: return {"valid": False, "code": RC_OBJECT_MISMATCH} # VALID_PATH if approval.get("vault_path") != job["vault_path"]: return {"valid": False, "code": RC_PATH_MISMATCH} # VALID_COMMIT if approval.get("expected_commit") != job["expected_commit"]: return {"valid": False, "code": RC_COMMIT_MISMATCH} # VALID_PROVENANCE if approval.get("expected_provenance_hash") != job["expected_provenance_hash"]: return {"valid": False, "code": RC_PROVENANCE_MISMATCH} # VALID_MISSION if approval.get("mission_id") != job["mission_id"]: return {"valid": False, "code": RC_MISSION_MISMATCH} # DELETE_CREDENTIAL (fail-closed — tolaria_delete prüft selbst) return {"valid": True}