feat: add workload-authenticated account-authority HTTP lookup (USER-WP-0037-T03 partial)
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:
parent
e82ace9b4d
commit
2252b4ca6d
4 changed files with 238 additions and 3 deletions
|
|
@ -14,6 +14,7 @@ from user_engine.adapters import (
|
|||
HTTPIdentityProvisioningAdapter,
|
||||
HTTPTenantManagementAdapter,
|
||||
)
|
||||
from user_engine.domain import Actor, PrincipalType
|
||||
from user_engine.service import UserEngineService
|
||||
from user_engine.identity_state import AutheliaIdentityState
|
||||
from user_engine.oidc import OIDCClient
|
||||
|
|
@ -127,6 +128,19 @@ def create_application() -> PortalApplication:
|
|||
if os.environ.get("USER_ENGINE_IDENTITY_STATE_URL")
|
||||
else None
|
||||
),
|
||||
hub_authority_caller=(
|
||||
Actor(
|
||||
issuer=_required("USER_ENGINE_HUB_AUTHORITY_ISSUER"),
|
||||
subject=_required("USER_ENGINE_HUB_AUTHORITY_SUBJECT"),
|
||||
tenant=os.environ.get("USER_ENGINE_HUB_AUTHORITY_TENANT", "").strip()
|
||||
or "platform:root",
|
||||
principal_type=PrincipalType.SERVICE,
|
||||
audience=(),
|
||||
)
|
||||
if os.environ.get("USER_ENGINE_HUB_AUTHORITY_TOKEN")
|
||||
else None
|
||||
),
|
||||
hub_authority_token=os.environ.get("USER_ENGINE_HUB_AUTHORITY_TOKEN", "").strip(),
|
||||
)
|
||||
|
||||
from user_engine.operations_status import check_services
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue