feat: add canonical identity administration

This commit is contained in:
2026-08-20 12:27:36 +02:00
parent 43f0128e6a
commit 34357fe0ec
13 changed files with 1813 additions and 28 deletions
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@govoplan/identity-webui",
"version": "0.1.18",
"private": true,
"type": "module",
"main": "src/index.ts",
"module": "src/index.ts",
"types": "src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./styles/identity.css": "./src/styles/identity.css"
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
},
"scripts": {
"test:identity-admin-ui": "node tests/identity-admin-ui-structure.test.mjs"
}
}
+145
View File
@@ -0,0 +1,145 @@
import {
apiFetch,
apiPath,
type ApiSettings
} from "@govoplan/core-webui";
export type IdentityAccountLink = {
id: string;
identity_id: string;
account_id: string;
is_primary: boolean;
source: string;
created_at: string;
updated_at: string;
};
export type IdentityItem = {
id: string;
display_name?: string | null;
external_subject?: string | null;
source: string;
primary_account_id?: string | null;
account_ids: string[];
account_links: IdentityAccountLink[];
status: "active" | "inactive";
is_active: boolean;
settings: Record<string, unknown>;
management_scope: "system";
created_at: string;
updated_at: string;
};
export type IdentityDraft = {
display_name?: string | null;
external_subject?: string | null;
source: string;
};
export async function listIdentities(
settings: ApiSettings,
query = "",
includeInactive = true
): Promise<IdentityItem[]> {
const result = await apiFetch<{ identities: IdentityItem[] }>(
settings,
apiPath("/api/v1/identity/identities", {
query: query.trim() || undefined,
include_inactive: includeInactive,
limit: 500
})
);
return result.identities;
}
export function createIdentity(
settings: ApiSettings,
payload: IdentityDraft
): Promise<IdentityItem> {
return apiFetch(settings, "/api/v1/identity/identities", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function updateIdentity(
settings: ApiSettings,
identityId: string,
payload: Partial<IdentityDraft>
): Promise<IdentityItem> {
return apiFetch(
settings,
`/api/v1/identity/identities/${encodeURIComponent(identityId)}`,
{
method: "PATCH",
body: JSON.stringify(payload)
}
);
}
export function setIdentityActive(
settings: ApiSettings,
identityId: string,
active: boolean,
reason: string
): Promise<IdentityItem> {
return apiFetch(
settings,
`/api/v1/identity/identities/${encodeURIComponent(identityId)}/${active ? "activate" : "deactivate"}`,
{
method: "POST",
body: JSON.stringify({ reason: reason.trim() || null })
}
);
}
export function addIdentityAccountLink(
settings: ApiSettings,
identityId: string,
payload: {
account_id: string;
source: string;
make_primary: boolean;
reason?: string | null;
}
): Promise<IdentityItem> {
return apiFetch(
settings,
`/api/v1/identity/identities/${encodeURIComponent(identityId)}/account-links`,
{
method: "POST",
body: JSON.stringify(payload)
}
);
}
export function promoteIdentityAccountLink(
settings: ApiSettings,
identityId: string,
linkId: string,
reason: string
): Promise<IdentityItem> {
return apiFetch(
settings,
`/api/v1/identity/identities/${encodeURIComponent(identityId)}/account-links/${encodeURIComponent(linkId)}`,
{
method: "PATCH",
body: JSON.stringify({
is_primary: true,
reason: reason.trim() || null
})
}
);
}
export function removeIdentityAccountLink(
settings: ApiSettings,
identityId: string,
linkId: string
): Promise<void> {
return apiFetch(
settings,
`/api/v1/identity/identities/${encodeURIComponent(identityId)}/account-links/${encodeURIComponent(linkId)}`,
{ method: "DELETE" }
);
}
+744
View File
@@ -0,0 +1,744 @@
import {
Plus,
Star,
Trash2,
UserCheck,
UserMinus
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
AdminPageLayout,
Button,
Card,
DataGrid,
Dialog,
FilterBar,
FormField,
FormGrid,
MetricCard,
MetricGrid,
PageActionBar,
SelectionList,
SelectionListItem,
SelectionListItemContent,
StatePanel,
StatusBadge,
TableActionGroup,
ToggleSwitch,
WorkspaceLayout,
formatDateTime,
hasScope,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings,
type AuthInfo,
type DataGridColumn
} from "@govoplan/core-webui";
import {
addIdentityAccountLink,
createIdentity,
listIdentities,
promoteIdentityAccountLink,
removeIdentityAccountLink,
setIdentityActive,
updateIdentity,
type IdentityAccountLink,
type IdentityDraft,
type IdentityItem
} from "../api/identities";
type Props = {
settings: ApiSettings;
auth: AuthInfo;
};
type LinkDraft = {
accountId: string;
source: string;
makePrimary: boolean;
reason: string;
};
const EMPTY_DRAFT: IdentityDraft = {
display_name: "",
external_subject: "",
source: "local"
};
const EMPTY_LINK: LinkDraft = {
accountId: "",
source: "local",
makePrimary: false,
reason: ""
};
export default function IdentityAdminPage({ settings, auth }: Props) {
const [items, setItems] = useState<IdentityItem[]>([]);
const [selectedId, setSelectedId] = useState("");
const [draft, setDraft] = useState<IdentityDraft>(EMPTY_DRAFT);
const [savedKey, setSavedKey] = useState("");
const [search, setSearch] = useState("");
const [showInactive, setShowInactive] = useState(true);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [createOpen, setCreateOpen] = useState(false);
const [createDraft, setCreateDraft] = useState<IdentityDraft>(EMPTY_DRAFT);
const [linkOpen, setLinkOpen] = useState(false);
const [linkDraft, setLinkDraft] = useState<LinkDraft>(EMPTY_LINK);
const [lifecycleOpen, setLifecycleOpen] = useState(false);
const [lifecycleReason, setLifecycleReason] = useState("");
const [removeLink, setRemoveLink] = useState<IdentityAccountLink | null>(null);
const { requestDiscard } = useUnsavedChanges();
const selected = items.find((item) => item.id === selectedId) ?? null;
const canWrite = hasScope(auth, "identity:identity:admin")
|| hasScope(auth, "system:accounts:update")
|| hasScope(auth, "access:account:update");
const canManageLinks = hasScope(auth, "identity:account_link:admin")
|| canWrite;
const dirty = Boolean(selected && draftKey(draft) !== savedKey);
const applyIdentity = useCallback((item: IdentityItem | null) => {
const next = item ? draftFromIdentity(item) : EMPTY_DRAFT;
setDraft(next);
setSavedKey(item ? draftKey(next) : "");
}, []);
const reload = useCallback(async (preferredId?: string) => {
setLoading(true);
setError("");
try {
const next = await listIdentities(settings, "", true);
setItems(next);
const nextId = preferredId && next.some((item) => item.id === preferredId)
? preferredId
: next.some((item) => item.id === selectedId)
? selectedId
: next[0]?.id ?? "";
setSelectedId(nextId);
applyIdentity(next.find((item) => item.id === nextId) ?? null);
} catch (caught) {
setError(errorMessage(caught));
} finally {
setLoading(false);
}
}, [applyIdentity, selectedId, settings]);
useEffect(() => {
void reload();
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
const visibleItems = useMemo(() => {
const needle = search.trim().toLocaleLowerCase();
return items.filter((item) => {
if (!showInactive && !item.is_active) return false;
if (!needle) return true;
return `${item.display_name ?? ""} ${item.external_subject ?? ""} ${item.id} ${item.account_ids.join(" ")}`
.toLocaleLowerCase()
.includes(needle);
});
}, [items, search, showInactive]);
const save = async (): Promise<boolean> => {
if (!selected || !canWrite) return false;
setBusy(true);
setError("");
try {
const updated = await updateIdentity(settings, selected.id, draft);
setSuccess("Identity saved.");
await reload(updated.id);
return true;
} catch (caught) {
setError(errorMessage(caught));
return false;
} finally {
setBusy(false);
}
};
useUnsavedDraftGuard({
dirty,
onSave: save,
onDiscard: () => applyIdentity(selected),
title: "Unsaved identity changes",
message: "Save or discard the current identity changes before continuing."
});
const selectIdentity = (item: IdentityItem) => {
if (item.id === selectedId) return;
requestDiscard(() => {
setSelectedId(item.id);
applyIdentity(item);
setError("");
setSuccess("");
});
};
const create = async () => {
if (!createDraft.display_name?.trim() || busy) return;
setBusy(true);
setError("");
try {
const created = await createIdentity(settings, createDraft);
setCreateOpen(false);
setCreateDraft(EMPTY_DRAFT);
setSuccess("Identity created.");
await reload(created.id);
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const addLink = async () => {
if (!selected || !linkDraft.accountId.trim() || busy) return;
setBusy(true);
setError("");
try {
const updated = await addIdentityAccountLink(settings, selected.id, {
account_id: linkDraft.accountId.trim(),
source: linkDraft.source.trim() || "local",
make_primary: linkDraft.makePrimary,
reason: linkDraft.reason.trim() || null
});
setLinkOpen(false);
setLinkDraft(EMPTY_LINK);
setSuccess("Account link added.");
await reload(updated.id);
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const promote = async (link: IdentityAccountLink) => {
if (!selected || busy) return;
setBusy(true);
setError("");
try {
const updated = await promoteIdentityAccountLink(
settings,
selected.id,
link.id,
"Promoted through Identity administration"
);
setSuccess("Primary account changed.");
await reload(updated.id);
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const remove = async () => {
if (!selected || !removeLink || busy) return;
setBusy(true);
setError("");
try {
await removeIdentityAccountLink(settings, selected.id, removeLink.id);
setRemoveLink(null);
setSuccess("Account link removed.");
await reload(selected.id);
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const applyLifecycle = async () => {
if (!selected || busy) return;
setBusy(true);
setError("");
try {
const updated = await setIdentityActive(
settings,
selected.id,
!selected.is_active,
lifecycleReason
);
setLifecycleOpen(false);
setLifecycleReason("");
setSuccess(updated.is_active ? "Identity reactivated." : "Identity deactivated.");
await reload(updated.id);
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const linkColumns = useMemo<DataGridColumn<IdentityAccountLink>[]>(() => [
{
id: "account",
header: "Account ID",
width: "1fr",
minWidth: 220,
sortable: true,
filterable: true,
value: (row) => row.account_id,
render: (row) => <code>{row.account_id}</code>
},
{
id: "primary",
header: "Role",
width: 130,
sortable: true,
value: (row) => row.is_primary ? "primary" : "linked",
render: (row) => (
<StatusBadge
status={row.is_primary ? "active" : "neutral"}
label={row.is_primary ? "Primary" : "Linked"}
/>
)
},
{
id: "source",
header: "Source",
width: 180,
sortable: true,
filterable: true,
value: (row) => row.source
},
{
id: "created",
header: "Linked",
width: 190,
sortable: true,
value: (row) => row.created_at,
render: (row) => formatDateTime(row.created_at)
},
{
id: "actions",
header: "Actions",
width: 100,
sticky: "end",
align: "right",
render: (row) => (
<TableActionGroup actions={[
{
id: "promote",
label: "Promote to primary",
icon: <Star aria-hidden="true" />,
applicable: !row.is_primary,
disabled: !canManageLinks || busy,
disabledReason: !canManageLinks
? "Identity account-link administration permission is required."
: undefined,
onClick: () => void promote(row)
},
{
id: "remove",
label: "Remove account link",
icon: <Trash2 aria-hidden="true" />,
variant: "danger",
disabled: !canManageLinks || busy,
disabledReason: row.is_primary && (selected?.account_links.length ?? 0) > 1
? "Promote another account before removing the primary link."
: !canManageLinks
? "Identity account-link administration permission is required."
: undefined,
onClick: () => setRemoveLink(row)
}
]} />
)
}
], [busy, canManageLinks, selected?.account_links.length]);
const actionBar = (
<PageActionBar
variant="editor"
state={busy ? "saving" : dirty ? "dirty" : "clean"}
refreshable
reloadAction={{
onReload: () => void reload(selectedId),
loading: loading
}}
primaryActions={
<Button
onClick={() => setCreateOpen(true)}
disabled={!canWrite || busy}
disabledReason={!canWrite
? "System identity administration permission is required."
: undefined}
>
<Plus aria-hidden="true" /> New identity
</Button>
}
destructiveActions={selected ? (
<Button
variant={selected.is_active ? "danger" : "secondary"}
onClick={() => setLifecycleOpen(true)}
disabled={!canWrite || busy}
disabledReason={!canWrite
? "System identity administration permission is required."
: undefined}
>
{selected.is_active
? <><UserMinus aria-hidden="true" /> Deactivate</>
: <><UserCheck aria-hidden="true" /> Reactivate</>}
</Button>
) : null}
discardAction={{
label: "Discard changes",
disabled: !selected,
onClick: () => applyIdentity(selected)
}}
saveAction={{
label: "Save",
disabled: !selected || !canWrite || busy,
disabledReason: !canWrite
? "System identity administration permission is required."
: undefined,
onClick: () => void save()
}}
/>
);
return (
<AdminPageLayout
archetype="workspace"
title="Identity directory"
description="Manage canonical system identities and their opaque platform-account links."
loading={loading && !items.length}
error={error}
success={success}
actions={actionBar}
className="identity-admin-page"
helpContextId="identity.admin.directory"
>
<p className="muted identity-admin-scope-note">
<strong>Management scope:</strong> {selected?.management_scope ?? "system"}.
{" "}The active tenant is the
actor context only; Identity does not grant account access or suspend authentication.
</p>
<MetricGrid columns={3} density="compact" minimum="compact">
<MetricCard label="Identities" value={items.length} />
<MetricCard
label="Active"
value={items.filter((item) => item.is_active).length}
tone="good"
/>
<MetricCard
label="Without account"
value={items.filter((item) => !item.account_links.length).length}
tone="warning"
/>
</MetricGrid>
<WorkspaceLayout
variant="split"
primarySize="compact"
surface="contained"
primaryScrollable={false}
contentScrollable={false}
primaryLabel="Identities"
contentLabel="Identity details"
contentClassName="identity-admin-workspace"
primary={<div className="identity-admin-list">
<FilterBar surface="panel">
<input
type="search"
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Search identities or account IDs"
aria-label="Search identities"
/>
<ToggleSwitch
label="Show inactive"
checked={showInactive}
onChange={setShowInactive}
/>
</FilterBar>
<SelectionList variant="navigation" label="Identities">
{visibleItems.map((item) => (
<SelectionListItem
key={item.id}
selected={item.id === selectedId}
onClick={() => selectIdentity(item)}
>
<SelectionListItemContent
title={item.display_name || item.id}
description={item.primary_account_id || "No account linked"}
/>
<StatusBadge status={item.status} />
</SelectionListItem>
))}
{!visibleItems.length
? <StatePanel size="compact" description="No matching identities." />
: null}
</SelectionList>
</div>}
>
{!selected ? (
<StatePanel
size="fill"
title="Identity directory"
description="Create or select an identity to inspect it."
/>
) : (
<div className="identity-admin-detail">
<Card
title={selected.display_name || selected.id}
>
<p className="muted">System identity · {selected.status}</p>
<FormGrid columns={2} collapseAt="standard" className="">
<FormField label="Display name">
<input
value={draft.display_name ?? ""}
disabled={!canWrite || busy}
onChange={(event) => setDraft({
...draft,
display_name: event.target.value
})}
/>
</FormField>
<FormField label="External subject">
<input
value={draft.external_subject ?? ""}
disabled={!canWrite || busy}
onChange={(event) => setDraft({
...draft,
external_subject: event.target.value
})}
/>
</FormField>
<FormField label="Source">
<input
value={draft.source}
disabled={!canWrite || busy}
onChange={(event) => setDraft({
...draft,
source: event.target.value
})}
/>
</FormField>
<FormField label="Identity ID">
<input value={selected.id} readOnly />
</FormField>
</FormGrid>
</Card>
<Card
title="Account links"
actions={
<Button
onClick={() => setLinkOpen(true)}
disabled={!canManageLinks || busy}
disabledReason={!canManageLinks
? "Identity account-link administration permission is required."
: undefined}
>
<Plus aria-hidden="true" /> Add account link
</Button>
}
>
<p className="muted">
One opaque account reference is primary. Authentication and
account lookup remain owned by Access.
</p>
<DataGrid
id="identity-account-links"
rows={selected.account_links}
columns={linkColumns}
initialFit="container"
getRowKey={(row) => row.id}
emptyText="No platform accounts are linked."
/>
</Card>
<Card title="Accepted normalized facts">
<pre className="identity-admin-settings">
{JSON.stringify(selected.settings, null, 2)}
</pre>
</Card>
</div>
)}
</WorkspaceLayout>
<Dialog
open={createOpen}
title="Create identity"
onClose={() => !busy && setCreateOpen(false)}
closeDisabled={busy}
footer={<>
<Button onClick={() => setCreateOpen(false)} disabled={busy}>Cancel</Button>
<Button
variant="primary"
onClick={() => void create()}
disabled={busy || !createDraft.display_name?.trim()}
>
Create identity
</Button>
</>}
>
<FormGrid columns={2} collapseAt="standard" className="">
<FormField label="Display name">
<input
value={createDraft.display_name ?? ""}
disabled={busy}
onChange={(event) => setCreateDraft({
...createDraft,
display_name: event.target.value
})}
/>
</FormField>
<FormField label="Source">
<input
value={createDraft.source}
disabled={busy}
onChange={(event) => setCreateDraft({
...createDraft,
source: event.target.value
})}
/>
</FormField>
<FormField label="External subject">
<input
value={createDraft.external_subject ?? ""}
disabled={busy}
onChange={(event) => setCreateDraft({
...createDraft,
external_subject: event.target.value
})}
/>
</FormField>
</FormGrid>
</Dialog>
<Dialog
open={linkOpen}
title="Add account link"
onClose={() => !busy && setLinkOpen(false)}
closeDisabled={busy}
footer={<>
<Button onClick={() => setLinkOpen(false)} disabled={busy}>Cancel</Button>
<Button
variant="primary"
onClick={() => void addLink()}
disabled={busy || !linkDraft.accountId.trim()}
>
Add link
</Button>
</>}
>
<FormGrid columns={2} collapseAt="standard" className="">
<FormField label="Account ID">
<input
value={linkDraft.accountId}
disabled={busy}
onChange={(event) => setLinkDraft({
...linkDraft,
accountId: event.target.value
})}
/>
</FormField>
<FormField label="Source">
<input
value={linkDraft.source}
disabled={busy}
onChange={(event) => setLinkDraft({
...linkDraft,
source: event.target.value
})}
/>
</FormField>
<ToggleSwitch
label="Make primary"
checked={linkDraft.makePrimary}
onChange={(makePrimary) => setLinkDraft({ ...linkDraft, makePrimary })}
/>
<FormField label="Reason">
<input
value={linkDraft.reason}
disabled={busy}
onChange={(event) => setLinkDraft({
...linkDraft,
reason: event.target.value
})}
/>
</FormField>
</FormGrid>
<p className="muted">
The first account link becomes primary automatically. Identity stores
only the account reference and provenance.
</p>
</Dialog>
<Dialog
open={lifecycleOpen}
title={selected?.is_active ? "Deactivate identity" : "Reactivate identity"}
onClose={() => !busy && setLifecycleOpen(false)}
closeDisabled={busy}
footer={<>
<Button onClick={() => setLifecycleOpen(false)} disabled={busy}>Cancel</Button>
<Button
variant={selected?.is_active ? "danger" : "primary"}
onClick={() => void applyLifecycle()}
disabled={busy}
>
{selected?.is_active ? "Deactivate" : "Reactivate"}
</Button>
</>}
>
<p>
{selected?.is_active
? "Deactivation hides the identity from ordinary directory search. It does not suspend authentication, erase links, or revoke permissions."
: "Reactivation restores the identity to ordinary directory search."}
</p>
<FormField label="Reason">
<textarea
rows={3}
value={lifecycleReason}
disabled={busy}
onChange={(event) => setLifecycleReason(event.target.value)}
/>
</FormField>
</Dialog>
<Dialog
open={Boolean(removeLink)}
title="Remove account link"
onClose={() => !busy && setRemoveLink(null)}
closeDisabled={busy}
footer={<>
<Button onClick={() => setRemoveLink(null)} disabled={busy}>Cancel</Button>
<Button variant="danger" onClick={() => void remove()} disabled={busy}>
Remove link
</Button>
</>}
>
<p>
Remove account <strong>{removeLink?.account_id}</strong> from this
identity? The account itself is not deleted.
</p>
</Dialog>
</AdminPageLayout>
);
}
function draftFromIdentity(item: IdentityItem): IdentityDraft {
return {
display_name: item.display_name ?? "",
external_subject: item.external_subject ?? "",
source: item.source
};
}
function draftKey(value: IdentityDraft): string {
return JSON.stringify({
display_name: value.display_name?.trim() || null,
external_subject: value.external_subject?.trim() || null,
source: value.source.trim()
});
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
+3
View File
@@ -0,0 +1,3 @@
export { default, identityModule } from "./module";
export * from "./api/identities";
export { default as IdentityAdminPage } from "./features/IdentityAdminPage";
+62
View File
@@ -0,0 +1,62 @@
import { createElement, lazy } from "react";
import type {
AdminSectionsUiCapability,
PlatformWebModule
} from "@govoplan/core-webui";
import "./styles/identity.css";
const IdentityAdminPage = lazy(() => import("./features/IdentityAdminPage"));
const readScopes = [
"identity:identity:read",
"identity:identity:admin",
"identity:account_link:admin",
"system:accounts:read"
];
const adminSections: AdminSectionsUiCapability = {
sections: [
{
id: "system-identities",
moduleId: "identity",
kind: "management",
surfaceId: "identity.admin.directory",
label: "Identity directory",
group: "SYSTEM",
order: 30,
anyOf: readScopes,
render: ({ settings, auth }) =>
createElement(IdentityAdminPage, { settings, auth })
}
]
};
export const identityModule: PlatformWebModule = {
id: "identity",
label: "Identity",
version: "0.1.18",
dependencies: [],
optionalDependencies: ["access", "audit", "idm"],
viewSurfaces: [
{
id: "identity.admin.directory",
moduleId: "identity",
kind: "section",
label: "Identity directory",
order: 30
},
{
id: "identity.admin.account-links",
moduleId: "identity",
kind: "section",
label: "Identity account links",
parentId: "identity.admin.directory",
order: 20
}
],
uiCapabilities: {
"admin.sections": adminSections
}
};
export default identityModule;
+25
View File
@@ -0,0 +1,25 @@
.identity-admin-page .identity-admin-workspace {
min-height: 34rem;
}
.identity-admin-page .identity-admin-list {
display: grid;
min-height: 0;
}
.identity-admin-page .identity-admin-detail {
display: grid;
gap: var(--space-4);
align-content: start;
}
.identity-admin-page .identity-admin-scope-note {
margin: 0;
}
.identity-admin-page .identity-admin-settings {
margin: 0;
max-height: 14rem;
overflow: auto;
white-space: pre-wrap;
}
@@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
const moduleSource = readFileSync("src/module.ts", "utf8");
const page = readFileSync("src/features/IdentityAdminPage.tsx", "utf8");
const api = readFileSync("src/api/identities.ts", "utf8");
assert.match(moduleSource, /"admin.sections": adminSections/);
assert.match(moduleSource, /identity\.admin\.directory/);
assert.match(page, /<AdminPageLayout/);
assert.match(page, /<PageActionBar/);
assert.match(page, /refreshable/);
assert.match(page, /saveAction=/);
assert.match(page, /useUnsavedDraftGuard/);
assert.match(page, /management_scope/);
assert.match(page, /Promote/);
assert.match(api, /account-links/);
assert.match(api, /include_inactive/);
console.log("Identity administration UI structural contract passed.");