"""HTTP adapter for audited Owner authorization and writer provisioning. This module layers on top of ``aios_authorized_task_writer_api`` so existing routes and the legacy exact ``writer:provision`` route remain available. The new endpoint does not mint or modify bearer credentials. It authenticates an existing, non-wildcard, explicitly allow-listed Owner credential and then invokes the audited broker. Endpoint: POST /api/governance/owner-authorizations """ from __future__ import annotations import os import urllib.parse from typing import Any, Mapping import aios_authorized_task_writer_api as bridge_api from authorized_task_writer_provisioning import ProvisioningError from owner_authorization_broker import ( OwnerAuthorizationBroker, OwnerAuthorizationError, OwnerPrincipal, ) api = bridge_api.api _original_do_post = api.H.do_POST _ENDPOINT = "/api/governance/owner-authorizations" def _enabled() -> bool: return os.getenv( "AIOS_GITOPS_OWNER_AUTHORIZATION_BROKER_ENABLED", "" ).strip().lower() in {"1", "true", "yes", "on"} def _configured_intake_scope() -> str: scopes = [ item.strip() for item in os.getenv( "AIOS_GITOPS_OWNER_AUTHORIZATION_INTAKE_SCOPES", "" ).split(",") if item.strip() ] if len(scopes) != 1: raise OwnerAuthorizationError( "EXACTLY_ONE_HTTP_INTAKE_SCOPE_REQUIRED", 503 ) scope = scopes[0] if scope == "writer:provision" or scope == "*" or scope.endswith(":*"): raise OwnerAuthorizationError("HTTP_INTAKE_SCOPE_REJECTED", 503) return scope def _scope_set(auth: Mapping[str, Any]) -> frozenset[str]: raw = auth.get("scopes") if raw is None: raw = auth.get("scope") if raw is None: return frozenset() if isinstance(raw, str): candidates = raw.replace(",", " ").split() elif isinstance(raw, (list, tuple, set, frozenset)): candidates = [str(item) for item in raw] else: raise OwnerAuthorizationError("OWNER_SCOPE_EVIDENCE_INVALID", 403) return frozenset(item.strip() for item in candidates if item.strip()) def _credential_id(auth: Mapping[str, Any]) -> str: for name in ( "credential_id", "token_id", "key_id", "auth_id", ): value = str(auth.get(name) or "").strip() if value: return value return "" def _authenticate_existing_owner(handler) -> OwnerPrincipal | None: scope = _configured_intake_scope() auth = handler.need(scope) if not auth: return None if auth.get("role") != "owner": handler.sendj({"error": "OWNER_ROLE_REQUIRED"}, 403) return None identity = str(auth.get("account") or auth.get("identity") or "").strip() if not identity: handler.sendj({"error": "OWNER_IDENTITY_REQUIRED"}, 403) return None scopes = _scope_set(auth) if any(item == "*" or item.endswith(":*") for item in scopes): handler.sendj( {"error": "WILDCARD_OWNER_CREDENTIAL_REJECTED"}, 403 ) return None return OwnerPrincipal( identity=identity, role="owner", authenticated_scope=scope, token_scopes=scopes, credential_id=_credential_id(auth), ) def _broker_do_post(self): path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/" if path != _ENDPOINT: return _original_do_post(self) if not _enabled(): return self.sendj( {"error": "OWNER_AUTHORIZATION_BROKER_DISABLED"}, 503 ) try: principal = _authenticate_existing_owner(self) if principal is None: return data = self.body() result = OwnerAuthorizationBroker().authorize_and_provision( data, principal ) return self.sendj(result) except (OwnerAuthorizationError, ProvisioningError) as exc: body = {"error": exc.code} if exc.details: body["details"] = exc.details return self.sendj(body, exc.status) except Exception: # Never expose registry data, bearer tokens, signer output or traceback. return self.sendj( {"error": "OWNER_AUTHORIZATION_BROKER_INTERNAL_ERROR"}, 500 ) api.H.do_POST = _broker_do_post if __name__ == "__main__": api.main()