feat: add workload-authenticated account-authority HTTP lookup (USER-WP-0037-T03 partial)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 57s
Account journey acceptance / journeys (push) Successful in 10s

Exposes POST /api/v1/platform/account-authority/lookup over the existing
lookup_account_authority service method, gated by a dedicated Hub workload
bearer distinct from end-user OIDC claims and the trusted proxy secret. The
route is absent unless USER_ENGINE_HUB_AUTHORITY_TOKEN is configured.

T02 (root-grant custody/mapping) and the remainder of T03 (real workload
credential admission) depend on decisions outside this repo, per
credential-routing.md and NetKingdom's root-custody canon. Requested via
State Hub messages to net-kingdom and state-hub; workplan set to blocked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 227621@bnt-lap001
Assistant-Session: ffcc35e9-8ffa-4881-a473-02ca94d6b366
This commit is contained in:
tegwick 2026-09-28 20:20:07 +02:00
parent e82ace9b4d
commit 2252b4ca6d
4 changed files with 238 additions and 3 deletions

View file

@ -92,6 +92,8 @@ class PortalApplication:
registration_rate_limit: int = 10,
registration_rate_window_seconds: int = 60,
identity_lookup: Any | None = None,
hub_authority_caller: Actor | None = None,
hub_authority_token: str = "",
) -> None:
if len(trusted_proxy_secret) < 24:
raise ValueError("trusted proxy secret must contain at least 24 characters")
@ -128,6 +130,12 @@ class PortalApplication:
self._registration_attempts: dict[str, deque[float]] = {}
self._registration_attempts_lock = Lock()
self.identity_lookup = identity_lookup
if hub_authority_token and len(hub_authority_token) < 24:
raise ValueError("hub authority token must contain at least 24 characters")
if bool(hub_authority_caller) != bool(hub_authority_token):
raise ValueError("hub authority lookup needs both a caller and a token, or neither")
self.hub_authority_caller = hub_authority_caller
self.hub_authority_token = hub_authority_token
def __call__(self, environ: Mapping[str, Any], start_response: StartResponse) -> Iterable[bytes]:
correlation_id = environ.get("HTTP_X_REQUEST_ID") or f"corr_{secrets.token_hex(12)}"
@ -397,6 +405,9 @@ class PortalApplication:
environ, start_response, correlation_id
)
if path == "/api/v1/platform/account-authority/lookup" and method == "POST":
return self._account_authority_lookup(environ, start_response, correlation_id)
actor = self._actor(environ)
self._set_account_navigation(environ, actor)
privileged = path == "/platform" or path.startswith(("/platform/", "/admin/", "/api/v1/platform/", "/api/v1/tenants/"))
@ -1401,6 +1412,31 @@ class PortalApplication:
def _actor(self, environ: Mapping[str, Any]) -> Any:
return self.service.identity_adapter.normalize(self._claims(environ))
def _account_authority_lookup(
self, environ: Mapping[str, Any], start_response: StartResponse, correlation_id: str,
) -> Iterable[bytes]:
"""Independently authenticated workload read; never end-user claims.
The caller identity is the fixed configured Hub workload, not
anything forwarded from a human bearer or verified OIDC claims.
"""
if self.hub_authority_caller is None or not self.hub_authority_token:
raise NotFoundError("account authority lookup is unavailable")
header = str(environ.get("HTTP_AUTHORIZATION", ""))
prefix = "Bearer "
supplied = header[len(prefix):] if header.startswith(prefix) else ""
if not supplied or not secrets.compare_digest(supplied, self.hub_authority_token):
raise AuthorizationDenied("workload authentication required")
body = self._body(environ)
result = self.service.lookup_account_authority(
self.hub_authority_caller,
issuer=str(body.get("issuer") or ""),
subject=str(body.get("subject") or ""),
tenant=str(body.get("tenant") or ""),
correlation_id=correlation_id,
)
return self._json(start_response, "200 OK", _jsonable(result), correlation_id)
def _start_public_registration(
self,
environ: Mapping[str, Any],