fix: select existing tenants by name in the operator portal
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
3758a4bb5e
commit
d177d47975
8 changed files with 127 additions and 13 deletions
|
|
@ -316,6 +316,9 @@ class InMemoryUserEngineStore:
|
|||
if membership.tenant == tenant
|
||||
)
|
||||
|
||||
def membership_tenants(self) -> tuple[str, ...]:
|
||||
return tuple(sorted({membership.tenant for membership in self.memberships.values()}))
|
||||
|
||||
def values_for_user(self, user_id: str) -> tuple[ProfileValue, ...]:
|
||||
return tuple(
|
||||
value for value in self.profile_values.values() if value.user_id == user_id
|
||||
|
|
|
|||
|
|
@ -157,6 +157,18 @@ class PostgresUserEngineStore:
|
|||
self._query_records("memberships", tenant=tenant),
|
||||
)
|
||||
|
||||
def membership_tenants(self) -> tuple[str, ...]:
|
||||
with self._cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT DISTINCT tenant FROM user_engine_records "
|
||||
"WHERE record_type = %s AND tenant IS NOT NULL ORDER BY tenant",
|
||||
("memberships",),
|
||||
)
|
||||
return tuple(
|
||||
str(row["tenant"] if isinstance(row, Mapping) else row[0])
|
||||
for row in cursor.fetchall()
|
||||
)
|
||||
|
||||
def save_application(self, application: Application) -> None:
|
||||
self._upsert_record(application)
|
||||
|
||||
|
|
|
|||
|
|
@ -282,6 +282,9 @@ class UserEngineStore(Protocol):
|
|||
def memberships_for_tenant(self, tenant: str) -> tuple[Membership, ...]:
|
||||
"""Return memberships scoped to a tenant."""
|
||||
|
||||
def membership_tenants(self) -> tuple[str, ...]:
|
||||
"""Return distinct tenant identifiers referenced by membership records."""
|
||||
|
||||
def save_application(self, application: Application) -> None:
|
||||
"""Create or replace an application registration."""
|
||||
|
||||
|
|
|
|||
|
|
@ -477,6 +477,7 @@ def _write_reference_records(store: UserEngineStore) -> dict[str, Any]:
|
|||
store.save_identity(identity)
|
||||
store.save_tenant_account(tenant_account)
|
||||
store.save_membership(membership)
|
||||
assert membership.tenant in store.membership_tenants()
|
||||
store.save_application(application)
|
||||
store.save_binding(binding)
|
||||
store.save_catalog(catalog)
|
||||
|
|
|
|||
|
|
@ -738,11 +738,26 @@ class PortalApplication:
|
|||
if path == "/platform/tenant" and method == "GET":
|
||||
self.service.resolve_tenant_context(actor, PLATFORM_TENANT)
|
||||
query = parse_qs(str(environ.get("QUERY_STRING", "")))
|
||||
lookup = query.get("tenant", [""])[0]
|
||||
lookup = query.get("tenant", [""])[0].strip()
|
||||
view = query.get("view", ["lifecycle"])[0]
|
||||
if view not in {"lifecycle", "users"}:
|
||||
raise ValidationError("unknown tenant view")
|
||||
if not lookup.startswith("tenant:") or lookup == PLATFORM_TENANT:
|
||||
if not lookup.startswith("tenant:"):
|
||||
matches = tuple(
|
||||
tenant for tenant in self._known_membership_tenants()
|
||||
if tenant.rsplit(":", 1)[-1] == lookup
|
||||
)
|
||||
if len(matches) != 1:
|
||||
message = (
|
||||
"Several tenants use that name. Choose a tenant below."
|
||||
if matches else
|
||||
"No tenant with users matches that name. Choose a tenant below or enter its full identifier."
|
||||
)
|
||||
return self._html(start_response, self._platform(
|
||||
self._csrf_token(environ), error=message,
|
||||
), correlation_id)
|
||||
lookup = matches[0]
|
||||
if lookup == PLATFORM_TENANT:
|
||||
raise ValidationError("a non-platform tenant identifier is required")
|
||||
return self._redirect(
|
||||
start_response,
|
||||
|
|
@ -1690,10 +1705,32 @@ class PortalApplication:
|
|||
<p>Your password and MFA remain on the identity-provider surface.</p>""",
|
||||
)
|
||||
|
||||
def _platform(self, csrf_token: str) -> str:
|
||||
def _known_membership_tenants(self) -> tuple[str, ...]:
|
||||
# A navigation index of user-owned memberships, not an authority inventory.
|
||||
return tuple(
|
||||
tenant for tenant in self.service.store.membership_tenants()
|
||||
if tenant.startswith("tenant:") and tenant != PLATFORM_TENANT
|
||||
)
|
||||
|
||||
def _platform(self, csrf_token: str, *, error: str = "") -> str:
|
||||
known_tenants = "".join(
|
||||
f'<li><a href="/admin/{escape(quote(tenant, safe=""))}">'
|
||||
f'{escape(tenant.rsplit(":", 1)[-1])}</a> '
|
||||
f'<small>({escape(tenant)})</small></li>'
|
||||
for tenant in self._known_membership_tenants()
|
||||
) or '<li>No tenants with user memberships are recorded yet.</li>'
|
||||
message = f'<p role="alert">{escape(error)}</p>' if error else ""
|
||||
return self._page_html(
|
||||
"Platform administration",
|
||||
f"""<h1>Platform administration</h1>
|
||||
<section aria-labelledby="manage-tenant"><h2 id="manage-tenant">Manage an existing tenant</h2>
|
||||
{message}
|
||||
<p>Choose a tenant to manage its users:</p><ul>{known_tenants}</ul>
|
||||
<form method="get" action="/platform/tenant">
|
||||
<label>Tenant name or full identifier <input name="tenant" required placeholder="demo-company" aria-describedby="tenant-lookup-help"></label>
|
||||
<p id="tenant-lookup-help">Enter a listed name, such as demo-company. You can also use a full identifier, such as tenant:trial:demo-company.</p>
|
||||
<button type="submit" name="view" value="users">Manage users</button>
|
||||
<button type="submit" name="view" value="lifecycle">Open tenant lifecycle</button></form></section>
|
||||
<section aria-labelledby="create-tenant"><h2 id="create-tenant">Create tenant</h2>
|
||||
<form method="post" action="/platform/tenants">
|
||||
<input type="hidden" name="csrf_token" value="{escape(csrf_token)}">
|
||||
|
|
@ -1703,12 +1740,7 @@ class PortalApplication:
|
|||
<label>Name <input name="admin_display_name" autocomplete="name"></label>
|
||||
<label>Email <input name="admin_email" type="email" autocomplete="email"></label></fieldset>
|
||||
<button type="submit">Create tenant</button></form></section>
|
||||
<section aria-labelledby="manage-tenant"><h2 id="manage-tenant">Manage an existing tenant</h2>
|
||||
<form method="get" action="/platform/tenant">
|
||||
<label>Tenant identifier <input name="tenant" required pattern="tenant:.+" placeholder="tenant:friendly:example"></label>
|
||||
<button type="submit" name="view" value="users">Manage users</button>
|
||||
<button type="submit" name="view" value="lifecycle">Open tenant lifecycle</button></form>
|
||||
<p>Tenant records, metadata, and retirement are owned by the tenant authority.</p></section>""",
|
||||
""",
|
||||
)
|
||||
|
||||
def _platform_tenant(self, record: Any, csrf_token: str) -> str:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue