From 3c85e563ee2717def728723fda91ab89a25bf1d9 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 11 Sep 2026 20:52:47 +0200 Subject: [PATCH] fix: preserve tenant creation metadata and report authority denial Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc --- src/user_engine/adapters/tenant_management.py | 6 ++-- tests/test_tenant_management_adapter.py | 11 ++++++ tests/test_web.py | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/user_engine/adapters/tenant_management.py b/src/user_engine/adapters/tenant_management.py index ca1ac11..f3a98cd 100644 --- a/src/user_engine/adapters/tenant_management.py +++ b/src/user_engine/adapters/tenant_management.py @@ -52,6 +52,8 @@ class HTTPTenantManagementAdapter: "tenant_id": tenant, "identifier": tenant, "actor": ACTOR, + "display_name": display_name, + "correlation_id": correlation_id, }).encode(), headers={ "Authorization": f"Bearer {self.bearer_token}", @@ -65,12 +67,12 @@ class HTTPTenantManagementAdapter: with urlopen(request, timeout=self.timeout) as response: result = json.loads(response.read()) except HTTPError as exc: - exc.read(4096) if exc.code == 409: + exc.read(4096) return TenantProvisioningResult( 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: raise RuntimeError("tenant authority unavailable") from exc return TenantProvisioningResult( diff --git a/tests/test_tenant_management_adapter.py b/tests/test_tenant_management_adapter.py index a23cb71..d318c68 100644 --- a/tests/test_tenant_management_adapter.py +++ b/tests/test_tenant_management_adapter.py @@ -63,11 +63,22 @@ class TenantManagementAdapterTests(unittest.TestCase): "tenant_id": "tenant:friendly:new", "identifier": "tenant:friendly:new", "actor": "user-engine", + "display_name": "New", + "correlation_id": "corr-1", }) self.assertEqual(result.status, "created") 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): def setUp(self): self.adapter = HTTPTenantManagementAdapter( diff --git a/tests/test_web.py b/tests/test_web.py index 6482dc8..6c0bf55 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -6,6 +6,10 @@ import unittest from dataclasses import replace from datetime import timedelta 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.domain import ( @@ -898,6 +902,37 @@ class PortalApplicationTests(unittest.TestCase): ) 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): claims = self.platform_claims() oidc = OIDCClient(