Add governed module and interface controls

This commit is contained in:
2026-08-04 05:20:47 +02:00
parent 5bc7d748f8
commit d6e7c8b0b1
27 changed files with 1700 additions and 74 deletions
+85 -5
View File
@@ -5,9 +5,17 @@ from sqlalchemy.exc import SQLAlchemyError
from govoplan_core.admin.models import SystemSettings
from govoplan_core.admin.settings import SYSTEM_SETTINGS_ID
from govoplan_core.auth import ApiPrincipal, get_api_principal
from govoplan_core.auth import ApiPrincipal, get_api_principal, require_any_scope
from govoplan_core.core.maintenance import saved_maintenance_mode
from govoplan_core.core.module_entitlements import (
module_entitlement_payload,
tenant_module_entitlement_state,
)
from govoplan_core.core.modules import FrontendModule, FrontendRoute, ModuleManifest, NavItem, PublicFrontendRoute
from govoplan_core.core.platform_interfaces import (
manifest_interface_catalog,
platform_interface_catalog,
)
from govoplan_core.core.registry import PlatformRegistry, manifest_view_surfaces
from govoplan_core.core.views import (
VIEW_SURFACE_CONTRACT_VERSION,
@@ -17,6 +25,7 @@ from govoplan_core.core.views import (
)
from govoplan_core.db.session import get_database
from govoplan_core.i18n import system_i18n_payload
from govoplan_core.tenancy.scope import Tenant
def _registry(request: Request) -> PlatformRegistry:
@@ -26,6 +35,49 @@ def _registry(request: Request) -> PlatformRegistry:
return registry
def _effective_manifest_state(
request: Request,
principal: ApiPrincipal,
) -> tuple[PlatformRegistry, tuple[ModuleManifest, ...], object | None]:
"""Resolve only manifests available in the principal's active context."""
registry = _registry(request)
manifests = tuple(registry.manifests())
entitlement = None
principal_ref = getattr(principal, "principal", None)
tenant_id = getattr(principal_ref, "tenant_id", None)
if tenant_id is not None:
try:
with get_database().session() as session:
tenant = session.get(Tenant, tenant_id)
if tenant is None:
raise HTTPException(
status_code=403,
detail="The active tenant is unavailable.",
)
manifest_map = {manifest.id: manifest for manifest in manifests}
entitlement = tenant_module_entitlement_state(
tenant.settings or {},
manifest_map,
runtime_active_modules=manifest_map,
)
except (RuntimeError, SQLAlchemyError) as exc:
raise HTTPException(
status_code=503,
detail="Tenant module entitlement could not be resolved.",
) from exc
effective_ids = (
set(entitlement.effective_modules)
if entitlement is not None
else {manifest.id for manifest in manifests}
)
return (
registry,
tuple(manifest for manifest in manifests if manifest.id in effective_ids),
entitlement,
)
def _nav_item_payload(item: NavItem, module_id: str | None = None) -> dict[str, object]:
return {
"path": item.path,
@@ -156,9 +208,14 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
@router.get("/modules")
def modules(
request: Request,
_principal: ApiPrincipal = Depends(get_api_principal),
principal: ApiPrincipal = Depends(get_api_principal),
):
registry = _registry(request)
registry, manifests, entitlement = _effective_manifest_state(
request,
principal,
)
principal_ref = getattr(principal, "principal", None)
tenant_id = getattr(principal_ref, "tenant_id", None)
return {
"modules": [
{
@@ -178,13 +235,36 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
for declaration in manifest.external_providers
],
"runtime_ui_capabilities": _runtime_ui_capabilities(manifest.id, settings, registry),
"interface_catalog": {
key: value
for key, value in manifest_interface_catalog(manifest).items()
if key != "declarations"
},
"nav": [_nav_item_payload(item, manifest.id) for item in manifest.nav_items],
"frontend": _frontend_payload(manifest),
}
for manifest in registry.manifests()
]
for manifest in manifests
],
"module_entitlement": (
module_entitlement_payload(tenant_id, entitlement)
if tenant_id is not None and entitlement is not None
else None
),
}
@router.get("/interface-catalog")
def interface_catalog(
request: Request,
principal: ApiPrincipal = Depends(
require_any_scope("admin:module:read", "system:settings:read")
),
):
_registry_item, manifests, _entitlement = _effective_manifest_state(
request,
principal,
)
return platform_interface_catalog(manifests)
@router.get("/public-modules")
def public_modules(request: Request):
registry = _registry(request)