"""Fail-closed authentication and policy checks for HUX-12 evidence. The evidence producer is a loopback-only companion process. It receives a dedicated Vault key and a non-secret policy file; neither the browser relay nor the agent worker can use its trust class. This module deliberately performs no network discovery: the producer supplies observations and this boundary binds them to the configured Jenkins, Harbor, Flux, workload, pod and health facts. """ from __future__ import annotations import json import os import re import stat from collections.abc import Mapping from pathlib import Path from typing import Any from urllib.parse import urlsplit from hux.errors import Forbidden, Invalid KEY_FILE_ENV = "HUX_RELEASE_EVIDENCE_KEY_FILE" POLICY_FILE_ENV = "HUX_RELEASE_EVIDENCE_POLICY_FILE" MAX_KEY_BYTES = 4096 MAX_POLICY_BYTES = 64 * 1024 POLICY_FIELDS = {"schema", "max_evidence_age_seconds", "workloads"} WORKLOAD_FIELDS = { "review_url_prefix", "jenkins_job_url", "image_repository", "flux_kustomization", "health_url", } def _regular_file(path: str, modes: set[int], limit: int) -> bytes: """Read one bounded regular file without accepting a symlink or loose mode.""" if not path: raise Invalid("HUX-12 evidence configuration is missing") flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) descriptor = -1 try: descriptor = os.open(Path(path), flags) info = os.fstat(descriptor) if not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) not in modes: raise Invalid("HUX-12 evidence file permissions are invalid") data = os.read(descriptor, limit + 1) except OSError as error: raise Invalid("HUX-12 evidence configuration is unavailable") from error finally: if descriptor >= 0: os.close(descriptor) if not data or len(data) > limit: raise Invalid("HUX-12 evidence file is empty or too large") return data def evidence_key(environ: Mapping[str, str]) -> str: """Return the file-only producer key; inline environment secrets are ignored.""" data = _regular_file(environ.get(KEY_FILE_ENV, ""), {0o400}, MAX_KEY_BYTES) try: key = data.decode("utf-8", errors="strict").strip() except UnicodeDecodeError as error: raise Invalid("HUX-12 evidence key is invalid") from error if len(key) < 32: raise Invalid("HUX-12 evidence key is too short") return key def _https_url(value: Any, field: str, *, prefix: bool = False) -> str: if not isinstance(value, str) or not value or len(value) > 500: raise Invalid(f"{field} is missing or too long") parsed = urlsplit(value) if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment: raise Invalid(f"{field} must be a credential-free HTTPS URL") if prefix and not value.endswith("/"): raise Invalid(f"{field} must end with /") return value def load_policy(environ: Mapping[str, str]) -> dict[str, Any]: """Load and strictly validate the non-secret producer policy.""" raw = _regular_file(environ.get(POLICY_FILE_ENV, ""), {0o400, 0o440, 0o444}, MAX_POLICY_BYTES) try: policy = json.loads(raw) except (UnicodeDecodeError, json.JSONDecodeError) as error: raise Invalid("HUX-12 evidence policy is not valid JSON") from error if not isinstance(policy, dict) or set(policy) != POLICY_FIELDS or policy.get("schema") != "hux.release_evidence_policy.v1": raise Invalid("HUX-12 evidence policy shape is invalid") age = policy.get("max_evidence_age_seconds") workloads = policy.get("workloads") if not isinstance(age, int) or isinstance(age, bool) or not 60 <= age <= 86400: raise Invalid("HUX-12 evidence age bound is invalid") if not isinstance(workloads, dict) or not workloads or len(workloads) > 16: raise Invalid("HUX-12 workload policy is invalid") for name, item in workloads.items(): if not isinstance(name, str) or not isinstance(item, dict) or set(item) != WORKLOAD_FIELDS: raise Invalid("HUX-12 workload policy shape is invalid") _https_url(item["review_url_prefix"], "review_url_prefix", prefix=True) _https_url(item["jenkins_job_url"], "jenkins_job_url") _https_url(item["health_url"], "health_url") if not re.fullmatch(r"[a-z0-9./_-]+", str(item["image_repository"])): raise Invalid("HUX-12 image repository is invalid") if not re.fullmatch(r"[a-z0-9-]{1,63}", str(item["flux_kustomization"])): raise Invalid("HUX-12 Flux kustomization is invalid") return policy def configured(environ: Mapping[str, str]) -> bool: """Whether producer authentication and policy are both healthy.""" try: evidence_key(environ) load_policy(environ) except Invalid: return False return True def workload_policy(environ: Mapping[str, str], workload: str) -> dict[str, Any]: """Return the exact allowlist for a workload or fail closed.""" item = load_policy(environ)["workloads"].get(workload) if item is None: raise Invalid("workload is not enabled for release evidence") return item def require_review_creator(trust: str) -> None: """Only the authenticated browser path may record the human review decision.""" if trust != "router": raise Forbidden("reviewed releases require router trust") def require_evidence_producer(trust: str) -> None: """Only the dedicated producer may advance a reviewed release.""" if trust != "evidence": raise Forbidden("release transitions require evidence trust")