6 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
zemion 46e09d0c68 Release govoplan-idm v0.1.25: unify interface contracts and documentation
Module Package Release / publish-packages (push) Successful in 11s
2026-09-08 01:32:43 +02:00
zemion 5c7586f6d9 fix(webui): bind identity retention to help
Module Package Release / publish-packages (push) Successful in 12s
2026-08-24 11:36:40 +02:00
zemion b2a641cac2 docs: complete German structured documentation
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 01:15:34 +02:00
zemion e9f8e0a1f8 docs(idm): complete German reference coverage
Module Package Release / publish-packages (push) Successful in 11s
2026-08-23 20:19:15 +02:00
16 changed files with 544 additions and 73 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/idm-webui",
"version": "0.1.18",
"version": "0.1.26",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
@@ -19,7 +19,7 @@
"LICENSE"
],
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@govoplan/core-webui": "^0.1.45",
"@vitejs/plugin-react": "^5.2.0",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
+2 -2
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-idm"
version = "0.1.21"
version = "0.1.26"
description = "GovOPlaN identity management bridge module."
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.29",
"govoplan-core>=0.1.45",
"govoplan-identity>=0.1.18",
"govoplan-organizations>=0.1.18",
]
+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:
@@ -0,0 +1,190 @@
"""German translations for public structured documentation metadata."""
from __future__ import annotations
from typing import Any
GERMAN_STRUCTURED_TRANSLATIONS: dict[str, dict[str, Any]] = {'idm.reference.assignment-governance': {'consequence_classes': {'emergency_override': 'Umgeht den '
'normalen '
'geregelten '
'Antrags- '
'oder '
'Gewährungspfad '
'und '
'erfordert '
'beibehaltene '
'Gründe und '
'Nachweise.',
'function_decision': 'Erweitert '
'oder '
'beendet '
'eine '
'geregelte '
'Änderung '
'und behält '
'handelnde '
'Person, '
'Kommentar, '
'Politik und '
'Workflow-Nachweise.',
'governance_settings': 'Ändert, '
'ob '
'direkte '
'Zuordnungsmutationen '
'genehmigte '
'Änderungsnachweise '
'erfordern.',
'timed_escalation': 'Zeichnet '
'eine '
'überfällige '
'Überprüfung '
'und genaue '
'Zielfunktion '
'auf, ohne '
'eine '
'Genehmigung '
'zu ersetzen '
'oder '
'abzuschließen.'}},
'idm.reference.fields-and-consequences': {'consequence_classes': {'acting_for': 'Ermöglicht es '
'einem gebundenen '
'Konto, anstelle '
'einer '
'Quellzuweisung '
'zu handeln, wenn '
'Organisationen '
'dies zulassen.',
'assignment': 'Ändert die '
'effektive '
'institutionelle '
'Funktion, die '
'von optionalen '
'nachgelagerten '
'Fähigkeiten '
'verbraucht wird.',
'deactivate_or_expire': 'Entfernt '
'die '
'Tatsache '
'aus '
'der '
'effektiven '
'Auflösung, '
'während '
'Provenienz '
'und '
'Lebenszyklus '
'Nachweise '
'beibehalten.',
'delegation': 'Erstellt eine '
'begrenzte '
'abgeleitete '
'Zuweisung, die '
'an die '
'Quellzuweisung '
'gebunden bleibt.',
'escalation': 'Leitet eine '
'überfällige '
'Überprüfung '
'sichtbar zu '
'einer genau '
'konfigurierten '
'Funktion, ohne '
'die Entscheidung '
'abzuschließen.',
'retention': 'Ändert, wie lange '
'detaillierte '
'Zuordnungsänderungsnachweise '
'verfügbar '
'bleiben.'}},
'idm.reference.typed-relationships': {'consequences': ['Ein zukünftiger Start verzögert die '
'Mitgliedschaft bis zum ausgewählten '
'Zeitpunkt.',
'Expiry entfernt die Beziehung von der '
'effektiven Auflösung, während Nachweise '
'aufbewahrt werden.',
'Der Widerruf entfernt die Beziehung '
'sofort von der effektiven Auflösung und '
'kann nicht rückgängig gemacht werden.',
'Das Ändern einer extern beschafften '
'Tatsache ohne übereinstimmende Provenienz '
'kann die Verantwortlichkeit für die '
'Abstimmung unterbrechen.'],
'limitations': ['Mitgliedschaftsbeschlüsse sind '
'mieterspezifisch und lehnen '
'mieterübergreifende Gruppenreferenzen ab.',
'Eine widerrufene Beziehung ist '
'unveränderlich und erfordert einen Ersatz '
'für eine spätere Wiederverwendung.',
'Die Mitgliedschaft allein aktiviert '
'niemals eine Identität oder erteilt eine '
'Antragsberechtigung.'],
'outcome': 'Der Mandant hat erklärbare, effektiv datierte '
'Geschäftsmitgliedschaftsfakten, die '
'nachgelagerte Verbraucher lösen können, ohne '
'IDM-Interna zu importieren oder Zugriffsrechte '
'abzuleiten.',
'prerequisites': ['Die Identitäten existieren im '
'Mandanten-Identitätsverzeichnis.',
'Die handelnde Person hat die Berechtigung '
'zum Lesen von Beziehungen und die '
'Berechtigung zum Schreiben von '
'Mutationen.',
'Die verantwortliche Quelle, das '
'effektive Fenster, die Art der Beziehung '
'und der Geschäftszweck sind bekannt.'],
'steps': ['Erstellen oder wählen Sie eine typisierte Gruppe '
'mit einem stabilen Schlüssel, Typ und Herkunft '
'aus.',
'Erstellen Sie eine Beziehung zu durchsuchbaren '
'Betreff- und Zielreferenzen und dem '
'beabsichtigten Gültigkeitsfenster.',
'Überprüfen Sie effektive Mitgliedschaften zum '
'jeweiligen Zeitpunkt und überprüfen Sie jede '
'eingeschlossene oder ausgeschlossene '
'Entscheidung.',
'Widerrufen Sie eine Beziehung mit einem '
'vorgehaltenen Grund, wenn die Tatsache vor ihrem '
'geplanten Ende aufhören muss.'],
'verification': 'Laden Sie beide Verzeichnisse neu, '
'bestätigen Sie die Datensatzrevision und '
'die Quellfelder und lösen Sie dann die '
'Mitgliedschaften der Zielgruppe zu Zeiten '
'vor, während und nach dem '
'Gültigkeitsfenster auf. Stellen Sie '
'sicher, dass Zugriffsberechtigungen '
'unverändert bleiben.'},
'idm.scim-provisioning': {'consequences': ['Ein unvollständiger oder fehlgeschlagener Snapshot '
'kann eine lokale Identität nicht deaktivieren.',
'Ein geänderter unveränderlicher Wert oder eine '
'Kollision blockiert die automatische Verknüpfung.',
'Ein vollständiger Snapshot kann nur dann eine '
'Deaktivierung vorschlagen, wenn die Mandantrichtlinie '
'ihn explizit auswählt.'],
'limitations': ['Diese Version zeigt eine Vorschau, wendet jedoch keine '
'SCIM-Bereitstellungspläne an.',
'Cursor-Paginierung wird nicht verwendet, bis '
'angekündigt und durch einen Provider-Zieltest '
'abgedeckt.',
'Eine SCIM-Gruppenmitgliedschaft wird niemals '
'automatisch zur Zugriffsberechtigung.'],
'prerequisites': ['Der Anbieter stellt RFC 7643 Benutzer- und '
'Gruppenressourcen über SCIM 2.0 zur Verfügung.',
'Ein anbietereigenes unveränderliches '
'Übereinstimmungsattribut wurde ausgewählt und '
'kollisionsgetestet.',
'Die Authentifizierung erfolgt in einem Scoped Access '
'Credential-Umschlag.'],
'steps': ['Lesen Sie jede Benutzer- und Gruppenseite in einem '
'vollständigen Snapshot.',
'Überprüfung von Schema, Paginierung, Kollision und Diagnose '
'von unveränderlichen Werten.',
'Überprüfen Sie jede Erstellung, Verknüpfung, Aktualisierung, '
'Deaktivierung oder Quarantäne und die erwartete lokale '
'Überarbeitung.',
'Entsorgen und neu erstellen Sie den Plan nach jeder '
'Anbieter, Mapping oder lokale Revision Änderung.'],
'verification': 'Vergleichen Sie Seitensummen, Quellen- und '
'Planverdauungen, erwartete lokale Überarbeitungen, '
'Kollisionsdiagnosen und die Abwesenheitsrichtlinien, '
'bevor Sie eine spätere Ausführung genehmigen.'}}
+224 -21
View File
@@ -1,5 +1,8 @@
from __future__ import annotations
from govoplan_core.core.modules import with_documentation_structured_translations
from govoplan_idm.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS
from datetime import UTC, datetime
from pathlib import Path
@@ -66,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.21"
MODULE_VERSION = "0.1.26"
IDM_READ_SCOPES = (
"idm:organization_assignment:read",
@@ -204,7 +207,9 @@ def _idm_directory(context: ModuleContext) -> object:
from govoplan_idm.backend.directory import SqlIdmDirectory
identities = context.registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
organizations = context.registry.require_capability(CAPABILITY_ORGANIZATION_DIRECTORY)
organizations = context.registry.require_capability(
CAPABILITY_ORGANIZATION_DIRECTORY
)
if not isinstance(identities, IdentityDirectory):
raise RuntimeError(f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}")
if not isinstance(organizations, OrganizationDirectory):
@@ -376,12 +381,32 @@ manifest = ModuleManifest(
factory=create_idm_search_source,
),
),
nav_items=(NavItem(path="/idm", label="IDM", icon="users", required_any=IDM_READ_SCOPES, order=72),),
nav_items=(
NavItem(
path="/idm",
label="IDM",
icon="users",
required_any=IDM_READ_SCOPES,
order=72,
),
),
frontend=FrontendModule(
module_id="idm",
package_name="@govoplan/idm-webui",
routes=(FrontendRoute(path="/idm", component="IdmPage", required_any=IDM_READ_SCOPES, order=72),),
nav_items=(NavItem(path="/idm", label="IDM", icon="users", required_any=IDM_READ_SCOPES, order=72),),
routes=(
FrontendRoute(
path="/idm", component="IdmPage", required_any=IDM_READ_SCOPES, order=72
),
),
nav_items=(
NavItem(
path="/idm",
label="IDM",
icon="users",
required_any=IDM_READ_SCOPES,
order=72,
),
),
product_areas=(
ProductAreaContribution(
id="people-responsibility",
@@ -451,6 +476,27 @@ manifest = ModuleManifest(
),
),
documentation=(
DocumentationTopic(
id="idm.workspace-layout",
title="IDM workspace layout",
summary="Find workspace actions and read consistently arranged content.",
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"),
order=5,
translations={"de": {
"title": "Identitätsmanagement: Aufbau des Arbeitsbereichs",
"summary": "Arbeitsbereichsaktionen finden und einheitlich angeordnete Inhalte lesen.",
"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(
id="idm.scim-provisioning",
title="Preview SCIM 2.0 identity provisioning",
@@ -548,9 +594,7 @@ manifest = ModuleManifest(
"organizations",
"records",
),
conditions=(
DocumentationCondition(required_modules=("idm", "access")),
),
conditions=(DocumentationCondition(required_modules=("idm", "access")),),
links=(
DocumentationLink(
label="Data-subject requests",
@@ -563,6 +607,28 @@ manifest = ModuleManifest(
kind="runtime",
),
),
translations={
"de": {
"title": "IDM-Daten in einer Betroffenenanfrage prüfen",
"summary": (
"Mandantenbezogene institutionelle Funktions- und Beziehungsmerkmale erfassen und dabei gesteuerte "
"Entscheidungsnachweise bewahren."
),
"body": (
"IDM durchsucht bestätigte Konto- und Identitätsselektoren sowie namensraumgebundene Zuweisungs-, Beziehungs- und "
"Zuweisungsänderungsverweise. Ergebnisse umfassen zeitlich wirksame Organisationsfunktionszuweisungen, typisierte "
"Identitätsbeziehungen mit minimiertem Gruppenkontext, gesteuerte Zuweisungsanträge oder -erteilungen und zugehörige "
"Lebenszyklusereignisse. Betrifft ein Datensatz eine andere kandidierende oder handelnde Person, werden deren Identitäts- "
"und Kontokennungen aus dem automatischen Export entfernt. Einstellungen, Gruppeneigenschaften, externe Quellverweise, "
"Herkunft, Begründungen, Nachweislisten, Richtlinienentscheidungen, Workflow-Interna, Idempotenzschlüssel, Anfrage-Digests, "
"undurchsichtige Metadaten, Ereigniskommentare und -details, unbeteiligte Datensätze und andere Mandanten sind ausgeschlossen. "
"Zuweisungen und Beziehungen sind wirksame institutionelle Tatsachen; Korrektur, Widerruf, Deaktivierung oder Ablauf "
"erfordern daher eine berechtigte IDM-Lebenszyklusprüfung. Gesteuerte Änderungs- und Ereignisdatensätze bewahren "
"ausdrückliche Gründe für Entscheidungsnachweise. Identity besitzt den Personendatensatz, Organizations Funktionen und "
"Einheiten und Access die aus bestätigten IDM-Tatsachen abgeleitete Befugnis."
),
}
},
metadata={
"kind": "guide",
"help_contexts": [
@@ -587,6 +653,21 @@ manifest = ModuleManifest(
documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin", "operator"),
related_modules=("search", "identity", "organizations"),
translations={
"de": {
"title": "Berechtigte IDM-Datensätze durchsuchen",
"summary": (
"Typisierte Gruppen, zeitlich wirksame Beziehungen und Organisationsfunktionszuweisungen für die "
"berechtigungsbewusste Plattform-Suche bereitstellen."
),
"body": (
"Ist Search installiert, liefert IDM begrenzte Verzeichnis- und Zuweisungsmetadaten, ohne uneingeschränkte "
"Herkunftsdaten zu kopieren. Jedes Ergebnis bleibt mandantengebunden und prüft die aktuelle Leseberechtigung für "
"Zuweisung oder Beziehung erneut. Festgeschriebene IDM-Lebenszyklusereignisse aktualisieren den abgeleiteten Index; ein "
"betrieblicher Neuaufbau gleicht Datensätze ab, die vor der Aktivierung von Search angelegt wurden."
),
}
},
order=25,
),
DocumentationTopic(
@@ -602,6 +683,20 @@ manifest = ModuleManifest(
documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin", "operator"),
related_modules=("identity", "organizations", "access"),
translations={
"de": {
"title": "Brücke zwischen Identität und Organisation",
"summary": (
"IDM löst auf, welche Identitäten und Konten Organisationsfunktionszuweisungen zugeordnet werden können."
),
"body": (
"Identity besitzt normalisierte Identitäten und Kontoverknüpfungen. Organizations besitzt Einheiten und Funktionen. "
"IDM besitzt die Zuweisungsverknüpfungen zwischen Identitäten und Organisationsfunktionen, einschließlich Identitätssuche "
"für Organisationszuweisungen und zukünftiger Synchronisations-/Zuordnungsabläufe. Access darf diese Verknüpfungen nutzen, "
"wenn IDM installiert ist; IDM benötigt Access jedoch nicht, um Rechte auszuwerten."
),
}
},
order=26,
),
DocumentationTopic(
@@ -631,6 +726,26 @@ manifest = ModuleManifest(
kind="api",
),
),
translations={
"de": {
"title": "Governance von IDM-Zuweisungen",
"summary": (
"Mandantenbezogene IDM-Einstellungen können freigegebene Änderungsanträge verlangen, bevor "
"Identitäts-Funktionszuweisungen angewendet werden."
),
"body": (
"Zuweisungsverknüpfungen haben hohe Auswirkung, weil sie später Zugriffsentscheidungen speisen können. Mandanten können "
"aufgezeichnete Änderungsanträge für Anlage und Aktualisierung von Zuweisungen verlangen. Zusätzlich lassen sich "
"Obergrenzen für Tiefe und Gültigkeit von Delegationsketten sowie ausdrückliche Eskalationsziele und Fristen für Prüfungen "
"durch Inhabende, verantwortliche Stelle oder Empfangende konfigurieren. IDM prüft vollständige Wege bei jeder "
"Entscheidung und der endgültigen Anwendung gegen die aktuelle Policy. Abgelaufene Fristen werden als sichtbarer "
"eskalierter Zustand mit Notifications- und Auditnachweis erfasst; eine Freigabe wird nicht unterstellt. Ein periodischer "
"Worker sendet genau ein Ablaufereignis, wenn eine zukünftige Zuweisung endet. Kennzeichnung und Ereignis werden gemeinsam "
"festgeschrieben, damit Wiederholungen idempotent bleiben. Der alte Bereich organizations:function:assign bleibt während "
"des Übergangs gültig; neue Rollenvorlagen sollten idm:organization_assignment:write gewähren."
),
}
},
metadata={
"kind": "reference",
"help_contexts": [
@@ -672,10 +787,24 @@ manifest = ModuleManifest(
),
),
links=(
DocumentationLink(label="IDM relationship administration", href="/idm", kind="runtime"),
DocumentationLink(label="Typed groups API", href="/api/v1/idm/typed-groups", kind="api"),
DocumentationLink(label="Identity relationships API", href="/api/v1/idm/relationships", kind="api"),
DocumentationLink(label="Typed relationship contract", href="docs/TYPED_RELATIONSHIPS.md", kind="repository"),
DocumentationLink(
label="IDM relationship administration", href="/idm", kind="runtime"
),
DocumentationLink(
label="Typed groups API",
href="/api/v1/idm/typed-groups",
kind="api",
),
DocumentationLink(
label="Identity relationships API",
href="/api/v1/idm/relationships",
kind="api",
),
DocumentationLink(
label="Typed relationship contract",
href="docs/TYPED_RELATIONSHIPS.md",
kind="repository",
),
),
translations={
"de": {
@@ -779,9 +908,7 @@ manifest = ModuleManifest(
documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin", "operator", "user"),
related_modules=("identity", "organizations", "access"),
conditions=(
DocumentationCondition(any_scopes=IDM_READ_SCOPES),
),
conditions=(DocumentationCondition(any_scopes=IDM_READ_SCOPES),),
links=(
DocumentationLink(
label="IDM assignments",
@@ -794,6 +921,24 @@ manifest = ModuleManifest(
kind="api",
),
),
translations={
"de": {
"title": "Einer Identität eine Organisationsfunktion zuweisen",
"summary": (
"IDM verknüpft Identitäten oder Konten mit Organisationsfunktionen; Access kann bestätigte Verknüpfungen bei "
"vorhandener Funktions-Rollenzuordnung verwenden."
),
"body": (
"Legen Sie zuerst Einheit und Funktion in Organizations an und stellen Sie sicher, dass Person und Konto in Identity "
"bestehen. Erstellen Sie anschließend die Zuweisung in IDM. Direkte Zuweisungen halten fest, wer die Funktion innehat. "
"Delegierte Zuweisungen benötigen eine Quellzuweisung und eine delegierbare Funktion. Stellvertretungszuweisungen benötigen "
"Quellzuweisung, handelndes Konto und eine Funktion, die Handeln an Stelle zulässt. Access bildet bestätigte "
"Funktionsmerkmale auf Rollen und Rechte ab; ohne eine solche Zuordnung wird die Zuweisung gespeichert, gewährt aber keine "
"Anwendungsberechtigungen. Der Zuweisungsarbeitsbereich nutzt die verfügbare Anwendungsbreite, sodass Governance-Steuerungen "
"und Zuweisungsdaten gemeinsam sichtbar bleiben."
),
}
},
metadata={
"kind": "workflow",
"help_contexts": [
@@ -835,7 +980,14 @@ manifest = ModuleManifest(
layer="configured",
documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin", "operator", "user"),
related_modules=("identity", "organizations", "access", "policy", "audit", "workflow_engine"),
related_modules=(
"identity",
"organizations",
"access",
"policy",
"audit",
"workflow_engine",
),
links=(
DocumentationLink(
label="Function assignment workflows",
@@ -843,6 +995,30 @@ manifest = ModuleManifest(
kind="repository",
),
),
translations={
"de": {
"title": "Felder und Folgen von IDM-Zuweisungen",
"summary": (
"Referenz für direkte Zuweisungen, Delegation, Stellvertretung, Wirksamkeitsdaten, gesteuerte Änderungen, Nachweise und "
"Aufbewahrung."
),
"body": (
"Identität und Konto wählen, wer die institutionelle Tatsache erhält; Funktion und Einheit gehören Organizations. Die "
"Quelle unterscheidet direkte, delegierte, stellvertretende, Verzeichnis-, Governance- und Systemtatsachen. Delegation und "
"Stellvertretung verlangen eine gültige Quellzuweisung und die entsprechende Organizations-Funktionsberechtigung. Eine "
"delegierte Person handelt als sie selbst; bei Stellvertretung muss Access zusätzlich den exakten Repräsentationskontext "
"auswählen, bevor daraus Befugnis entsteht. Quell- und abgeleitete Zuweisungen müssen aktuell, aktiv, mandantenlokal und "
"funktionskompatibel bleiben. Der Untereinheitenbereich erweitert die organisatorische Reichweite. Deaktivierung und Ablauf "
"bewahren die Herkunft, entfernen die Zuweisung aber aus der wirksamen Auflösung. Entscheidungen zu gesteuerten Anträgen "
"und Erteilungen bewahren handelnde Person, Richtlinie, Workflow-Revision, Kommentare und Nachweise. Delegierte Befugnis "
"wird über die vollständige Quellkette gegen aktuelle Tiefen- und Gültigkeitsgrenzen geprüft. Eine konfigurierte Frist pro "
"Schritt erzeugt einen sichtbaren eskalierten Zustand und eine exakte Zielfunktionsroute; sie ersetzt oder protokolliert "
"niemals automatisch eine freigebende Person. Eine Notfallüberschreibung ist nicht der Normalweg und benötigt einen "
"ausdrücklichen Grund. Eine IDM-Zuweisung allein gewährt niemals Anwendungsberechtigungen; Access verlangt eine "
"ausdrückliche Zuordnung."
),
}
},
metadata={
"kind": "reference",
"help_contexts": [
@@ -878,16 +1054,43 @@ manifest = ModuleManifest(
maturity="vertical_slice",
documentation_ref="docs/FUNCTION_ASSIGNMENT_WORKFLOWS.md",
test_ref="tests/test_assignment_workflow.py",
known_limits=("SCIM provisioning is a deterministic preview; governed plan execution is not implemented yet.",),
known_limits=(
"SCIM provisioning is a deterministic preview; governed plan execution is not implemented yet.",
),
supported_authority_modes=("external_authoritative", "external_mirror"),
owned_concepts=("function assignment", "assignment delegation", "acting-for assignment", "assignment request", "typed group", "identity relationship"),
non_owned_concepts=("identity", "organization function", "application role", "workflow runtime"),
recovery_docs=("docs/FUNCTION_ASSIGNMENT_WORKFLOWS.md", "docs/TYPED_RELATIONSHIPS.md", "docs/SCIM_PROVISIONING.md"),
security_docs=("docs/FUNCTION_ASSIGNMENT_WORKFLOWS.md", "docs/TYPED_RELATIONSHIPS.md", "docs/SCIM_PROVISIONING.md"),
owned_concepts=(
"function assignment",
"assignment delegation",
"acting-for assignment",
"assignment request",
"typed group",
"identity relationship",
),
non_owned_concepts=(
"identity",
"organization function",
"application role",
"workflow runtime",
),
recovery_docs=(
"docs/FUNCTION_ASSIGNMENT_WORKFLOWS.md",
"docs/TYPED_RELATIONSHIPS.md",
"docs/SCIM_PROVISIONING.md",
),
security_docs=(
"docs/FUNCTION_ASSIGNMENT_WORKFLOWS.md",
"docs/TYPED_RELATIONSHIPS.md",
"docs/SCIM_PROVISIONING.md",
),
operations_docs=("README.md", "docs/SCIM_PROVISIONING.md"),
),
)
manifest = with_documentation_structured_translations(
manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS
)
def get_manifest() -> ModuleManifest:
return manifest
+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()
+15 -2
View File
@@ -7,6 +7,14 @@ from govoplan_idm.backend.api.v1.routes import ORGANIZATION_IDENTITY_READ_SCOPES
class IdmInterfaceDocumentationContractTests(unittest.TestCase):
def test_all_static_topics_have_complete_german_content(self) -> None:
for topic in manifest.documentation:
german = (topic.translations or {}).get("de", {})
self.assertEqual({"title", "summary", "body"}, set(german), topic.id)
self.assertTrue(
all(str(value).strip() for value in german.values()), topic.id
)
def test_route_and_contributed_action_remain_declared(self) -> None:
frontend = manifest.frontend
self.assertIsNotNone(frontend)
@@ -54,8 +62,13 @@ class IdmInterfaceDocumentationContractTests(unittest.TestCase):
"idm.typed-groups.action.resolve-memberships",
relationships.metadata["help_contexts"],
)
self.assertIn("Revocation immediately", relationships.metadata["consequences"][2])
self.assertIn("Access permissions remain unchanged", relationships.metadata["verification"])
self.assertIn(
"Revocation immediately", relationships.metadata["consequences"][2]
)
self.assertIn(
"Access permissions remain unchanged",
relationships.metadata["verification"],
)
def test_relationship_writers_may_use_identity_search_selectors(self) -> None:
self.assertIn("idm:relationship:write", ORGANIZATION_IDENTITY_READ_SCOPES)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/idm-webui",
"version": "0.1.21",
"version": "0.1.26",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -17,7 +17,7 @@
"./styles/idm.css": "./src/styles/idm.css"
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@govoplan/core-webui": "^0.1.45",
"@vitejs/plugin-react": "^5.2.0",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
@@ -38,5 +38,7 @@ assert(api.includes("/api/v1/idm/typed-groups") && api.includes("/api/v1/idm/rel
assert(moduleSource.includes('"idm:relationship:read"') && moduleSource.includes('"idm:relationship:write"'), "Relationship-only administrators can enter the IDM product surface");
assert(translations.includes('"Typed groups and identity relationships": "Typisierte Gruppen und Identitätsbeziehungen"') && translations.includes('"Revoked": "Widerrufen"'), "The relationship administration vocabulary has German reference translations");
assert(!relationships.includes("window.confirm"), "Relationship administration does not use browser-native consequential confirmation");
assert(relationships.includes('<ContentGrid columns={1}>') && (relationships.match(/bodyLayout="table"/g) ?? []).length === 2, "Typed groups and effective relationships use spaced full-width table cards");
assert(!relationships.includes('className="idm-muted idm-card-note"') && changes.includes('bodyLayout="table"') && page.includes('bodyLayout="table"'), "All IDM collection tables share the table-card treatment without a repeated tagline");
console.log("IDM surfaces satisfy the recorded interface pattern-language contract.");
@@ -347,12 +347,13 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
<>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<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>
)}
+6 -7
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">
@@ -772,7 +772,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
<option value="full">i18n:govoplan-idm.full.7f021a14</option>
</select>
</FormField>
<FormField label="i18n:govoplan-idm.change_retention_days.4a91f7d3" documentation={IDM_FIELD_DOCUMENTATION}>
<FormField label="i18n:govoplan-idm.change_retention_days.4a91f7d3" documentation={IDM_FIELD_DOCUMENTATION} helpContextId="idm.field.retention" helpModuleId="idm">
<input
type="number"
min="0"
@@ -835,7 +835,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
<TypedRelationshipsPanel settings={settings} auth={auth} />
{canReadAssignments && <Card title="i18n:govoplan-idm.assignments.a0d19ec5" collapsible collapseKey="idm.assignments" actions={<AdminIconButton label="i18n:govoplan-idm.add_assignment.08f2a0d5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canManage || busy || !model.functions.length} disabledReason={idmDisabledReason(false, busy, canManage) ?? (!model.functions.length ? IDM_INTERFACE_I18N.noFunctions : undefined)} onClick={openCreateAssignment} />}>
{canReadAssignments && <Card bodyLayout="table" title="i18n:govoplan-idm.assignments.a0d19ec5" collapsible collapseKey="idm.assignments" actions={<AdminIconButton label="i18n:govoplan-idm.add_assignment.08f2a0d5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canManage || busy || !model.functions.length} disabledReason={idmDisabledReason(false, busy, canManage) ?? (!model.functions.length ? IDM_INTERFACE_I18N.noFunctions : undefined)} onClick={openCreateAssignment} />}>
<DataGrid
id="idm-organization-function-assignments"
rows={assignments}
@@ -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
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react";
import { Eye, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
import {
ActionBlockerHint,
@@ -7,6 +7,7 @@ import {
ApiError,
Button,
Card,
ContentGrid,
DataGrid,
DateTimeField,
Dialog,
@@ -385,13 +386,15 @@ export default function TypedRelationshipsPanel({ settings, auth }: Props) {
)}
<LoadingFrame loading={loading} label="Loading typed groups and relationships">
<ContentGrid columns={1}>
<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}
@@ -419,12 +422,13 @@ export default function TypedRelationshipsPanel({ settings, auth }: Props) {
</Card>
<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}
@@ -449,8 +453,8 @@ export default function TypedRelationshipsPanel({ settings, auth }: Props) {
)}
>
<DataGrid id="idm-identity-relationships" rows={relationships} columns={relationshipColumns} getRowKey={(row) => row.id} emptyText="No identity relationships found." initialFit="container" />
<p className="idm-muted idm-card-note">Business membership is an institutional fact. It does not grant application permissions; Access evaluates authority separately.</p>
</Card>
</ContentGrid>
</LoadingFrame>
{renderGroupEditor()}
+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",
-4
View File
@@ -24,10 +24,6 @@
width: 100%;
}
.idm-card-note {
margin-top: 12px;
}
.idm-check-list {
display: grid;
gap: 10px;