Decouple scopes from tenancy schema
Some checks failed
Dependency Audit / dependency-audit (push) Has been cancelled
Module Matrix / module-matrix (push) Has been cancelled

This commit is contained in:
2026-07-10 17:33:43 +02:00
parent 635d25c74c
commit 79af252e88
27 changed files with 529 additions and 75 deletions

View File

@@ -25,6 +25,9 @@ CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER = "access.governanceMaterializer"
CAPABILITY_TENANCY_TENANT_RESOLVER = "tenancy.tenantResolver"
CAPABILITY_AUDIT_SINK = "audit.sink"
CAPABILITY_AUTH_PRINCIPAL_RESOLVER = "auth.principalResolver"
CAPABILITY_AUTH_PERMISSION_EVALUATOR = "auth.permissionEvaluator"
ACCESS_CAPABILITY_NAMES = frozenset(
{
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER,
@@ -38,6 +41,15 @@ ACCESS_CAPABILITY_NAMES = frozenset(
CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER,
CAPABILITY_TENANCY_TENANT_RESOLVER,
CAPABILITY_AUDIT_SINK,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
}
)
AUTH_CAPABILITY_NAMES = frozenset(
{
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
}
)

View File

@@ -17,7 +17,7 @@ from govoplan_core.server.registry import parse_enabled_modules
MODULE_SETTINGS_KEY = "module_management"
INSTALL_PLAN_KEY = "install_plan"
REQUIRED_PLATFORM_MODULES = ("tenancy", "access")
REQUIRED_PLATFORM_MODULES = ("access",)
PROTECTED_MODULES = (*REQUIRED_PLATFORM_MODULES, "admin")
INSTALL_PLAN_ACTIONS = ("install", "uninstall")
INSTALL_PLAN_STATUSES = ("planned", "applied", "blocked")

View File

@@ -215,6 +215,8 @@ class ModuleManifest:
version: str
dependencies: tuple[str, ...] = ()
optional_dependencies: tuple[str, ...] = ()
required_capabilities: tuple[str, ...] = ()
optional_capabilities: tuple[str, ...] = ()
permissions: tuple[PermissionDefinition, ...] = ()
role_templates: tuple[RoleTemplate, ...] = ()
route_factory: RouteFactory | None = None

View File

@@ -19,6 +19,7 @@ class OrganizationUnitRef:
tenant_id: str
slug: str
name: str
unit_type_id: str | None = None
parent_id: str | None = None
description: str | None = None
status: OrganizationStatus = "active"
@@ -31,6 +32,7 @@ class OrganizationFunctionRef:
organization_unit_id: str
slug: str
name: str
function_type_id: str | None = None
description: str | None = None
delegable: bool = False
act_in_place_allowed: bool = False
@@ -41,10 +43,10 @@ class OrganizationFunctionRef:
class OrganizationFunctionAssignmentRef:
id: str
tenant_id: str
account_id: str
identity_id: str
function_id: str
organization_unit_id: str
identity_id: str | None = None
account_id: str | None = None
applies_to_subunits: bool = False
source: FunctionAssignmentSource = "direct"
delegated_from_assignment_id: str | None = None
@@ -76,6 +78,14 @@ class OrganizationDirectory(Protocol):
def get_function_assignment(self, assignment_id: str) -> OrganizationFunctionAssignmentRef | None:
...
def function_assignments_for_identity(
self,
identity_id: str,
*,
tenant_id: str | None = None,
) -> Sequence[OrganizationFunctionAssignmentRef]:
...
def function_assignments_for_account(
self,
account_id: str,

View File

@@ -152,12 +152,16 @@ class PlatformRegistry:
def validate(self) -> RegistrySnapshot:
ordered = tuple(self._topologically_sorted())
available_capabilities = set(self._capability_factories)
seen_permissions: dict[str, PermissionDefinition] = {}
for manifest in ordered:
_validate_manifest_shape(manifest)
for dependency in manifest.dependencies:
if dependency not in self._manifests:
raise RegistryError(f"Module {manifest.id!r} depends on unknown module {dependency!r}")
for capability in manifest.required_capabilities:
if capability not in available_capabilities:
raise RegistryError(f"Module {manifest.id!r} requires unavailable capability {capability!r}")
for dependency in manifest.optional_dependencies:
if dependency == manifest.id:
raise RegistryError(f"Module {manifest.id!r} cannot list itself as an optional dependency")
@@ -230,10 +234,16 @@ def _validate_manifest_shape(manifest: ModuleManifest) -> None:
_validate_dependency_list(manifest.id, "dependencies", manifest.dependencies)
_validate_dependency_list(manifest.id, "optional_dependencies", manifest.optional_dependencies)
_validate_capability_list(manifest.id, "required_capabilities", manifest.required_capabilities)
_validate_capability_list(manifest.id, "optional_capabilities", manifest.optional_capabilities)
overlap = set(manifest.dependencies) & set(manifest.optional_dependencies)
if overlap:
joined = ", ".join(sorted(overlap))
raise RegistryError(f"Module {manifest.id!r} lists dependencies as both required and optional: {joined}")
capability_overlap = set(manifest.required_capabilities) & set(manifest.optional_capabilities)
if capability_overlap:
joined = ", ".join(sorted(capability_overlap))
raise RegistryError(f"Module {manifest.id!r} lists capabilities as both required and optional: {joined}")
if manifest.migration_spec is not None:
if manifest.migration_spec.module_id != manifest.id:
@@ -275,6 +285,14 @@ def _validate_dependency_list(module_id: str, field_name: str, dependencies: tup
raise RegistryError(f"Module {module_id!r} has invalid dependency id {dependency!r}")
def _validate_capability_list(module_id: str, field_name: str, capabilities: tuple[str, ...]) -> None:
if len(capabilities) != len(set(capabilities)):
raise RegistryError(f"Module {module_id!r} has duplicate {field_name}")
for capability in capabilities:
if not capability.strip() or any(part == "" for part in capability.split(".")):
raise RegistryError(f"Module {module_id!r} has invalid capability name {capability!r}")
def _validate_nav_item(module_id: str, item: NavItem) -> None:
if not item.path.startswith("/"):
raise RegistryError(f"Navigation item for module {module_id!r} must start with '/': {item.path!r}")