"""Authenticated get-by-id and human entry transport; no policy or consume API. The caller owes entitlement, durable presentation/disposition and audit custody before invoking add_entry. This adapter is deliberately not a browser route. """ from __future__ import annotations from datetime import datetime import re import time from .approval_client import ApprovalEngineError, EntryResult, is_success from .http_transport import JSONTransport, TransportError, fixed_origin from .oidc import HumanSession from .provenance import assert_human_control_dischargeable class ApprovalHTTPClient: def __init__(self, origin: str, session: HumanSession, *, transport=None, clock=time.time, allow_internal_http=False): self.origin = fixed_origin(origin, allow_internal_http=allow_internal_http) assert_human_control_dischargeable(session.principal_type) if session.tenant.value != "tenant:platform": raise ValueError("platform approver required") self.session = session self.transport = transport or JSONTransport(allow_internal_http=allow_internal_http) self.clock = clock def _call(self, method: str, approval_id: str, suffix="") -> dict: if not isinstance(approval_id, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}", approval_id): raise ValueError("invalid approval id") if self.session.expires_at <= self.clock(): raise ApprovalEngineError(401, "session_expired") try: status, data = self.transport.request(method, self.origin + "/v1/approvals/" + approval_id + suffix, headers={"Authorization": "Bearer " + self.session.access_token, "Content-Type": "application/json"}, body=b"{}" if method == "POST" else None) except TransportError: # A failed POST transport may already have committed. Never retry it # automatically or manufacture a disposition from this uncertainty. raise ApprovalEngineError(502, "upstream_unavailable") from None if status != 200: reason = data.get("error") known = {"unauthenticated", "forbidden", "not_found", "conflict", "duplicate_approver", "unprocessable", "store_unavailable"} raise ApprovalEngineError(status, reason if isinstance(reason, str) and reason in known else "upstream_refused") binding = data.get("binding") if (data.get("id") != approval_id or not isinstance(binding, dict) or binding.get("human_control") is not True or not isinstance(binding.get("digest"), str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", binding["digest"]) or data.get("status") not in {"requested", "approved", "consumed", "expired", "revoked", "superseded"}): raise ApprovalEngineError(502, "invalid_approval_response") return data def get_approval(self, approval_id: str) -> dict: return self._call("GET", approval_id) def add_entry(self, approval_id: str, *, expected_binding_digest=None, before_post=None) -> EntryResult: current = self.get_approval(approval_id) if expected_binding_digest is not None and current["binding"]["digest"] != expected_binding_digest: raise ApprovalEngineError(409, "binding_changed") if before_post is not None: before_post() # Recheck session/policy and reserve AFTER the last read. duplicate = False try: data = self._call("POST", approval_id, "/entries") except ApprovalEngineError as exc: if not is_success(exc): raise duplicate = True data = self.get_approval(approval_id) entries = data.get("entries") if not isinstance(entries, list) or any(not isinstance(e, dict) for e in entries): raise ApprovalEngineError(502, "entry_correlation_missing") matches = [e for e in entries if e.get("subject_id") == self.session.subject] try: if len(matches) != 1 or matches[0].get("principal_type") != "human": raise ValueError() at = matches[0]["approved_at"] if not isinstance(at, str) or datetime.fromisoformat(at.replace("Z", "+00:00")).tzinfo is None: raise ValueError() except (KeyError, TypeError, ValueError): raise ApprovalEngineError(502, "entry_correlation_missing") from None return EntryResult(approval_id, self.session.subject, at, data["status"], duplicate)