from __future__ import annotations from govoplan_core.core.modules import with_documentation_structured_translations from govoplan_identity.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS from pathlib import Path from govoplan_core.core.access import ( CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER, ) from govoplan_core.core.identity import ( CAPABILITY_IDENTITY_DIRECTORY, CAPABILITY_IDENTITY_SEARCH, ) from govoplan_core.core.module_guards import persistent_table_uninstall_guard from govoplan_core.core.modules import ( CapabilityDocumentation, DocumentationCondition, DocumentationTopic, FrontendModule, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleManifest, PermissionDefinition, RoleTemplate, ViewSurface, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.db.base import Base from govoplan_identity.backend.db import models as identity_models # noqa: F401 - populate metadata from govoplan_identity.backend.dsar_provider import ( IDENTITY_DSAR_CAPABILITY, IdentityDsarProvider, ) def _permission( scope: str, label: str, description: str, *, level: str = "tenant", ) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Identity", level=level, module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission( "identity:identity:read", "View identities", "Search and read normalized identities and their account links.", ), _permission( "identity:identity:admin", "Administer identities", "Create, update, activate, and deactivate canonical system identities.", level="system", ), _permission( "identity:account_link:admin", "Administer identity account links", "Add, remove, and select the primary platform account for a canonical identity.", level="system", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="identity_viewer", name="Identity viewer", description="Read normalized identities and account links.", permissions=("identity:identity:read",), ), RoleTemplate( slug="identity_administrator", name="Identity administrator", description="Administer the canonical system identity directory and account links.", permissions=( "identity:identity:read", "identity:identity:admin", "identity:account_link:admin", ), ), ) def _route_factory(context: ModuleContext): del context from govoplan_identity.backend.api.v1.routes import router return router def _identity_directory(context: ModuleContext) -> object: del context from govoplan_identity.backend.directory import SqlIdentityDirectory return SqlIdentityDirectory() def _dsar_provider(context: ModuleContext) -> IdentityDsarProvider: del context return IdentityDsarProvider() manifest = ModuleManifest( id="identity", name="Identity", version="0.1.21", required_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, route_factory=_route_factory, provides_interfaces=( ModuleInterfaceProvider(name=IDENTITY_DSAR_CAPABILITY, version="0.1.0"), ), frontend=FrontendModule( module_id="identity", package_name="@govoplan/identity-webui", view_surfaces=( ViewSurface( id="identity.admin.directory", module_id="identity", kind="section", label="Identity directory", order=30, ), ViewSurface( id="identity.admin.account-links", module_id="identity", kind="section", label="Identity account links", parent_id="identity.admin.directory", order=20, ), ), ), migration_spec=MigrationSpec( module_id="identity", metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), migration_after=("access",), ), uninstall_guard_providers=( persistent_table_uninstall_guard( identity_models.Identity, identity_models.IdentityAccountLink, label="Identity", ), ), capability_factories={ CAPABILITY_IDENTITY_DIRECTORY: _identity_directory, CAPABILITY_IDENTITY_SEARCH: _identity_directory, IDENTITY_DSAR_CAPABILITY: _dsar_provider, }, capability_documentation={ IDENTITY_DSAR_CAPABILITY: CapabilityDocumentation( label="Identity data-subject request provider", summary=( "Exports a corroborated system identity and matching account link " "without automatically mutating cross-tenant identity state." ), contract_version="0.1.0", ), }, documentation=( DocumentationTopic( id="identity.data-subject-requests", title="Identity data-subject requests", summary=( "Export a canonical identity only after its exact identity and " "account-link identifiers corroborate each other." ), body=( "Identity records are system-scoped rather than tenant-owned. The " "data-subject provider therefore requires an exact identity identifier " "and either its exact linked account or account-link identifier before " "returning display, external-subject, lifecycle, and matching-link data. " "Other links and arbitrary identity settings are excluded. A tenant " "request cannot automatically deactivate the identity or remove the " "link because either action can affect authentication and memberships " "outside that tenant. Erasure is recorded as a manual review requiring " "Identity, Access, tenancy, and retention owners." ), layer="configured", documentation_types=("admin", "user"), audience=("user", "system_admin", "identity_admin", "auditor"), related_modules=("core", "access", "tenancy"), order=23, translations={ "de": { "title": "Betroffenenanfragen für Identitäten", "summary": ( "Eine kanonische Identität nur exportieren, wenn exakte Identitäts- und Kontoverknüpfungskennungen einander bestätigen." ), "body": ( "Identity-Datensätze sind systemweit und nicht mandanteneigen. Der Betroffenen-Provider verlangt deshalb eine exakte " "Identitätskennung und entweder das exakt verknüpfte Konto oder die Kennung der Kontoverknüpfung, bevor er Anzeige-, " "externes Subjekt-, Lebenszyklus- und passende Verknüpfungsdaten ausgibt. Andere Verknüpfungen und beliebige " "Identitätseinstellungen sind ausgeschlossen. Eine Mandantenanfrage darf die Identität nicht automatisch deaktivieren " "oder die Verknüpfung entfernen, weil beides Authentifizierung und Mitgliedschaften außerhalb dieses Mandanten beeinflussen " "kann. Eine Löschung wird als manuelle Prüfung unter Beteiligung der Zuständigen für Identity, Access, Tenancy und " "Aufbewahrung erfasst." ), } }, metadata={ "help_contexts": ["privacy.data-subject-requests"], "consequence_classes": { "corroborated_export": ( "Discloses one matching identity/account-link pair only." ), "manual_erasure_review": ( "Prevents a tenant request from changing system-wide identity state." ), }, }, ), DocumentationTopic( id="identity.model", title="Identity directory", summary="Identity owns normalized subjects and links them to platform accounts. Access owns authorization.", body=( "An identity can have multiple accounts. Identity facts remain separate from authentication sessions, " "organization functions, and permission decisions." ), layer="configured", documentation_types=("admin", "user"), audience=("tenant_admin", "access_admin", "operator"), translations={ "de": { "title": "Identitätsverzeichnis", "summary": ( "Identity besitzt normalisierte Subjekte und verknüpft sie mit Plattformkonten; Access besitzt die Autorisierung." ), "body": ( "Eine Identität kann mehrere Konten besitzen. Identitätsmerkmale bleiben von Authentifizierungssitzungen, " "Organisationsfunktionen und Berechtigungsentscheidungen getrennt." ), } }, order=24, ), DocumentationTopic( id="identity.administration", title="Administer the canonical identity directory", summary="Manage system-scoped identities and their account links without taking over authentication or access control.", body=( "The Identity administration surface lists, creates, inspects, updates, deactivates, and reactivates canonical identities. These records are system-scoped; the current tenant is shown only as the acting administrative context. Account references remain opaque to Identity and one account can be linked to only one identity through this administration API. The first link becomes primary automatically. A primary link cannot be removed while another link remains: promote the replacement first. Every write and primary-account transition is recorded as a system audit event. Deactivation is reversible and does not suspend authentication, erase links, or change permissions." ), layer="configured", documentation_types=("admin", "user"), audience=("system_admin", "identity_admin", "access_admin"), conditions=( DocumentationCondition( required_modules=("identity",), any_scopes=( "identity:identity:read", "identity:identity:admin", "identity:account_link:admin", ), ), ), order=26, translations={ "de": { "title": "Kanonisches Identitätsverzeichnis administrieren", "summary": ( "Systemweite Identitäten und ihre Kontoverknüpfungen verwalten, ohne Authentifizierung oder Zugriffskontrolle zu übernehmen." ), "body": ( "Die Identity-Administrationsoberfläche listet, erstellt, prüft, aktualisiert, deaktiviert und reaktiviert kanonische " "Identitäten. Diese Datensätze sind systemweit; der aktuelle Mandant wird nur als administrativer Handlungskontext gezeigt. " "Kontoverweise bleiben für Identity undurchsichtig, und ein Konto darf über diese API nur mit einer Identität verknüpft " "sein. Die erste Verknüpfung wird automatisch primär. Eine primäre Verknüpfung kann nicht entfernt werden, solange eine " "weitere besteht; machen Sie zuerst den Ersatz primär. Jeder Schreibvorgang und jeder Wechsel des primären Kontos wird als " "System-Auditereignis festgehalten. Die Deaktivierung ist umkehrbar und sperrt weder die Authentifizierung noch löscht sie " "Verknüpfungen oder verändert Berechtigungen." ), } }, metadata={ "kind": "workflow", "help_contexts": ["identity.admin.directory"], "prerequisites": [ "The administrator has system identity administration permission.", "Account IDs are obtained from an authorized Access administration workflow.", ], "outcome": "Canonical identity and link state changes atomically with system-scoped audit evidence.", "verification": "Reload the identity, verify its primary marker and lifecycle state, then inspect the corresponding system audit records.", }, ), DocumentationTopic( id="identity.lifecycle", title="Administer identity and account-link lifecycle", summary="Deactivate identities reversibly and change the primary account without discarding link provenance.", body=( "Deactivation removes an identity from ordinary search while direct resolution retains an explicit inactive record; it preserves all account links and is not account suspension or erasure. " "A primary-account change selects an existing link, atomically demotes the previous primary, preserves multiple-account compatibility, and records actor, old/new account, and link-source evidence. The authorized caller commits or rolls back state and audit evidence together." ), layer="configured", documentation_types=("admin",), audience=("tenant_admin", "access_admin", "operator"), order=25, translations={ "de": { "title": "Lebenszyklus von Identitäten und Kontoverknüpfungen administrieren", "summary": ( "Identitäten umkehrbar deaktivieren und das primäre Konto ändern, ohne die Herkunft der Verknüpfungen zu verwerfen." ), "body": ( "Eine Deaktivierung entfernt die Identität aus der gewöhnlichen Suche, während die direkte Auflösung einen ausdrücklich " "inaktiven Datensatz beibehält. Alle Kontoverknüpfungen bleiben erhalten; es handelt sich weder um Kontosperrung noch " "Löschung. Ein Wechsel des primären Kontos wählt eine bestehende Verknüpfung, stuft das bisherige primäre Konto atomar " "zurück, erhält die Kompatibilität mehrerer Konten und zeichnet handelnde Person, altes/neues Konto und " "Verknüpfungsquellennachweis auf. Der berechtigte Aufrufer schreibt Zustand und Auditnachweis gemeinsam fest oder setzt " "beides zurück." ), } }, metadata={ "kind": "reference", "prerequisites": [ "The caller has separately established lifecycle authority.", "The replacement primary account is already linked to the identity.", ], "outcome": "Identity visibility or primary-account state changes without deleting account-link provenance.", "verification": "Confirm ordinary versus include-inactive directory results, inspect every retained account link, and review the matching identity lifecycle audit record.", }, ), ), architecture=declared_module_architecture( layer="institutional_foundation", kind="foundation", maturity="vertical_slice", documentation_ref="docs/IDENTITY_MODEL.md", test_ref="tests/test_directory.py", known_limits=( "Identity proofing and external-directory reconciliation are outside the current vertical slice.", ), owned_concepts=("identity", "identity-account link"), non_owned_concepts=( "account authentication", "function assignment", "contact point", "organization", ), security_docs=("docs/IDENTITY_MODEL.md",), ), ) manifest = with_documentation_structured_translations( manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS ) def get_manifest() -> ModuleManifest: return manifest