145 lines
4.3 KiB
Python
145 lines
4.3 KiB
Python
"""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()
|