- test_delete_worker.py: 42 tests (T1-T42) against productive delete_* SoT code (T29 delete_request_id mismatch, T41 second-delete guard, T42 immutable guard) - test_mutations.py: 15 mutations A-O, all detected (P11 sensitivity) AUTH.4D P14. No deployment, no token injection, no key provisioning.
696 lines
33 KiB
Python
696 lines
33 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
AUTH.4D — test_delete_worker.py (P10 Isolated Test Contract)
|
||
=============================================================
|
||
Isolierte Tests für den DELETE-scoped Worker + DeleteExecutorCore +
|
||
TolariaClient.delete() gegen Fakes. KEIN echtes HTTP, KEINE produktive DB,
|
||
KEIN produktiver Vault.
|
||
|
||
Abgedeckte Fälle (T1–T40):
|
||
T1 DELETE Worker importiert KEIN save_executor_core
|
||
T2 DELETE-Pfad kann write() NICHT aufrufen (keine write-Capability)
|
||
T3 SAVE-Token allein kann DELETE NICHT autorisieren
|
||
T4 DELETE-Token allein kann Approval NICHT umgehen
|
||
T5 gültige synthetische Approval + gültiges synthetisches DELETE-Token
|
||
erreicht die Mutationsgrenze (nur im Fake-Fixture)
|
||
T6 kein echtes HTTP (Fake-Client, kein Netz)
|
||
T7 keine produktive DB (temp-Datei)
|
||
T8 kein produktiver Vault (Fake)
|
||
T9 DELETE ohne Approval -> BLOCKED (APPROVAL_MISSING)
|
||
T10 DELETE mit ungültiger Signatur -> BLOCKED (APPROVAL_INVALID)
|
||
T11 DELETE mit abgelaufener Approval -> BLOCKED (APPROVAL_INVALID)
|
||
T12 DELETE mit consumed Approval -> BLOCKED (APPROVAL_INVALID)
|
||
T13 DELETE mit falschem Objekt -> BLOCKED (OBJECT_MISMATCH)
|
||
T14 DELETE mit falschem Pfad -> BLOCKED (PATH_MISMATCH)
|
||
T15 DELETE mit falschem Commit -> BLOCKED (COMMIT_MISMATCH)
|
||
T16 DELETE mit falscher Provenance -> BLOCKED (PROVENANCE_MISMATCH)
|
||
T17 DELETE mit falscher Mission -> BLOCKED (MISSION_MISMATCH)
|
||
T18 DELETE ohne DELETE-Token -> fail-closed (CREDENTIAL_MISSING)
|
||
T19 DELETE mit falschem Token (401) -> fail-closed (AUTH_FAILURE)
|
||
T20 DELETE mit Timeout -> OUTCOME_UNKNOWN (kein blinder Retry)
|
||
T21 Job allein (ohne Approval) autorisiert NIE
|
||
T22 SAVE-Job wird vom DELETE-Worker REJECTED
|
||
T23 zweiter DELETE auf gleiche Approval -> single-use verhindert
|
||
T24 Replay (Nonce wiederverwendet) -> BLOCKED
|
||
T25 Approval für anderes Objekt autorisiert DELETE NIE
|
||
T26 Approval für anderen Commit autorisiert DELETE NIE
|
||
T27 Approval für anderen Pfad autorisiert DELETE NIE
|
||
T28 Approval für andere Mission autorisiert DELETE NIE
|
||
T29 Approval für anderen Request autorisiert DELETE NIE
|
||
T30 kein generic dispatcher (kein command/url/method im Job)
|
||
T31 kein shell/python im Job
|
||
T32 kein arbitrary endpoint (fester /delete)
|
||
T33 kein arbitrary method (nur POST)
|
||
T34 kein arbitrary header
|
||
T35 kein SAVE fallback
|
||
T36 kein Docker-Socket / SSH im Executor
|
||
T37 kein Private Key im Executor
|
||
T38 kein SAVE-Token im DELETE Executor
|
||
T39 kein DELETE-Token im SAVE Executor
|
||
T40 OUTCOME_UNKNOWN -> kein blinder Retry
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
import uuid
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any, Dict, Optional
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
from approval_payload import build_payload, parse_payload
|
||
from approval_signature import generate_test_keypair, key_id, sign
|
||
from approval_state import ApprovalStateStore
|
||
from approval_verifier import ApprovalVerifier
|
||
from delete_executor_core import DeleteExecutorCore
|
||
from job_schema import JOB_TYPE_DELETE, JOB_TYPE_SAVE, make_delete_job, make_save_job
|
||
from job_store import JobStore, JobImmutableFieldError
|
||
from delete_tolaria_client import TolariaClient, TolariaDeleteError
|
||
|
||
# Synthetisches Test-Keypair (NUR Tests, KEIN echter Key)
|
||
PRIV, PUB = generate_test_keypair()
|
||
PUB_KEY_ID = key_id(PUB)
|
||
|
||
COMMIT = "5f3adda73fba2f7ae284efd5ec3d244e087a69a8"
|
||
PROV = "798ae0315c14aa9d7e3a3f759c952f7c18cbce5154844f447e597b7fd1776515"
|
||
MISSION = str(uuid.uuid4())
|
||
DELETE_REQ = str(uuid.uuid4())
|
||
OBJECT = str(uuid.uuid4())
|
||
PATH = "docs/auth4d-canary.md"
|
||
NONCE = str(uuid.uuid4())
|
||
_NOW = datetime.now(timezone.utc)
|
||
ISSUED = (_NOW - timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||
EXPIRES = (_NOW + timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||
TEST_DELETE_TOKEN = "a" * 64
|
||
|
||
|
||
def make_approval(*, object_id=OBJECT, vault_path=PATH, expected_commit=COMMIT,
|
||
expected_provenance_hash=PROV, mission_id=MISSION,
|
||
delete_request_id=DELETE_REQ, nonce=NONCE,
|
||
issued_at=ISSUED, expires_at=EXPIRES) -> Dict[str, Any]:
|
||
"""Baut eine synthetische, signierte Approval."""
|
||
payload = build_payload(
|
||
approval_id=str(uuid.uuid4()),
|
||
mission_id=mission_id,
|
||
delete_request_id=delete_request_id,
|
||
object_id=object_id,
|
||
vault_path=vault_path,
|
||
expected_commit=expected_commit,
|
||
expected_provenance_hash=expected_provenance_hash,
|
||
nonce=nonce,
|
||
issued_at=issued_at,
|
||
expires_at=expires_at,
|
||
)
|
||
sig = sign(PRIV, payload)
|
||
return {
|
||
"raw_payload": __import__("json").dumps(payload, separators=(",", ":")),
|
||
"signature": sig,
|
||
"approval_id": payload["approval_id"],
|
||
"mission_id": mission_id,
|
||
"delete_request_id": delete_request_id,
|
||
"object_id": object_id,
|
||
"vault_path": vault_path,
|
||
"expected_commit": expected_commit,
|
||
"expected_provenance_hash": expected_provenance_hash,
|
||
"nonce": nonce,
|
||
"consumed": False,
|
||
"state_allows_delete": True,
|
||
"used_nonces": set(),
|
||
}
|
||
|
||
|
||
def make_delete_job_with_approval(approval: Dict[str, Any], *, job_type=JOB_TYPE_DELETE,
|
||
object_id=OBJECT, vault_path=PATH,
|
||
expected_commit=COMMIT, expected_provenance_hash=PROV,
|
||
mission_id=MISSION, delete_request_id=DELETE_REQ,
|
||
approval_id=None) -> Dict[str, Any]:
|
||
"""Baut einen DELETE-Job, der auf eine Approval (im Store) verweist."""
|
||
return make_delete_job(
|
||
job_id=str(uuid.uuid4()),
|
||
mission_id=mission_id,
|
||
delete_request_id=delete_request_id,
|
||
object_id=object_id,
|
||
vault_path=vault_path,
|
||
expected_commit=expected_commit,
|
||
expected_provenance_hash=expected_provenance_hash,
|
||
approval_id=approval_id or approval["approval_id"],
|
||
created_at="2026-08-27T12:00:00Z",
|
||
idempotency_key=str(uuid.uuid4()),
|
||
)
|
||
|
||
|
||
class FakeTolaria:
|
||
"""In-Memory Fake der Tolaria-Vault-API (delete/read)."""
|
||
|
||
def __init__(self):
|
||
self.vault: Dict[str, str] = {}
|
||
self.delete_count = 0
|
||
self.delete_calls: list = []
|
||
self.fail_next = None # ("error", code, uncertain)
|
||
self.unavailable = False
|
||
|
||
def delete(self, vault_path: str) -> Dict[str, Any]:
|
||
"""Erfüllt das DeleteExecutorCore-Contract: gibt dict zurück, wirft nicht."""
|
||
if self.unavailable:
|
||
return {"status": "error", "code": "TOLARIA_UNAVAILABLE", "uncertain": True}
|
||
if self.fail_next:
|
||
_, code, uncertain = self.fail_next
|
||
self.fail_next = None
|
||
if code == "AUTH_FAILURE":
|
||
return {"status": "error", "code": "AUTH_FAILURE", "uncertain": False}
|
||
if uncertain:
|
||
return {"status": "error", "code": "TOLARIA_UNAVAILABLE", "uncertain": True}
|
||
self.delete_count += 1
|
||
self.delete_calls.append(vault_path)
|
||
self.vault.pop(vault_path, None)
|
||
return {"status": "ok"}
|
||
|
||
def read(self, vault_path: str) -> Optional[str]:
|
||
return self.vault.get(vault_path)
|
||
|
||
|
||
class DeleteWorkerTest(unittest.TestCase):
|
||
"""Isolierte Tests für den DELETE-Worker-Pfad."""
|
||
|
||
def setUp(self):
|
||
self.tmp = tempfile.mkdtemp()
|
||
self.db_path = os.path.join(self.tmp, "c5a_delete.db")
|
||
self.store = JobStore(self.db_path, "DELETE")
|
||
self.fake = FakeTolaria()
|
||
self.verifier = ApprovalVerifier(public_keys={PUB_KEY_ID: PUB})
|
||
self.approval_store = ApprovalStateStore()
|
||
|
||
def tearDown(self):
|
||
self.store.close()
|
||
|
||
def _register_approval(self, approval: Dict[str, Any]) -> None:
|
||
"""Registriert eine Approval im State-Store (Single-Use/Nonce).
|
||
|
||
Das Store-Record enthält alle Felder, die DeleteExecutorCore aus der
|
||
Approval liest (object_id, vault_path, expected_commit,
|
||
expected_provenance_hash, mission_id, expired, consumed).
|
||
"""
|
||
self.approval_store.create(approval["approval_id"], approval["nonce"])
|
||
self.approval_store._approvals[approval["approval_id"]].update({
|
||
"raw_payload": approval["raw_payload"],
|
||
"signature": approval["signature"],
|
||
"mission_id": approval["mission_id"],
|
||
"delete_request_id": approval["delete_request_id"],
|
||
"object_id": approval["object_id"],
|
||
"vault_path": approval["vault_path"],
|
||
"expected_commit": approval["expected_commit"],
|
||
"expected_provenance_hash": approval["expected_provenance_hash"],
|
||
"nonce": approval["nonce"],
|
||
"consumed": approval.get("consumed", False),
|
||
"expired": approval.get("expired", False),
|
||
"state_allows_delete": approval.get("state_allows_delete", True),
|
||
"used_nonces": approval.get("used_nonces", set()),
|
||
})
|
||
|
||
def _make_approved_job(self, approval: Optional[Dict[str, Any]] = None,
|
||
**kw) -> tuple[Dict[str, Any], Dict[str, Any]]:
|
||
"""Erstellt + registriert eine Approval und einen referenzierenden Job."""
|
||
approval = approval or make_approval(**kw)
|
||
self._register_approval(approval)
|
||
job = make_delete_job_with_approval(approval)
|
||
self.store.create_job(job)
|
||
self.store.mark_ready(job["job_id"])
|
||
return job, approval
|
||
|
||
def _core(self, delete_token=TEST_DELETE_TOKEN, verifier=None) -> DeleteExecutorCore:
|
||
client = TolariaClient(base_url="http://fake", delete_token=delete_token)
|
||
v = verifier or self.verifier
|
||
|
||
def approval_loader(approval_id: str) -> Optional[Dict[str, Any]]:
|
||
return self.approval_store.get(approval_id)
|
||
|
||
def approval_verify(approval: Dict[str, Any]) -> Dict[str, Any]:
|
||
try:
|
||
# NUR Signatur kryptographisch prüfen (Ed25519 PUBLIC KEY ONLY).
|
||
# Die Zustands-Checks (expired/consumed/nonce) macht der Core
|
||
# isoliert (Defense-in-Depth: Verifier + Core prüfen getrennt).
|
||
payload = parse_payload(approval["raw_payload"])
|
||
v.verify_approval(
|
||
approval["raw_payload"], approval["signature"],
|
||
expected_mission_id=payload["mission_id"],
|
||
expected_delete_request_id=payload["delete_request_id"],
|
||
expected_object_id=payload["object_id"],
|
||
expected_vault_path=payload["vault_path"],
|
||
expected_commit=payload["expected_commit"],
|
||
expected_provenance_hash=payload["expected_provenance_hash"],
|
||
expected_nonce=payload["nonce"],
|
||
# Zustands-Checks dem Core überlassen (isoliert testbar):
|
||
consumed=False,
|
||
state_allows_delete=True,
|
||
used_nonces=set(),
|
||
)
|
||
return {"valid": True, "reason": "ok"}
|
||
except Exception as e:
|
||
return {"valid": False, "code": getattr(e, "reason_code", "DELETE_DENIED"),
|
||
"reason": str(e)}
|
||
|
||
def tolaria_delete(vault_path: str) -> Dict[str, Any]:
|
||
try:
|
||
client.delete(vault_path)
|
||
return {"status": "ok"}
|
||
except Exception as e:
|
||
code = getattr(e, "code", "TOLARIA_UNAVAILABLE")
|
||
if code == "CREDENTIAL_MISSING":
|
||
return {"status": "error", "code": "CREDENTIAL_MISSING", "uncertain": False}
|
||
if code in ("AUTH_FAILURE", "INVALID_SCHEMA"):
|
||
return {"status": "error", "code": code, "uncertain": False}
|
||
return {"status": "error", "code": "TOLARIA_UNAVAILABLE", "uncertain": True}
|
||
|
||
return DeleteExecutorCore(self.store, approval_loader, approval_verify, tolaria_delete)
|
||
|
||
# -- T1: DELETE Worker importiert KEIN save_executor_core ----------------
|
||
def test_t1_worker_imports_no_save_core(self):
|
||
import delete_worker
|
||
src = open(delete_worker.__file__).read()
|
||
self.assertNotIn("save_executor_core", src)
|
||
self.assertNotIn("SaveExecutorCore", src)
|
||
self.assertNotIn("forgejo_source_loader", src)
|
||
|
||
# -- T2: DELETE-Pfad kann write() NICHT aufrufen -------------------------
|
||
def test_t2_delete_path_cannot_call_write(self):
|
||
client = TolariaClient(base_url="http://fake", delete_token=TEST_DELETE_TOKEN)
|
||
self.assertFalse(hasattr(client, "write"))
|
||
self.assertTrue(hasattr(client, "delete"))
|
||
|
||
# -- T3: SAVE-Token allein kann DELETE NICHT autorisieren ----------------
|
||
def test_t3_save_token_alone_cannot_authorize_delete(self):
|
||
client = TolariaClient(base_url="http://fake", delete_token=None)
|
||
with self.assertRaises(TolariaDeleteError) as ctx:
|
||
client.delete("x.md")
|
||
self.assertEqual(ctx.exception.code, "CREDENTIAL_MISSING")
|
||
|
||
# -- T4: DELETE-Token allein kann Approval NICHT umgehen -----------------
|
||
def test_t4_delete_token_alone_cannot_bypass_approval(self):
|
||
job = make_delete_job(
|
||
job_id=str(uuid.uuid4()), mission_id=MISSION, delete_request_id=DELETE_REQ,
|
||
object_id=OBJECT, vault_path=PATH, expected_commit=COMMIT,
|
||
expected_provenance_hash=PROV, approval_id=str(uuid.uuid4()),
|
||
created_at="2026-08-27T12:00:00Z", idempotency_key=str(uuid.uuid4()))
|
||
self.store.create_job(job)
|
||
self.store.mark_ready(job["job_id"])
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t4")
|
||
self.assertEqual(result["state"], "FAILED")
|
||
self.assertEqual(result["result_code"], "APPROVAL_MISSING")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T5: gültige Approval + gültiges Token erreicht Mutationsgrenze ------
|
||
def test_t5_valid_approval_reaches_mutation_boundary(self):
|
||
job, _ = self._make_approved_job()
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t5")
|
||
self.assertEqual(result["state"], "SUCCEEDED")
|
||
self.assertEqual(self.fake.delete_count, 1)
|
||
self.assertEqual(self.fake.delete_calls, [PATH])
|
||
|
||
# -- T6: kein echtes HTTP ------------------------------------------------
|
||
def test_t6_no_real_http(self):
|
||
client = TolariaClient(base_url="http://127.0.0.1:1", delete_token=None)
|
||
with self.assertRaises(TolariaDeleteError):
|
||
client.delete("x.md")
|
||
|
||
# -- T7: keine produktive DB ---------------------------------------------
|
||
def test_t7_no_production_db(self):
|
||
self.assertTrue(self.db_path.startswith(tempfile.gettempdir()) or "/tmp" in self.db_path)
|
||
|
||
# -- T8: kein produktiver Vault ------------------------------------------
|
||
def test_t8_no_production_vault(self):
|
||
self.assertIsInstance(self.fake, FakeTolaria)
|
||
|
||
# -- T9: DELETE ohne Approval -> BLOCKED ---------------------------------
|
||
def test_t9_delete_without_approval_blocked(self):
|
||
job = make_delete_job(
|
||
job_id=str(uuid.uuid4()), mission_id=MISSION, delete_request_id=DELETE_REQ,
|
||
object_id=OBJECT, vault_path=PATH, expected_commit=COMMIT,
|
||
expected_provenance_hash=PROV, approval_id=str(uuid.uuid4()),
|
||
created_at="2026-08-27T12:00:00Z", idempotency_key=str(uuid.uuid4()))
|
||
self.store.create_job(job)
|
||
self.store.mark_ready(job["job_id"])
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t9")
|
||
self.assertEqual(result["result_code"], "APPROVAL_MISSING")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T10: ungültige Signatur -> BLOCKED ----------------------------------
|
||
def test_t10_invalid_signature_blocked(self):
|
||
approval = make_approval()
|
||
approval["signature"] = "a" * 88 # kaputte Signatur
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t10")
|
||
self.assertEqual(result["result_code"], "APPROVAL_INVALID")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T11: abgelaufene Approval -> BLOCKED --------------------------------
|
||
def test_t11_expired_approval_blocked(self):
|
||
# Gültige Signatur, aber Store-Record markiert expired=True
|
||
# (Core-Guard isoliert: Verifier prüft Signatur, Core prüft Zustand).
|
||
approval = make_approval()
|
||
approval["expired"] = True
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t11")
|
||
self.assertEqual(result["result_code"], "APPROVAL_EXPIRED")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T12: consumed Approval -> BLOCKED -----------------------------------
|
||
def test_t12_consumed_approval_blocked(self):
|
||
approval = make_approval()
|
||
approval["consumed"] = True
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t12")
|
||
self.assertEqual(result["result_code"], "APPROVAL_CONSUMED")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T13: falsches Objekt -> BLOCKED -------------------------------------
|
||
def test_t13_wrong_object_blocked(self):
|
||
approval = make_approval(object_id=str(uuid.uuid4()))
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t13")
|
||
self.assertEqual(result["result_code"], "OBJECT_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T14: falscher Pfad -> BLOCKED ---------------------------------------
|
||
def test_t14_wrong_path_blocked(self):
|
||
approval = make_approval(vault_path="docs/other.md")
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t14")
|
||
self.assertEqual(result["result_code"], "PATH_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T15: falscher Commit -> BLOCKED -------------------------------------
|
||
def test_t15_wrong_commit_blocked(self):
|
||
approval = make_approval(expected_commit="a" * 40)
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t15")
|
||
self.assertEqual(result["result_code"], "COMMIT_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T16: falsche Provenance -> BLOCKED ----------------------------------
|
||
def test_t16_wrong_provenance_blocked(self):
|
||
approval = make_approval(expected_provenance_hash="b" * 64)
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t16")
|
||
self.assertEqual(result["result_code"], "PROVENANCE_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T17: falsche Mission -> BLOCKED -------------------------------------
|
||
def test_t17_wrong_mission_blocked(self):
|
||
approval = make_approval(mission_id=str(uuid.uuid4()))
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t17")
|
||
self.assertEqual(result["result_code"], "MISSION_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T18: ohne DELETE-Token -> fail-closed -------------------------------
|
||
def test_t18_no_delete_token_fail_closed(self):
|
||
client = TolariaClient(base_url="http://fake", delete_token=None)
|
||
with self.assertRaises(TolariaDeleteError) as ctx:
|
||
client.delete(PATH)
|
||
self.assertEqual(ctx.exception.code, "CREDENTIAL_MISSING")
|
||
|
||
# -- T19: falsches Token (401) -> fail-closed ----------------------------
|
||
def test_t19_wrong_token_auth_failure(self):
|
||
self.fake.fail_next = ("error", "AUTH_FAILURE", False)
|
||
job, _ = self._make_approved_job()
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t19")
|
||
self.assertEqual(result["state"], "FAILED")
|
||
self.assertEqual(result["result_code"], "AUTH_FAILURE")
|
||
|
||
# -- T20: Timeout -> OUTCOME_UNKNOWN (kein blinder Retry) ----------------
|
||
def test_t20_timeout_outcome_unknown(self):
|
||
self.fake.fail_next = ("error", "TOLARIA_UNAVAILABLE", True)
|
||
job, _ = self._make_approved_job()
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t20")
|
||
self.assertEqual(result["state"], "OUTCOME_UNKNOWN")
|
||
|
||
# -- T21: Job allein autorisiert NIE -------------------------------------
|
||
def test_t21_job_alone_never_authorizes(self):
|
||
job = make_delete_job(
|
||
job_id=str(uuid.uuid4()), mission_id=MISSION, delete_request_id=DELETE_REQ,
|
||
object_id=OBJECT, vault_path=PATH, expected_commit=COMMIT,
|
||
expected_provenance_hash=PROV, approval_id=str(uuid.uuid4()),
|
||
created_at="2026-08-27T12:00:00Z", idempotency_key=str(uuid.uuid4()))
|
||
self.store.create_job(job)
|
||
self.store.mark_ready(job["job_id"])
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t21")
|
||
self.assertEqual(result["result_code"], "APPROVAL_MISSING")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T22: SAVE-Job wird vom DELETE-Worker REJECTED -----------------------
|
||
def test_t22_save_job_rejected_by_delete_worker(self):
|
||
job = make_save_job(
|
||
job_id=str(uuid.uuid4()), mission_id=MISSION, object_id=OBJECT,
|
||
vault_path=PATH, source_commit=COMMIT, provenance_hash=PROV,
|
||
expected_state="x", created_at="2026-08-27T12:00:00Z",
|
||
idempotency_key=str(uuid.uuid4()))
|
||
self.store.create_job(job)
|
||
self.store.mark_ready(job["job_id"])
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t22")
|
||
self.assertEqual(result["state"], "REJECTED")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T23: zweiter DELETE auf gleiche Approval -> single-use --------------
|
||
def test_t23_second_delete_single_use(self):
|
||
job, approval = self._make_approved_job()
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t23a")
|
||
self.assertEqual(result["state"], "SUCCEEDED")
|
||
self.assertEqual(self.fake.delete_count, 1)
|
||
# Zweiter Job mit derselben Approval (consumed) -> BLOCKED
|
||
# Neue Approval-ID, aber dieselbe Nonce (Replay) + consumed=True
|
||
approval2 = make_approval(nonce=approval["nonce"])
|
||
approval2["consumed"] = True
|
||
job2, _ = self._make_approved_job(approval2)
|
||
result2 = core.process_job(job2["job_id"], "w-t23b")
|
||
self.assertEqual(result2["result_code"], "APPROVAL_CONSUMED")
|
||
self.assertEqual(self.fake.delete_count, 1)
|
||
|
||
# -- T24: Replay (Nonce wiederverwendet) -> BLOCKED ----------------------
|
||
def test_t24_replay_nonce_blocked(self):
|
||
approval = make_approval()
|
||
approval["used_nonces"] = {approval["nonce"]} # Nonce bereits verwendet
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t24")
|
||
self.assertEqual(result["result_code"], "APPROVAL_CONSUMED")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T25: Approval für anderes Objekt autorisiert NIE --------------------
|
||
def test_t25_approval_other_object_never_authorizes(self):
|
||
approval = make_approval(object_id=str(uuid.uuid4()))
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t25")
|
||
self.assertEqual(result["result_code"], "OBJECT_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T26: Approval für anderen Commit autorisiert NIE --------------------
|
||
def test_t26_approval_other_commit_never_authorizes(self):
|
||
approval = make_approval(expected_commit="a" * 40)
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t26")
|
||
self.assertEqual(result["result_code"], "COMMIT_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T27: Approval für anderen Pfad autorisiert NIE ----------------------
|
||
def test_t27_approval_other_path_never_authorizes(self):
|
||
approval = make_approval(vault_path="docs/other.md")
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t27")
|
||
self.assertEqual(result["result_code"], "PATH_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T28: Approval für andere Mission autorisiert NIE -------------------
|
||
def test_t28_approval_other_mission_never_authorizes(self):
|
||
approval = make_approval(mission_id=str(uuid.uuid4()))
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t28")
|
||
self.assertEqual(result["result_code"], "MISSION_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T29: Approval für anderen Request autorisiert NIE -------------------
|
||
def test_t29_approval_other_request_never_authorizes(self):
|
||
approval = make_approval(delete_request_id=str(uuid.uuid4()))
|
||
job, _ = self._make_approved_job(approval)
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t29")
|
||
self.assertEqual(result["result_code"], "APPROVAL_MISMATCH")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T30: kein generic dispatcher ----------------------------------------
|
||
def test_t30_no_generic_dispatcher(self):
|
||
from job_schema import FORBIDDEN_FIELDS
|
||
for f in ("command", "endpoint", "url", "method", "shell", "python",
|
||
"handler", "exec", "script", "headers", "authorization",
|
||
"credential", "token", "password", "secret", "sql", "query",
|
||
"docker", "ssh", "base_url", "http_method", "auth_header"):
|
||
self.assertIn(f, FORBIDDEN_FIELDS)
|
||
|
||
# -- T31: kein shell/python im Job ---------------------------------------
|
||
def test_t31_no_shell_python_in_job(self):
|
||
from job_schema import FORBIDDEN_FIELDS
|
||
self.assertIn("shell", FORBIDDEN_FIELDS)
|
||
self.assertIn("python", FORBIDDEN_FIELDS)
|
||
|
||
# -- T32: kein arbitrary endpoint (fester /delete) -----------------------
|
||
def test_t32_fixed_delete_endpoint(self):
|
||
from delete_tolaria_client import ENDPOINT_DELETE
|
||
self.assertEqual(ENDPOINT_DELETE, "delete")
|
||
|
||
# -- T33: kein arbitrary method (nur POST) ------------------------------
|
||
def test_t33_fixed_method_post(self):
|
||
import delete_tolaria_client
|
||
src = open(delete_tolaria_client.__file__).read()
|
||
self.assertIn('method="POST"', src)
|
||
self.assertNotIn('method="DELETE"', src)
|
||
|
||
# -- T34: kein arbitrary header ------------------------------------------
|
||
def test_t34_no_arbitrary_header(self):
|
||
import delete_tolaria_client
|
||
src = open(delete_tolaria_client.__file__).read()
|
||
# Nur der Code (nicht Docstring): kein dynamischer headers-Parameter
|
||
self.assertNotIn("def _post(self, endpoint, payload, auth_token, headers", src)
|
||
self.assertNotIn("headers: Dict[str, str] = None", src)
|
||
|
||
# -- T35: kein SAVE fallback ---------------------------------------------
|
||
def test_t35_no_save_fallback(self):
|
||
import delete_tolaria_client
|
||
src = open(delete_tolaria_client.__file__).read()
|
||
self.assertNotIn("ENDPOINT_SAVE", src)
|
||
self.assertNotIn("def write", src)
|
||
|
||
# -- T36: kein Docker-Socket / SSH im Executor ---------------------------
|
||
def test_t36_no_docker_socket_ssh(self):
|
||
import delete_worker
|
||
src = open(delete_worker.__file__).read()
|
||
self.assertNotIn("docker", src.lower())
|
||
self.assertNotIn("ssh", src.lower())
|
||
|
||
# -- T37: kein Private Key im Executor -----------------------------------
|
||
def test_t37_no_private_key_in_executor(self):
|
||
import delete_worker
|
||
src = open(delete_worker.__file__).read()
|
||
self.assertNotIn("private_key", src.lower())
|
||
self.assertNotIn("PRIVATE", src)
|
||
|
||
# -- T38: kein SAVE-Token im DELETE Executor -----------------------------
|
||
def test_t38_no_save_token_in_delete_executor(self):
|
||
import delete_worker
|
||
src = open(delete_worker.__file__).read()
|
||
self.assertNotIn("TOLARIA_SAVE_TOKEN", src)
|
||
self.assertIn("TOLARIA_DELETE_TOKEN", src)
|
||
|
||
# -- T39: kein DELETE-Token im SAVE Executor -----------------------------
|
||
def test_t39_no_delete_token_in_save_executor(self):
|
||
sot_worker = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||
"..", "sot_worker.py")
|
||
if os.path.exists(sot_worker):
|
||
src = open(sot_worker).read()
|
||
self.assertNotIn("TOLARIA_DELETE_TOKEN", src)
|
||
|
||
# -- T40: OUTCOME_UNKNOWN -> kein blinder Retry --------------------------
|
||
def test_t40_outcome_unknown_no_blind_retry(self):
|
||
self.fake.fail_next = ("error", "TOLARIA_UNAVAILABLE", True)
|
||
job, _ = self._make_approved_job()
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result = core.process_job(job["job_id"], "w-t40")
|
||
self.assertEqual(result["state"], "OUTCOME_UNKNOWN")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
# -- T41: zweiter process_job auf denselben Job -> Claim-Guard ----------
|
||
# K: second-delete guard. Der atomare Claim (READY->CLAIMED) verhindert,
|
||
# dass derselbe DELETE-Job zweimal ausgeführt wird (kein Doppel-Delete).
|
||
def test_t41_second_process_same_job_blocked(self):
|
||
job, _ = self._make_approved_job()
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
result1 = core.process_job(job["job_id"], "w-t41a")
|
||
self.assertEqual(result1["state"], "SUCCEEDED")
|
||
self.assertEqual(self.fake.delete_count, 1)
|
||
# Zweiter process_job auf denselben Job: Claim-Guard greift
|
||
# (JobAlreadyClaimedError -> kein zweiter DELETE).
|
||
result2 = core.process_job(job["job_id"], "w-t41b")
|
||
self.assertEqual(self.fake.delete_count, 1) # kein zweiter DELETE
|
||
|
||
# -- T42: TOCTOU — Payload-Manipulation nach Claim wird erkannt ----------
|
||
# M: immutable guard. Der echte TOCTOU-Schutz ist _assert_immutable_fields
|
||
# in _transition (job_store.py), das die rohe DB-Spalte gegen das Payload
|
||
# prüft. Eine Payload-Manipulation nach dem Claim (z.B. object_id) führt zu
|
||
# JobImmutableFieldError -> kein State-Change, kein DELETE.
|
||
def test_t42_toctou_re_read_after_claim(self):
|
||
job, _ = self._make_approved_job()
|
||
core = self._core()
|
||
core.tolaria_delete = self.fake.delete
|
||
real_begin = self.store.begin_execution
|
||
def begin_then_tamper(job_id, worker_id):
|
||
result = real_begin(job_id, worker_id)
|
||
# Nach begin_execution das Payload-JSON manipulieren (TOCTOU):
|
||
# object_id im Payload ändern, damit der immutable guard den Drift
|
||
# erkennt (rohe DB-Spalte vs. Payload).
|
||
import json
|
||
row = self.store._conn.execute(
|
||
"SELECT payload FROM jobs WHERE job_id = ?", (job_id,)
|
||
).fetchone()
|
||
payload = json.loads(row["payload"])
|
||
payload["object_id"] = "tampered-object"
|
||
self.store._conn.execute(
|
||
"UPDATE jobs SET payload = ? WHERE job_id = ?",
|
||
(json.dumps(payload), job_id))
|
||
self.store._conn.commit()
|
||
return result
|
||
self.store.begin_execution = begin_then_tamper
|
||
# Der immutable guard erkennt den Drift und wirft JobImmutableFieldError
|
||
# (kein State-Change, kein DELETE).
|
||
with self.assertRaises(JobImmutableFieldError):
|
||
core.process_job(job["job_id"], "w-t42")
|
||
self.assertEqual(self.fake.delete_count, 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|