fix: preserve tenant creation metadata and report authority denial
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 35s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-11 20:52:47 +02:00
parent b9ae48b00c
commit 3c85e563ee
3 changed files with 50 additions and 2 deletions

View file

@ -52,6 +52,8 @@ class HTTPTenantManagementAdapter:
"tenant_id": tenant, "tenant_id": tenant,
"identifier": tenant, "identifier": tenant,
"actor": ACTOR, "actor": ACTOR,
"display_name": display_name,
"correlation_id": correlation_id,
}).encode(), }).encode(),
headers={ headers={
"Authorization": f"Bearer {self.bearer_token}", "Authorization": f"Bearer {self.bearer_token}",
@ -65,12 +67,12 @@ class HTTPTenantManagementAdapter:
with urlopen(request, timeout=self.timeout) as response: with urlopen(request, timeout=self.timeout) as response:
result = json.loads(response.read()) result = json.loads(response.read())
except HTTPError as exc: except HTTPError as exc:
exc.read(4096)
if exc.code == 409: if exc.code == 409:
exc.read(4096)
return TenantProvisioningResult( return TenantProvisioningResult(
tenant=tenant, status="existing", resumed=True, external_ref=tenant, tenant=tenant, status="existing", resumed=True, external_ref=tenant,
) )
raise RuntimeError(f"tenant authority failed ({exc.code})") from exc raise self._redacted(exc) from exc
except URLError as exc: except URLError as exc:
raise RuntimeError("tenant authority unavailable") from exc raise RuntimeError("tenant authority unavailable") from exc
return TenantProvisioningResult( return TenantProvisioningResult(

View file

@ -63,11 +63,22 @@ class TenantManagementAdapterTests(unittest.TestCase):
"tenant_id": "tenant:friendly:new", "tenant_id": "tenant:friendly:new",
"identifier": "tenant:friendly:new", "identifier": "tenant:friendly:new",
"actor": "user-engine", "actor": "user-engine",
"display_name": "New",
"correlation_id": "corr-1",
}) })
self.assertEqual(result.status, "created") self.assertEqual(result.status, "created")
self.assertEqual(result.external_ref, "tenant:friendly:new") self.assertEqual(result.external_ref, "tenant:friendly:new")
def test_create_denial_is_not_reported_as_provisioning_outage(self):
adapter = HTTPTenantManagementAdapter(base_url="http://tenant-engine", bearer_token="synthetic")
error = _http_error(403, {"error_code": "write_denied", "detail": "private policy detail"})
with patch("user_engine.adapters.tenant_management.urlopen", side_effect=error):
with self.assertRaises(AuthorizationDenied) as caught:
adapter.create_tenant(tenant="tenant:trial:demo-company", display_name="Demo",
idempotency_key="demo-create", correlation_id="corr-demo")
self.assertNotIn("private policy", str(caught.exception))
class TenantLifecycleAdapterTests(unittest.TestCase): class TenantLifecycleAdapterTests(unittest.TestCase):
def setUp(self): def setUp(self):
self.adapter = HTTPTenantManagementAdapter( self.adapter = HTTPTenantManagementAdapter(

View file

@ -6,6 +6,10 @@ import unittest
from dataclasses import replace from dataclasses import replace
from datetime import timedelta from datetime import timedelta
from urllib.parse import quote, urlencode from urllib.parse import quote, urlencode
from urllib.error import HTTPError
from unittest.mock import patch
from user_engine.adapters.tenant_management import HTTPTenantManagementAdapter
from user_engine.adapters import InMemoryUserEngineStore, LocalAuthorizationCheckPort from user_engine.adapters import InMemoryUserEngineStore, LocalAuthorizationCheckPort
from user_engine.domain import ( from user_engine.domain import (
@ -898,6 +902,37 @@ class PortalApplicationTests(unittest.TestCase):
) )
self.assertIn(b"Manage an existing tenant", html) self.assertIn(b"Manage an existing tenant", html)
def test_platform_tenant_authority_denial_is_redacted_and_creates_no_admin(self):
oidc = OIDCClient(
issuer="https://kc.example", client_id="portal",
redirect_uri="https://users.example/oidc/callback", audience="portal",
)
oidc.sessions["platform"] = BrowserSession(
claims=self.platform_claims(), expires_at=9999999999,
csrf_token="platform-csrf",
)
self.app.oidc_client = oidc
self.app.tenant_management = HTTPTenantManagementAdapter(
base_url="https://tenants.example", bearer_token="test-service-boundary",
)
failure = HTTPError(
"https://tenants.example/tenants", 403, "Forbidden", {},
io.BytesIO(b'{"error_code":"write_denied","detail":"private-policy-detail"}'),
)
with patch("user_engine.adapters.tenant_management.urlopen", side_effect=failure), \
patch.object(self.app.service, "create_user") as create_user:
response, payload = invoke(
self.app, "/platform/tenants", method="POST",
cookie="ue_session=platform", form={
"csrf_token": "platform-csrf", "tenant": "tenant:trial:demo-company",
"admin_display_name": "First Admin", "admin_email": "admin@example.test",
},
)
self.assertEqual("403 Forbidden", response["status"])
self.assertNotIn(b"provisioning_unavailable", payload)
self.assertNotIn(b"private-policy-detail", payload)
create_user.assert_not_called()
def test_platform_browser_tenant_and_first_admin_bootstrap(self): def test_platform_browser_tenant_and_first_admin_bootstrap(self):
claims = self.platform_claims() claims = self.platform_claims()
oidc = OIDCClient( oidc = OIDCClient(