"""access-engine (flex-auth) Check client — step 2 of GH-DEC-2026-003. This engine consumes a decision; it never renders one. The DecisionEnvelope returned here is validated by ``authorization.validate_decision_envelope`` against the exact proposed action before it can satisfy the production stance. No estate-wide PDP exists by design: flex-auth runs per-consumer cluster-local pins, so the address is per-deployment configuration and its absence fails production closed. """ from __future__ import annotations import json from pathlib import Path from typing import Any, Callable from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen from secrets_engine.errors import DecisionError from secrets_engine.openbao import read_strict_token_file _MAX_BODY = 512 * 1024 def _status_message(status: int) -> str: if status in (401, 403): return "access-engine refused the caller" if status == 404: return "access-engine has no such endpoint" if status == 503: return "access-engine is unavailable" return f"access-engine returned HTTP {status}" def check_decision( *, base_url: str, token_file: Path, request: dict[str, Any], timeout_seconds: float = 3, opener: Callable[..., Any] = urlopen, ) -> dict[str, Any]: """POST /v1/check. Any non-200, non-JSON, or transport failure fails closed. Silence is never permission: an unreachable PDP raises rather than returning a permissive default. """ if not base_url or not base_url.startswith(("http://", "https://")): raise DecisionError("access-engine check URL is missing or invalid") token = read_strict_token_file(Path(token_file), purpose="access-engine credential") encoded = json.dumps(request).encode("utf-8") http_request = Request( base_url.rstrip("/") + "/v1/check", data=encoded, method="POST", ) http_request.add_header("Authorization", f"Bearer {token}") http_request.add_header("Content-Type", "application/json") http_request.add_header("Accept", "application/json") try: with opener(http_request, timeout=timeout_seconds) as response: if getattr(response, "status", 200) != 200: raise DecisionError("access-engine check did not return a decision") payload = json.loads(response.read(_MAX_BODY).decode("utf-8")) except HTTPError as e: raise DecisionError(f"access-engine check refused: {_status_message(e.code)}") from e except URLError as e: raise DecisionError("access-engine is unreachable for check") from e except json.JSONDecodeError as e: raise DecisionError("access-engine check returned a non-JSON body") from e if not isinstance(payload, dict): raise DecisionError("access-engine check returned a non-object body") return payload