trading-system-docs/tolaria/c5-sync-service/delete_tolaria_client.py
Rain Ocampo 9c2d6e365a AUTH.4D: DELETE-scoped executor + delete_request_id binding in SoT core
- delete_worker.py: DELETE-scoped worker loop (ApprovalStateStore, fail-closed)
- delete_tolaria_client.py: DELETE-only Tolaria client (fixed /delete endpoint)
- delete_entrypoint.py: DELETE runtime entrypoint (health/status + worker)
- delete_Dockerfile: DELETE executor image (no SAVE code, no credentials)
- delete_executor_core.py: minimal SoT patch — VALID_NONCE replay guard +
  VALID_DELETE_REQUEST binding (approval binds delete_request_id, fail-closed
  APPROVAL_MISMATCH). Closes P10/T29 security gap in productive SoT.

AUTH.4D P14. No deployment, no token injection, no key provisioning.
2026-08-28 02:28:48 +00:00

156 lines
6.1 KiB
Python

"""
AUTH.4D — tolaria_client.py (DELETE-scoped)
===========================================
Isolierter Tolaria-Client für den DELETE-Executor (DELETE-only).
Eigenschaften:
* FIXED Base URL (aus ENV C5_TOLARIA_BASE oder Default http://tolaria:5173/api/vault)
— NIE aus dem Job. Keine arbitrary URL.
* DELETE Endpoint fest: /delete. Read-Back fest: /content.
* Keine arbitrary headers/method.
* Fail-closed: kein HTTP ohne DELETE-Credential (TOLARIA_DELETE_TOKEN).
* OUTCOME_UNKNOWN bei unklarem HTTP-Ergebnis (kein blinder Retry).
* Token-Wert wird NIE geloggt.
Nur DELETE-Scope. Kein SAVE. Kein Master-Token. Keine write()-Capability.
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.request
from typing import Any, Dict, Optional
# FIXED Tolaria Base URL (produktiv: interne Docker-DNS-Adresse)
DEFAULT_TOLARIA_BASE = "http://tolaria:5173/api/vault"
ENV_TOLARIA_BASE = "C5_TOLARIA_BASE"
ENV_TOLARIA_DELETE_TOKEN = "TOLARIA_DELETE_TOKEN"
# Feste Endpoints (keine arbitrary URL/method)
ENDPOINT_DELETE = "delete"
ENDPOINT_CONTENT = "content"
# Eindeutige, enge Not-Found-Semantik der Tolaria-Vault-API
TOLARIA_NOT_FOUND_MSG = "Invalid or missing path"
class TolariaClientError(Exception):
pass
class TolariaUnavailableError(TolariaClientError):
pass
class TolariaDeleteError(TolariaClientError):
def __init__(self, message: str, code: str, http_code: Optional[int] = None):
super().__init__(message)
self.code = code
self.http_code = http_code
class TolariaClient:
"""
Isolierter Tolaria-Client (DELETE-only).
read() — POST /content (Read-Back / Verifikation)
delete() — POST /delete (NUR DELETE; fail-closed ohne Credential)
"""
def __init__(self, base_url: Optional[str] = None, timeout: float = 15.0,
delete_token: Optional[str] = None):
# FIXED Base URL: aus ENV oder Default. NIE aus dem Job.
self.base_url = (base_url or os.environ.get(ENV_TOLARIA_BASE)
or DEFAULT_TOLARIA_BASE).rstrip("/")
self.timeout = timeout
# DELETE-Credential explizit injiziert (kein verstecktes globales).
# Fehlend/leer -> fail-closed beim mutierenden Aufruf.
self.delete_token = delete_token
# -- HTTP-Helfer --------------------------------------------------------
def _post(self, endpoint: str, payload: Dict[str, Any],
auth_token: Optional[str] = None) -> Dict[str, Any]:
url = f"{self.base_url}/{endpoint.lstrip('/')}"
data = json.dumps(payload).encode("utf-8")
headers: Dict[str, str] = {"Content-Type": "application/json"}
# Authorization-Header NUR wenn ein Token explizit übergeben wird
# (mutierender DELETE). READ sendet KEIN Credential (Least Privilege).
if auth_token is not None:
headers["Authorization"] = f"Bearer {auth_token}"
req = urllib.request.Request(url, data=data, headers=headers,
method="POST")
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
body = resp.read().decode("utf-8")
return json.loads(body) if body else {}
except urllib.error.HTTPError as e:
if e.code >= 500:
raise TolariaUnavailableError(
f"Tolaria HTTP {e.code} auf {endpoint}",
"TOLARIA_UNAVAILABLE")
raise TolariaDeleteError(
f"Tolaria HTTP {e.code} auf {endpoint}: "
f"{e.read().decode('utf-8', 'replace')[:200]}",
"AUTH_FAILURE" if e.code in (401, 403) else "INVALID_SCHEMA",
http_code=e.code)
except (urllib.error.URLError, TimeoutError, OSError) as e:
raise TolariaUnavailableError(
f"Tolaria nicht erreichbar ({endpoint}): {e}",
"TOLARIA_UNAVAILABLE")
# -- Read ---------------------------------------------------------------
def read(self, vault_path: str) -> Optional[str]:
"""Read-Back eines Vault-Objekts. Gibt Inhalt oder None (nicht vorhanden).
vault_path ist der KANONISCHE RELATIVE Pfad (AUTH.4C2 PATH CONTRACT
REPAIR, OPTION A) — wird unverändert an Tolaria gesendet. Kein
/app/vault/-Prefix-Stripping, keine versteckte Rewrite-Logik.
"""
try:
resp = self._post(ENDPOINT_CONTENT, {"path": vault_path})
except TolariaDeleteError as e:
# Enger Not-Found-Fall: HTTP 400 + eindeutige 'Invalid or missing
# path'-Semantik -> Objekt existiert nicht -> None (kein Fehler).
if e.http_code == 400 and TOLARIA_NOT_FOUND_MSG in (e.args[0] or ""):
return None
raise
if "error" in resp:
return None
return resp.get("content")
# -- Delete (NUR DELETE) ------------------------------------------------
def _require_token(self) -> str:
"""Fail-closed: fehlendes/leeres DELETE-Credential -> kein HTTP."""
token = self.delete_token
if not token or not isinstance(token, str) or not token.strip():
raise TolariaDeleteError(
"Tolaria DELETE-Credential fehlt oder ist leer "
"(fail-closed, kein Request gesendet)",
"CREDENTIAL_MISSING")
return token
def delete(self, vault_path: str) -> Dict[str, Any]:
"""Löscht ein Vault-Objekt (POST /delete). DELETE-Scope.
vault_path ist der KANONISCHE RELATIVE Pfad (AUTH.4C2 PATH CONTRACT
REPAIR, OPTION A) — wird unverändert an Tolaria gesendet. Kein
/app/vault/-Prefix-Stripping, keine versteckte Rewrite-Logik.
Fail-closed: fehlendes/leeres DELETE-Credential -> lokaler Abbruch,
HTTP wird NICHT aufgerufen.
"""
token = self._require_token()
resp = self._post(ENDPOINT_DELETE, {"path": vault_path},
auth_token=token)
if resp is None:
resp = {}
if "error" in resp:
raise TolariaDeleteError(
f"Tolaria delete fehlgeschlagen: {resp['error']}",
"INVALID_SCHEMA")
return resp