2 Commits
Author SHA1 Message Date
zemion e2a39d7c4f fix(ui): align contextual documentation with headings
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:47 +02:00
zemion ba04593e29 refactor(idm): share directory dependency resolution
Module Package Release / publish-packages (push) Successful in 18s
Release v0.1.26. Coordinated integrity review: GovOPlaN/govoplan-core#298.
2026-09-08 12:19:39 +02:00
12 changed files with 110 additions and 42 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/idm-webui",
"version": "0.1.25",
"version": "0.1.26",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-idm"
version = "0.1.25"
version = "0.1.26"
description = "GovOPlaN identity management bridge module."
readme = "README.md"
requires-python = ">=3.12"
+20
View File
@@ -0,0 +1,20 @@
"""IDM route dependencies; resolve the current optional Core capability per call."""
from fastapi import HTTPException, status
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
def require_identity_directory(registry: object | None) -> IdentityDirectory:
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Identity directory is unavailable",
)
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
if not isinstance(capability, IdentityDirectory):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}",
)
return capability
@@ -15,7 +15,6 @@ from govoplan_core.core.events import (
emit_platform_event,
)
from govoplan_core.core.identity import (
CAPABILITY_IDENTITY_DIRECTORY,
IdentityDirectory,
)
from govoplan_core.core.idm import (
@@ -34,6 +33,7 @@ from govoplan_idm.backend.db.models import (
IdmTypedGroup,
)
from .directory_dependencies import require_identity_directory
from .schemas import (
IdentityRelationshipCreateRequest,
IdentityRelationshipDecisionItem,
@@ -154,19 +154,7 @@ def _tenant_row(session: Session, model, item_id: str, tenant_id: str, label: st
def _identity_directory() -> IdentityDirectory:
registry = get_registry()
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Identity directory is unavailable",
)
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
if not isinstance(capability, IdentityDirectory):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}",
)
return capability
return require_identity_directory(get_registry())
def _relationship_directory() -> IdmRelationshipDirectory:
+2 -14
View File
@@ -18,7 +18,6 @@ from govoplan_core.core.configuration_control import (
)
from govoplan_core.core.principal_cache import invalidate_auth_principals
from govoplan_core.core.identity import (
CAPABILITY_IDENTITY_DIRECTORY,
CAPABILITY_IDENTITY_SEARCH,
IdentityDirectory,
IdentityRef,
@@ -44,6 +43,7 @@ from govoplan_idm.backend.assignment_transitions import (
from govoplan_idm.backend.assignment_events import emit_assignment_event
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings
from .directory_dependencies import require_identity_directory
from .schemas import (
IdmSettingsItem,
IdmSettingsUpdateRequest,
@@ -187,19 +187,7 @@ def _default_settings(tenant_id: str) -> IdmSettingsItem:
def _identity_directory() -> IdentityDirectory:
registry = get_registry()
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Identity directory is unavailable",
)
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
if not isinstance(capability, IdentityDirectory):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}",
)
return capability
return require_identity_directory(get_registry())
def _identity_search() -> IdentitySearchProvider:
+9 -3
View File
@@ -69,7 +69,7 @@ from govoplan_idm.backend.search_source import create_idm_search_source
from govoplan_idm.backend.scim import SCIM_EXTERNAL_PROVIDER_ID
MODULE_VERSION = "0.1.25"
MODULE_VERSION = "0.1.26"
IDM_READ_SCOPES = (
"idm:organization_assignment:read",
@@ -480,7 +480,10 @@ manifest = ModuleManifest(
id="idm.workspace-layout",
title="IDM workspace layout",
summary="Find workspace actions and read consistently arranged content.",
body="Function requests and grants, typed groups, effective identity relationships, and function assignments use full-width table cards with consistent spacing. Card headings and actions remain above each table; explanatory taglines are kept out of the table surface. Relationship help still explains the essential boundary: institutional membership does not grant application permissions; Access evaluates authority separately. Administrators retain the existing read, write, request, grant, and decision permissions. Shared Core card and grid layouts replace per-section width or gap workarounds.",
body="Documentation books sit beside IDM and the relevant governance, request, or relationship "
"heading. Emergency override guidance is attached to that phrase, and field help stays with "
"its label. "
"Function requests and grants, typed groups, effective identity relationships, and function assignments use full-width table cards with consistent spacing. Card headings and actions remain above each table; explanatory taglines are kept out of the table surface. Relationship help still explains the essential boundary: institutional membership does not grant application permissions; Access evaluates authority separately. Administrators retain the existing read, write, request, grant, and decision permissions. Shared Core card and grid layouts replace per-section width or gap workarounds.",
layer="static",
documentation_types=("user", "admin"),
audience=("user", "module_admin", "operator"),
@@ -488,7 +491,10 @@ manifest = ModuleManifest(
translations={"de": {
"title": "Identitätsmanagement: Aufbau des Arbeitsbereichs",
"summary": "Arbeitsbereichsaktionen finden und einheitlich angeordnete Inhalte lesen.",
"body": "Funktionsanträge und -vergaben, typisierte Gruppen, wirksame Identitätsbeziehungen und Funktionszuordnungen verwenden Tabellenkarten über die gesamte Breite mit einheitlichen Abständen. Überschrift und Aktionen bleiben über der jeweiligen Tabelle; erläuternde Unterzeilen entfallen in der Tabellenfläche. Die Beziehungshilfe erklärt weiterhin die wesentliche Grenze: Institutionelle Mitgliedschaft erteilt keine Anwendungsrechte; Access bewertet Berechtigungen getrennt. Administratoren behalten die vorhandenen Lese-, Schreib-, Antrags-, Vergabe- und Entscheidungsrechte. Gemeinsame Core-Karten- und Rasterlayouts ersetzen lokale Breiten- oder Abstandsbehelfe.",
"body": "Dokumentationsbücher stehen neben IDM und der jeweiligen Überschrift zu Governance, Anfragen "
"oder Beziehungen. Hinweise zu Notfallübersteuerungen stehen direkt an diesem Begriff, und "
"Feldhilfe bleibt bei der Feldbezeichnung. "
"Funktionsanträge und -vergaben, typisierte Gruppen, wirksame Identitätsbeziehungen und Funktionszuordnungen verwenden Tabellenkarten über die gesamte Breite mit einheitlichen Abständen. Überschrift und Aktionen bleiben über der jeweiligen Tabelle; erläuternde Unterzeilen entfallen in der Tabellenfläche. Die Beziehungshilfe erklärt weiterhin die wesentliche Grenze: Institutionelle Mitgliedschaft erteilt keine Anwendungsrechte; Access bewertet Berechtigungen getrennt. Administratoren behalten die vorhandenen Lese-, Schreib-, Antrags-, Vergabe- und Entscheidungsrechte. Gemeinsame Core-Karten- und Rasterlayouts ersetzen lokale Breiten- oder Abstandsbehelfe.",
}},
),
DocumentationTopic(
+61
View File
@@ -0,0 +1,61 @@
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from fastapi import HTTPException
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
from govoplan_idm.backend.api.v1 import function_changes, relationships, routes
class DirectoryDependencyTests(unittest.TestCase):
def test_identical_routes_preserve_missing_invalid_and_success_contracts(self) -> None:
valid = Mock(spec=IdentityDirectory)
for route in (relationships, routes):
for registry, expected_status, expected_detail in (
(None, 503, "Identity directory is unavailable"),
(Mock(has_capability=Mock(return_value=False)), 503, "Identity directory is unavailable"),
(Mock(has_capability=Mock(return_value=True), require_capability=Mock(return_value=object())), 500,
f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}"),
):
with self.subTest(route=route.__name__, status=expected_status), patch.object(route, "get_registry", return_value=registry):
with self.assertRaises(HTTPException) as caught:
route._identity_directory()
self.assertEqual(expected_status, caught.exception.status_code)
self.assertEqual(expected_detail, caught.exception.detail)
registry = Mock(has_capability=Mock(return_value=True), require_capability=Mock(return_value=valid))
with patch.object(route, "get_registry", return_value=registry):
self.assertIs(valid, route._identity_directory())
registry.has_capability.assert_called_once_with(CAPABILITY_IDENTITY_DIRECTORY)
registry.require_capability.assert_called_once_with(CAPABILITY_IDENTITY_DIRECTORY)
def test_each_call_resolves_current_registry_without_caching_authority(self) -> None:
for route in (relationships, routes):
valid = Mock(spec=IdentityDirectory)
registry = Mock(has_capability=Mock(return_value=True), require_capability=Mock(return_value=valid))
with patch.object(route, "get_registry", side_effect=[registry, None]) as get_registry:
self.assertIs(valid, route._identity_directory())
with self.assertRaises(HTTPException) as caught:
route._identity_directory()
self.assertEqual(503, caught.exception.status_code)
self.assertEqual(2, get_registry.call_count)
def test_lookup_failure_is_not_silently_replaced_or_retried(self) -> None:
failure = RuntimeError("registry changed during lookup")
for route in (relationships, routes):
registry = Mock(has_capability=Mock(return_value=True), require_capability=Mock(side_effect=failure))
with patch.object(route, "get_registry", return_value=registry), self.assertRaises(RuntimeError) as caught:
route._identity_directory()
self.assertIs(failure, caught.exception)
self.assertEqual(1, registry.require_capability.call_count)
def test_function_changes_keeps_its_distinct_unavailable_contract(self) -> None:
registry = SimpleNamespace(capability=lambda _name: object())
with patch.object(function_changes, "get_registry", return_value=registry), self.assertRaises(HTTPException) as caught:
function_changes._identity_directory()
self.assertEqual(503, caught.exception.status_code)
self.assertEqual("The Identity directory is unavailable.", caught.exception.detail)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/idm-webui",
"version": "0.1.25",
"version": "0.1.26",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -349,11 +349,11 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
<Card
bodyLayout="table"
title="Function requests and grants"
titleHelp={<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />}
collapsible
collapseKey="idm.function-assignment-changes"
actions={(
<div className="button-row compact-actions">
<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />
{(canRequest || canGrant) ? <AdminIconButton label="Start governed change" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={busy} disabledReason={idmDisabledReason(false, busy)} onClick={openCreate} /> : null}
</div>
)}
+4 -5
View File
@@ -10,6 +10,7 @@ import { FormLayout, ActionToolbar,
Dialog,
DismissibleAlert,
DocumentationHelpLink,
TextWithHelp,
FormField,
LoadingFrame,
PageScrollViewport,
@@ -696,11 +697,10 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
<div className="content-pad idm-page">
<div className="page-heading split idm-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-idm.idm.61f4a7a2</PageTitle>
<PageTitle loading={loading} titleHelp={<DocumentationHelpLink reference={IDM_DOCUMENTATION} />}>i18n:govoplan-idm.idm.61f4a7a2</PageTitle>
<p>i18n:govoplan-idm.identity_links_intro.45fed9dd</p>
</div>
<ActionToolbar justify="end" className="idm-toolbar">
<DocumentationHelpLink reference={IDM_DOCUMENTATION} />
<Button
type="button"
onClick={() => requestDiscard(() => void loadData())}
@@ -753,9 +753,9 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
{canReadSettings && (
<Card
title="i18n:govoplan-idm.idm_governance.6e4f3251"
titleHelp={<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />}
collapsible
collapseKey="idm.governance"
actions={<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />}
>
<FormLayout columns={2} gap="small" collapseAt="workspace" className="" onSubmit={(event) => { event.preventDefault(); void submitSettings(); }}>
<div className="idm-check-list wide">
@@ -946,8 +946,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
{selectedFunctionIsGoverned && (
<div className="wide idm-governance-override">
<DismissibleAlert tone="warning" dismissible={false}>
Direct changes to this governed function are emergency overrides. Use a request or grant above for the normal process.
<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />
Direct changes to this governed function are <TextWithHelp help={<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />}>emergency overrides</TextWithHelp>. Use a request or grant above for the normal process.
</DismissibleAlert>
<FormField label="Emergency override reason" documentation={IDM_FIELD_DOCUMENTATION}>
<textarea
@@ -390,11 +390,11 @@ export default function TypedRelationshipsPanel({ settings, auth }: Props) {
<Card
bodyLayout="table"
title="Typed groups"
titleHelp={<DocumentationHelpLink reference={IDM_RELATIONSHIP_DOCUMENTATION} />}
collapsible
collapseKey="idm.typed-groups"
actions={(
<ActionToolbar justify="end">
<DocumentationHelpLink reference={IDM_RELATIONSHIP_DOCUMENTATION} />
<ToggleSwitch
label="Show inactive groups"
checked={showInactiveGroups}
@@ -424,11 +424,11 @@ export default function TypedRelationshipsPanel({ settings, auth }: Props) {
<Card
bodyLayout="table"
title="Effective identity relationships"
titleHelp={<DocumentationHelpLink reference={IDM_RELATIONSHIP_DOCUMENTATION} />}
collapsible
collapseKey="idm.identity-relationships"
actions={(
<ActionToolbar justify="end">
<DocumentationHelpLink reference={IDM_RELATIONSHIP_DOCUMENTATION} />
<ToggleSwitch
label="Show revoked relationships"
checked={showRevokedRelationships}
+6
View File
@@ -2,6 +2,9 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
en: {
"Direct changes to this governed function are": "Direct changes to this governed function are",
"emergency overrides": "emergency overrides",
". Use a request or grant above for the normal process.": ". Use a request or grant above for the normal process.",
"i18n:govoplan-idm.account.2b2936f8": "Account",
"i18n:govoplan-idm.active.7bd0e9f8": "Active",
"i18n:govoplan-idm.acting_for.8650e6a6": "acting for",
@@ -255,6 +258,9 @@ export const generatedTranslations: PlatformTranslations = {
"You may inspect relationship evidence but not change it.": "You may inspect relationship evidence but not change it."
},
de: {
"Direct changes to this governed function are": "Direkte Änderungen an dieser gesteuerten Funktion sind",
"emergency overrides": "Notfallübersteuerungen",
". Use a request or grant above for the normal process.": ". Verwenden Sie für den regulären Prozess einen Antrag oder eine Vergabe.",
"i18n:govoplan-idm.account.2b2936f8": "Konto",
"i18n:govoplan-idm.active.7bd0e9f8": "Aktiv",
"i18n:govoplan-idm.acting_for.8650e6a6": "in Vertretung",