Sync GovOPlaN module state

This commit is contained in:
2026-07-10 21:57:20 +02:00
parent c71de86a1f
commit a7c486788e
18 changed files with 1527 additions and 137 deletions

View File

@@ -13,9 +13,12 @@ from govoplan_core.core.access import (
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER,
CAPABILITY_ACCESS_TENANT_PROVISIONER,
CAPABILITY_ACCESS_SEMANTIC_DIRECTORY,
CAPABILITY_AUTH_API_PRINCIPAL_PROVIDER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_TENANT_CONTEXT_SWITCHER,
)
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
from govoplan_core.core.modules import (
DocumentationCondition,
@@ -318,14 +321,51 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = (
],
},
),
DocumentationTopic(
id="access.reference.external-function-role-mappings",
title="Function facts and access roles",
summary="Access maps accepted organization function facts to roles and permissions. IDM assignment ownership remains separate from authorization.",
body=(
"When IDM is installed, Access can consume identity-to-organization-function assignments through the IDM directory capability. "
"Organizations must be installed because Access validates mappings against the Organizations function directory instead of accepting arbitrary function identifiers. "
"An assignment does not grant rights by itself. Access grants rights only when an explicit external function role mapping connects the organization function ID to an assignable tenant role. "
"Tenant administrators manage these mappings under Admin > Function role mappings, and the same records are exposed through the admin API. "
"This keeps the audit trail clear: IDM records who holds a function, while Access records which function facts produce role-derived permissions."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin", "operator"),
order=32,
conditions=(
DocumentationCondition(
required_modules=("access", "organizations"),
any_scopes=("access:function:read", "access:role:read", "admin:roles:read"),
),
),
links=(
DocumentationLink(label="Function role mappings", href="/admin?section=tenant-function-role-mappings", kind="runtime"),
DocumentationLink(label="External function role mappings API", href="/api/v1/admin/external-function-role-mappings", kind="api"),
DocumentationLink(label="IDM assignments", href="/idm", kind="runtime"),
),
metadata={
"kind": "reference",
"route": "/admin",
"api_path": "/api/v1/admin/external-function-role-mappings",
"permission_scopes": ["access:function:write", "access:role:assign"],
"related_topic_ids": [
"idm.workflow.assign-function-to-identity",
"docs.reference.organization-identity-idm-access-boundary",
],
},
),
)
def _legacy_principal_resolver(context: ModuleContext) -> object:
del context
from govoplan_access.backend.auth.dependencies import LegacyPrincipalResolver
return LegacyPrincipalResolver()
idm_directory = _optional_idm_directory(context)
return LegacyPrincipalResolver(idm_directory=idm_directory)
def _legacy_permission_evaluator(context: ModuleContext) -> object:
@@ -335,18 +375,39 @@ def _legacy_permission_evaluator(context: ModuleContext) -> object:
return LegacyPermissionEvaluator()
def _access_directory(context: ModuleContext) -> object:
def _api_principal_provider(context: ModuleContext) -> object:
del context
from govoplan_access.backend.auth.dependencies import AccessApiPrincipalProvider
return AccessApiPrincipalProvider()
def _tenant_context_switcher(context: ModuleContext) -> object:
del context
from govoplan_access.backend.auth.tenant_context import AccessTenantContextSwitcher
return AccessTenantContextSwitcher()
def _access_directory(context: ModuleContext) -> object:
from govoplan_access.backend.directory import SqlAccessDirectory
return SqlAccessDirectory()
return SqlAccessDirectory(idm_directory=_optional_idm_directory(context))
def _access_semantic_directory(context: ModuleContext) -> object:
del context
from govoplan_access.backend.directory import SqlAccessDirectory
return SqlAccessDirectory()
return SqlAccessDirectory(idm_directory=_optional_idm_directory(context))
def _optional_idm_directory(context: ModuleContext) -> IdmDirectory | None:
if not context.registry.has_capability(CAPABILITY_IDM_DIRECTORY):
return None
capability = context.registry.require_capability(CAPABILITY_IDM_DIRECTORY)
if not isinstance(capability, IdmDirectory):
raise RuntimeError(f"Invalid capability: {CAPABILITY_IDM_DIRECTORY}")
return capability
def _access_explanation_service(context: ModuleContext) -> object:
@@ -404,7 +465,7 @@ manifest = ModuleManifest(
id="access",
name="Access",
version="0.1.6",
optional_dependencies=("identity", "organizations", "tenancy"),
optional_dependencies=("identity", "organizations", "tenancy", "idm"),
permissions=ACCESS_PERMISSIONS,
role_templates=ACCESS_ROLE_TEMPLATES,
route_factory=_route_factory,
@@ -424,6 +485,7 @@ manifest = ModuleManifest(
access_models.OrganizationUnit,
access_models.Function,
access_models.FunctionRoleAssignment,
access_models.ExternalFunctionRoleAssignment,
access_models.FunctionAssignment,
access_models.FunctionDelegation,
access_models.SystemRoleAssignment,
@@ -443,8 +505,10 @@ manifest = ModuleManifest(
nav_items=(NavItem(path="/admin", label="Admin", icon="admin", required_any=ADMIN_READ_SCOPES, order=900),),
),
capability_factories={
CAPABILITY_AUTH_API_PRINCIPAL_PROVIDER: _api_principal_provider,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER: _legacy_principal_resolver,
CAPABILITY_AUTH_PERMISSION_EVALUATOR: _legacy_permission_evaluator,
CAPABILITY_AUTH_TENANT_CONTEXT_SWITCHER: _tenant_context_switcher,
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER: _legacy_principal_resolver,
CAPABILITY_ACCESS_PERMISSION_EVALUATOR: _legacy_permission_evaluator,
CAPABILITY_ACCESS_DIRECTORY: _access_directory,