Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2a39d7c4f | ||
|
|
ba04593e29 | ||
|
|
46e09d0c68 | ||
|
|
5c7586f6d9 | ||
|
|
b2a641cac2 |
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/idm-webui",
|
"name": "@govoplan/idm-webui",
|
||||||
"version": "0.1.22",
|
"version": "0.1.26",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.45",
|
||||||
"@vitejs/plugin-react": "^5.2.0",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
|
|||||||
+2
-2
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-idm"
|
name = "govoplan-idm"
|
||||||
version = "0.1.22"
|
version = "0.1.26"
|
||||||
description = "GovOPlaN identity management bridge module."
|
description = "GovOPlaN identity management bridge module."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core>=0.1.29",
|
"govoplan-core>=0.1.45",
|
||||||
"govoplan-identity>=0.1.18",
|
"govoplan-identity>=0.1.18",
|
||||||
"govoplan-organizations>=0.1.18",
|
"govoplan-organizations>=0.1.18",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,
|
emit_platform_event,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.identity import (
|
from govoplan_core.core.identity import (
|
||||||
CAPABILITY_IDENTITY_DIRECTORY,
|
|
||||||
IdentityDirectory,
|
IdentityDirectory,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.idm import (
|
from govoplan_core.core.idm import (
|
||||||
@@ -34,6 +33,7 @@ from govoplan_idm.backend.db.models import (
|
|||||||
IdmTypedGroup,
|
IdmTypedGroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .directory_dependencies import require_identity_directory
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
IdentityRelationshipCreateRequest,
|
IdentityRelationshipCreateRequest,
|
||||||
IdentityRelationshipDecisionItem,
|
IdentityRelationshipDecisionItem,
|
||||||
@@ -154,19 +154,7 @@ def _tenant_row(session: Session, model, item_id: str, tenant_id: str, label: st
|
|||||||
|
|
||||||
|
|
||||||
def _identity_directory() -> IdentityDirectory:
|
def _identity_directory() -> IdentityDirectory:
|
||||||
registry = get_registry()
|
return require_identity_directory(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
|
|
||||||
|
|
||||||
|
|
||||||
def _relationship_directory() -> IdmRelationshipDirectory:
|
def _relationship_directory() -> IdmRelationshipDirectory:
|
||||||
|
|||||||
@@ -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.principal_cache import invalidate_auth_principals
|
||||||
from govoplan_core.core.identity import (
|
from govoplan_core.core.identity import (
|
||||||
CAPABILITY_IDENTITY_DIRECTORY,
|
|
||||||
CAPABILITY_IDENTITY_SEARCH,
|
CAPABILITY_IDENTITY_SEARCH,
|
||||||
IdentityDirectory,
|
IdentityDirectory,
|
||||||
IdentityRef,
|
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.assignment_events import emit_assignment_event
|
||||||
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings
|
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings
|
||||||
|
|
||||||
|
from .directory_dependencies import require_identity_directory
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
IdmSettingsItem,
|
IdmSettingsItem,
|
||||||
IdmSettingsUpdateRequest,
|
IdmSettingsUpdateRequest,
|
||||||
@@ -187,19 +187,7 @@ def _default_settings(tenant_id: str) -> IdmSettingsItem:
|
|||||||
|
|
||||||
|
|
||||||
def _identity_directory() -> IdentityDirectory:
|
def _identity_directory() -> IdentityDirectory:
|
||||||
registry = get_registry()
|
return require_identity_directory(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
|
|
||||||
|
|
||||||
|
|
||||||
def _identity_search() -> IdentitySearchProvider:
|
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.'}}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
from __future__ import annotations
|
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 datetime import UTC, datetime
|
||||||
from pathlib import Path
|
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
|
from govoplan_idm.backend.scim import SCIM_EXTERNAL_PROVIDER_ID
|
||||||
|
|
||||||
|
|
||||||
MODULE_VERSION = "0.1.22"
|
MODULE_VERSION = "0.1.26"
|
||||||
|
|
||||||
IDM_READ_SCOPES = (
|
IDM_READ_SCOPES = (
|
||||||
"idm:organization_assignment:read",
|
"idm:organization_assignment:read",
|
||||||
@@ -473,6 +476,27 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
documentation=(
|
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(
|
DocumentationTopic(
|
||||||
id="idm.scim-provisioning",
|
id="idm.scim-provisioning",
|
||||||
title="Preview SCIM 2.0 identity provisioning",
|
title="Preview SCIM 2.0 identity provisioning",
|
||||||
@@ -1063,5 +1087,10 @@ manifest = ModuleManifest(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
manifest = with_documentation_structured_translations(
|
||||||
|
manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_manifest() -> ModuleManifest:
|
def get_manifest() -> ModuleManifest:
|
||||||
return manifest
|
return manifest
|
||||||
|
|||||||
Executable
+61
@@ -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()
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/idm-webui",
|
"name": "@govoplan/idm-webui",
|
||||||
"version": "0.1.22",
|
"version": "0.1.26",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"./styles/idm.css": "./src/styles/idm.css"
|
"./styles/idm.css": "./src/styles/idm.css"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.45",
|
||||||
"@vitejs/plugin-react": "^5.2.0",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"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(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(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("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.");
|
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>}
|
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||||
<Card
|
<Card
|
||||||
|
bodyLayout="table"
|
||||||
title="Function requests and grants"
|
title="Function requests and grants"
|
||||||
|
titleHelp={<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />}
|
||||||
collapsible
|
collapsible
|
||||||
collapseKey="idm.function-assignment-changes"
|
collapseKey="idm.function-assignment-changes"
|
||||||
actions={(
|
actions={(
|
||||||
<div className="button-row compact-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}
|
{(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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { FormLayout, ActionToolbar,
|
|||||||
Dialog,
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
DocumentationHelpLink,
|
DocumentationHelpLink,
|
||||||
|
TextWithHelp,
|
||||||
FormField,
|
FormField,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
@@ -696,11 +697,10 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
<div className="content-pad idm-page">
|
<div className="content-pad idm-page">
|
||||||
<div className="page-heading split idm-heading">
|
<div className="page-heading split idm-heading">
|
||||||
<div>
|
<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>
|
<p>i18n:govoplan-idm.identity_links_intro.45fed9dd</p>
|
||||||
</div>
|
</div>
|
||||||
<ActionToolbar justify="end" className="idm-toolbar">
|
<ActionToolbar justify="end" className="idm-toolbar">
|
||||||
<DocumentationHelpLink reference={IDM_DOCUMENTATION} />
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => requestDiscard(() => void loadData())}
|
onClick={() => requestDiscard(() => void loadData())}
|
||||||
@@ -753,9 +753,9 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
{canReadSettings && (
|
{canReadSettings && (
|
||||||
<Card
|
<Card
|
||||||
title="i18n:govoplan-idm.idm_governance.6e4f3251"
|
title="i18n:govoplan-idm.idm_governance.6e4f3251"
|
||||||
|
titleHelp={<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />}
|
||||||
collapsible
|
collapsible
|
||||||
collapseKey="idm.governance"
|
collapseKey="idm.governance"
|
||||||
actions={<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />}
|
|
||||||
>
|
>
|
||||||
<FormLayout columns={2} gap="small" collapseAt="workspace" className="" onSubmit={(event) => { event.preventDefault(); void submitSettings(); }}>
|
<FormLayout columns={2} gap="small" collapseAt="workspace" className="" onSubmit={(event) => { event.preventDefault(); void submitSettings(); }}>
|
||||||
<div className="idm-check-list wide">
|
<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>
|
<option value="full">i18n:govoplan-idm.full.7f021a14</option>
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -835,7 +835,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
|
|
||||||
<TypedRelationshipsPanel settings={settings} auth={auth} />
|
<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
|
<DataGrid
|
||||||
id="idm-organization-function-assignments"
|
id="idm-organization-function-assignments"
|
||||||
rows={assignments}
|
rows={assignments}
|
||||||
@@ -946,8 +946,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
{selectedFunctionIsGoverned && (
|
{selectedFunctionIsGoverned && (
|
||||||
<div className="wide idm-governance-override">
|
<div className="wide idm-governance-override">
|
||||||
<DismissibleAlert tone="warning" dismissible={false}>
|
<DismissibleAlert tone="warning" dismissible={false}>
|
||||||
Direct changes to this governed function are emergency overrides. Use a request or grant above for the normal process.
|
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.
|
||||||
<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />
|
|
||||||
</DismissibleAlert>
|
</DismissibleAlert>
|
||||||
<FormField label="Emergency override reason" documentation={IDM_FIELD_DOCUMENTATION}>
|
<FormField label="Emergency override reason" documentation={IDM_FIELD_DOCUMENTATION}>
|
||||||
<textarea
|
<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 { Eye, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
ActionBlockerHint,
|
ActionBlockerHint,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ApiError,
|
ApiError,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
ContentGrid,
|
||||||
DataGrid,
|
DataGrid,
|
||||||
DateTimeField,
|
DateTimeField,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -385,13 +386,15 @@ export default function TypedRelationshipsPanel({ settings, auth }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<LoadingFrame loading={loading} label="Loading typed groups and relationships">
|
<LoadingFrame loading={loading} label="Loading typed groups and relationships">
|
||||||
|
<ContentGrid columns={1}>
|
||||||
<Card
|
<Card
|
||||||
|
bodyLayout="table"
|
||||||
title="Typed groups"
|
title="Typed groups"
|
||||||
|
titleHelp={<DocumentationHelpLink reference={IDM_RELATIONSHIP_DOCUMENTATION} />}
|
||||||
collapsible
|
collapsible
|
||||||
collapseKey="idm.typed-groups"
|
collapseKey="idm.typed-groups"
|
||||||
actions={(
|
actions={(
|
||||||
<ActionToolbar justify="end">
|
<ActionToolbar justify="end">
|
||||||
<DocumentationHelpLink reference={IDM_RELATIONSHIP_DOCUMENTATION} />
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label="Show inactive groups"
|
label="Show inactive groups"
|
||||||
checked={showInactiveGroups}
|
checked={showInactiveGroups}
|
||||||
@@ -419,12 +422,13 @@ export default function TypedRelationshipsPanel({ settings, auth }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
|
bodyLayout="table"
|
||||||
title="Effective identity relationships"
|
title="Effective identity relationships"
|
||||||
|
titleHelp={<DocumentationHelpLink reference={IDM_RELATIONSHIP_DOCUMENTATION} />}
|
||||||
collapsible
|
collapsible
|
||||||
collapseKey="idm.identity-relationships"
|
collapseKey="idm.identity-relationships"
|
||||||
actions={(
|
actions={(
|
||||||
<ActionToolbar justify="end">
|
<ActionToolbar justify="end">
|
||||||
<DocumentationHelpLink reference={IDM_RELATIONSHIP_DOCUMENTATION} />
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label="Show revoked relationships"
|
label="Show revoked relationships"
|
||||||
checked={showRevokedRelationships}
|
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" />
|
<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>
|
</Card>
|
||||||
|
</ContentGrid>
|
||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
|
|
||||||
{renderGroupEditor()}
|
{renderGroupEditor()}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
|
|||||||
|
|
||||||
export const generatedTranslations: PlatformTranslations = {
|
export const generatedTranslations: PlatformTranslations = {
|
||||||
en: {
|
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.account.2b2936f8": "Account",
|
||||||
"i18n:govoplan-idm.active.7bd0e9f8": "Active",
|
"i18n:govoplan-idm.active.7bd0e9f8": "Active",
|
||||||
"i18n:govoplan-idm.acting_for.8650e6a6": "acting for",
|
"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."
|
"You may inspect relationship evidence but not change it.": "You may inspect relationship evidence but not change it."
|
||||||
},
|
},
|
||||||
de: {
|
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.account.2b2936f8": "Konto",
|
||||||
"i18n:govoplan-idm.active.7bd0e9f8": "Aktiv",
|
"i18n:govoplan-idm.active.7bd0e9f8": "Aktiv",
|
||||||
"i18n:govoplan-idm.acting_for.8650e6a6": "in Vertretung",
|
"i18n:govoplan-idm.acting_for.8650e6a6": "in Vertretung",
|
||||||
|
|||||||
@@ -24,10 +24,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.idm-card-note {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.idm-check-list {
|
.idm-check-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user