feat: add access module boundary migrations

This commit is contained in:
2026-07-10 12:51:16 +02:00
parent 37828fe340
commit 04681f1d75
41 changed files with 7229 additions and 738 deletions

View File

@@ -1,17 +1,34 @@
from __future__ import annotations
from pathlib import Path
from govoplan_access.backend.db.base import AccessBase
from govoplan_access.backend.db import models as access_models # noqa: F401 - populate access metadata
from govoplan_core.core.access import (
CAPABILITY_ACCESS_ADMINISTRATION,
CAPABILITY_ACCESS_DIRECTORY,
CAPABILITY_ACCESS_EXPLANATION,
CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER,
CAPABILITY_ACCESS_PERMISSION_EVALUATOR,
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER,
CAPABILITY_ACCESS_TENANT_PROVISIONER,
CAPABILITY_ACCESS_SEMANTIC_DIRECTORY,
)
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate
from govoplan_core.core.modules import (
DocumentationCondition,
DocumentationLink,
DocumentationTopic,
FrontendModule,
FrontendRoute,
MigrationSpec,
ModuleContext,
ModuleManifest,
NavItem,
PermissionDefinition,
RoleTemplate,
)
from govoplan_access.backend.configuration_provider import ACCESS_CONFIGURATION_CAPABILITY
def _permission(scope: str, label: str, description: str, category: str, level: str) -> PermissionDefinition:
@@ -44,6 +61,10 @@ ACCESS_PERMISSIONS: tuple[PermissionDefinition, ...] = (
_permission("access:role:read", "View roles", "Inspect tenant and system role definitions.", "Tenant access", "tenant"),
_permission("access:role:write", "Manage roles", "Create and update assignable roles.", "Tenant access", "tenant"),
_permission("access:role:assign", "Assign roles", "Bind roles to memberships, groups, accounts or services.", "Tenant access", "tenant"),
_permission("access:function:read", "View functions", "Inspect organization-bound functions and assignments.", "Tenant access", "tenant"),
_permission("access:function:write", "Manage functions", "Create and update organization units, functions, and role mappings.", "Tenant access", "tenant"),
_permission("access:function:assign", "Assign functions", "Assign organization-bound functions to accounts.", "Tenant access", "tenant"),
_permission("access:function:delegate", "Delegate functions", "Create and revoke permitted function delegations.", "Tenant access", "tenant"),
_permission("access:api_key:read", "View API keys", "List API keys without revealing secrets.", "Tenant access", "tenant"),
_permission("access:api_key:create", "Create API keys", "Create tenant API keys within delegation limits.", "Tenant access", "tenant"),
_permission("access:api_key:revoke", "Revoke API keys", "Revoke tenant API keys.", "Tenant access", "tenant"),
@@ -103,6 +124,10 @@ ACCESS_ROLE_TEMPLATES: tuple[RoleTemplate, ...] = (
"access:group:manage_members",
"access:role:read",
"access:role:assign",
"access:function:read",
"access:function:write",
"access:function:assign",
"access:function:delegate",
"access:api_key:read",
"access:api_key:create",
"access:api_key:revoke",
@@ -128,6 +153,169 @@ ADMIN_READ_SCOPES = (
"access:tenant:read",
"access:account:read",
"access:governance:read",
"access:function:read",
)
ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = (
DocumentationTopic(
id="access.workflow.grant-user-access",
title="Grant a person access",
summary="Use the access administration screens to create or update a tenant membership, place the person in groups, and assign only the roles they need.",
body="The common path is to find or create the person, review their existing membership, then use groups and roles to grant access. If a role or group is not available, the active governance rules or your own delegation limit may block the change.",
layer="configured",
documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin"),
order=30,
conditions=(
DocumentationCondition(
required_modules=("access",),
any_scopes=(
"admin:users:read",
"admin:groups:read",
"admin:roles:read",
"access:membership:read",
"access:group:read",
"access:role:read",
),
),
),
links=(
DocumentationLink(label="Access administration", href="/admin", kind="runtime"),
DocumentationLink(label="Users API", href="/api/v1/admin/users", kind="api"),
DocumentationLink(label="Groups API", href="/api/v1/admin/groups", kind="api"),
DocumentationLink(label="Roles API", href="/api/v1/admin/roles", kind="api"),
),
configuration_keys=("access_governance",),
metadata={
"kind": "workflow",
"outcome": "A person can sign in to the tenant and receives the intended access through groups and roles.",
"prerequisites": [
"You can open Admin.",
"You may read users, groups, and roles.",
"Write or assignment actions require matching management permissions.",
],
"steps": [
"Open Admin and go to Users.",
"Find the existing person or create a membership with their email address and display name.",
"Review current groups and direct roles before changing anything.",
"Add the person to the smallest group that grants the needed shared access.",
"Assign direct roles only when a group does not match the case.",
"Save and review any blocker message before asking a system or tenant owner for help.",
],
"result": "The membership has the intended effective permissions and no broader roles than necessary.",
"verification": "Open the user again and compare groups, direct roles, and effective permissions with the request.",
"related_field_ids": [
"access.user.email",
"access.user.display_name",
"access.user.groups",
"access.user.roles",
],
"related_topic_ids": [
"access.reference.admin-access-fields",
"docs.pattern.field-help",
],
},
),
DocumentationTopic(
id="access.reference.admin-access-fields",
title="Access administration fields",
summary="The access administration screens show tenant memberships, groups, roles, and API keys. Admin docs map the visible fields to API payloads and permission scopes.",
body="Users need the visible labels and a short explanation. Admins also need the backing route, API field, permission, and governance note so they can diagnose unavailable actions.",
layer="configured",
documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin", "operator"),
order=31,
conditions=(
DocumentationCondition(
required_modules=("access",),
any_scopes=(
"admin:users:read",
"admin:groups:read",
"admin:roles:read",
"admin:api_keys:read",
"access:membership:read",
"access:group:read",
"access:role:read",
"access:api_key:read",
),
),
),
links=(
DocumentationLink(label="Access administration", href="/admin", kind="runtime"),
DocumentationLink(label="Users API", href="/api/v1/admin/users", kind="api"),
DocumentationLink(label="Groups API", href="/api/v1/admin/groups", kind="api"),
DocumentationLink(label="Roles API", href="/api/v1/admin/roles", kind="api"),
DocumentationLink(label="API keys API", href="/api/v1/admin/api-keys", kind="api"),
),
configuration_keys=("access_governance",),
metadata={
"kind": "reference",
"route": "/admin",
"screen": "Admin",
"section": "Users, groups, roles, and API keys",
"fields": [
{
"field_id": "access.user.email",
"label": "Email",
"user_description": "The address used to identify the person when they sign in.",
"admin_description": "Stored on the account and membership payloads. It must be normalized and unique for the relevant login account.",
"api_path": "/api/v1/admin/users",
"api_field": "email",
"permission_scope": "access:membership:create",
"validation": "Must be a valid email address.",
"provenance": "Tenant membership creation or account lookup.",
},
{
"field_id": "access.user.display_name",
"label": "Display name",
"user_description": "The readable name shown in user lists and review screens.",
"admin_description": "Maps to display_name on user and account responses where available.",
"api_path": "/api/v1/admin/users",
"api_field": "display_name",
"permission_scope": "access:membership:update",
"validation": "Human-readable text; keep it recognizable for administrators.",
"provenance": "Tenant membership profile.",
},
{
"field_id": "access.user.groups",
"label": "Groups",
"user_description": "Shared access bundles that can add roles for many people at once.",
"admin_description": "Maps to group_ids when updating a user or group membership.",
"api_path": "/api/v1/admin/users/{user_id}",
"api_field": "group_ids",
"permission_scope": "access:group:manage_members",
"validation": "Groups must belong to the same tenant.",
"provenance": "User-group membership rows.",
},
{
"field_id": "access.user.roles",
"label": "Roles",
"user_description": "Direct access grants assigned to one person or inherited from groups.",
"admin_description": "Maps to role_ids on user and group role update requests.",
"api_path": "/api/v1/admin/users/{user_id}",
"api_field": "role_ids",
"permission_scope": "access:role:assign",
"validation": "Roles must be assignable and cannot exceed the actor's delegation limit.",
"provenance": "Direct user roles plus group role inheritance.",
},
{
"field_id": "access.api_key.scopes",
"label": "Scopes",
"user_description": "The actions an API key may perform.",
"admin_description": "Maps to scopes when creating an API key and is intersected with the owner's current permissions.",
"api_path": "/api/v1/admin/api-keys",
"api_field": "scopes",
"permission_scope": "access:api_key:create",
"validation": "Use the narrowest scopes possible.",
"provenance": "API key grant plus owner delegation.",
},
],
"related_topic_ids": [
"access.workflow.grant-user-access",
"docs.pattern.field-help",
],
},
),
)
@@ -152,6 +340,20 @@ def _access_directory(context: ModuleContext) -> object:
return SqlAccessDirectory()
def _access_semantic_directory(context: ModuleContext) -> object:
del context
from govoplan_access.backend.directory import SqlAccessDirectory
return SqlAccessDirectory()
def _access_explanation_service(context: ModuleContext) -> object:
del context
from govoplan_access.backend.explanation import SqlAccessExplanationService
return SqlAccessExplanationService()
def _tenant_provisioner(context: ModuleContext) -> object:
del context
from govoplan_access.backend.tenancy.provisioning import LegacyTenantAccessProvisioner
@@ -173,6 +375,13 @@ def _governance_materializer(context: ModuleContext) -> object:
return SqlAccessGovernanceMaterializer()
def _configuration_provider(context: ModuleContext) -> object:
del context
from govoplan_access.backend.configuration_provider import SqlAccessConfigurationProvider
return SqlAccessConfigurationProvider()
def _route_factory(context: ModuleContext):
from fastapi import APIRouter
@@ -194,16 +403,28 @@ manifest = ModuleManifest(
name="Access",
version="0.1.6",
dependencies=("tenancy",),
optional_dependencies=("identity", "organizations"),
permissions=ACCESS_PERMISSIONS,
role_templates=ACCESS_ROLE_TEMPLATES,
route_factory=_route_factory,
migration_spec=MigrationSpec(module_id="access", metadata=AccessBase.metadata),
migration_spec=MigrationSpec(
module_id="access",
metadata=AccessBase.metadata,
script_location=str(Path(__file__).with_name("migrations") / "versions"),
),
uninstall_guard_providers=(
persistent_table_uninstall_guard(
access_models.Account,
access_models.Identity,
access_models.IdentityAccountLink,
access_models.User,
access_models.Group,
access_models.Role,
access_models.OrganizationUnit,
access_models.Function,
access_models.FunctionRoleAssignment,
access_models.FunctionAssignment,
access_models.FunctionDelegation,
access_models.SystemRoleAssignment,
access_models.UserGroupMembership,
access_models.UserRoleAssignment,
@@ -224,10 +445,14 @@ manifest = ModuleManifest(
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER: _legacy_principal_resolver,
CAPABILITY_ACCESS_PERMISSION_EVALUATOR: _legacy_permission_evaluator,
CAPABILITY_ACCESS_DIRECTORY: _access_directory,
CAPABILITY_ACCESS_SEMANTIC_DIRECTORY: _access_semantic_directory,
CAPABILITY_ACCESS_EXPLANATION: _access_explanation_service,
CAPABILITY_ACCESS_TENANT_PROVISIONER: _tenant_provisioner,
CAPABILITY_ACCESS_ADMINISTRATION: _access_administration,
CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER: _governance_materializer,
ACCESS_CONFIGURATION_CAPABILITY: _configuration_provider,
},
documentation=ACCESS_DOCUMENTATION,
)