fix: preserve tenant creation metadata and report authority denial
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
b9ae48b00c
commit
3c85e563ee
3 changed files with 50 additions and 2 deletions
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue