diff --git a/docs/PACKAGE_REGISTRY_RELEASES.md b/docs/PACKAGE_REGISTRY_RELEASES.md index e058376..decd271 100644 --- a/docs/PACKAGE_REGISTRY_RELEASES.md +++ b/docs/PACKAGE_REGISTRY_RELEASES.md @@ -217,6 +217,11 @@ requirements. Missing dependency/interface providers and unsupported update windows are surfaced before an operator adds the entry to a plan; installer preflight remains authoritative. +Catalog entries also carry the permission definitions declared by the tagged +module manifest. Admin groups and exposes their scopes before an install or +update is planned. This is disclosure only: installing a module does not grant +its permissions to an account, role, group, tenant, or service account. + Package lifecycle and availability are intentionally separate: - install, update, and uninstall change the instance-wide package composition; diff --git a/tests/test_release_catalog_entry_synthesis.py b/tests/test_release_catalog_entry_synthesis.py index e8eb165..04b7e21 100644 --- a/tests/test_release_catalog_entry_synthesis.py +++ b/tests/test_release_catalog_entry_synthesis.py @@ -5,7 +5,7 @@ import tomllib import unittest from pathlib import Path -from govoplan_core.core.modules import ModuleManifest +from govoplan_core.core.modules import ModuleManifest, PermissionDefinition from govoplan_core.core.provider_governance import ( ExternalProviderDeclaration, ModuleArchitectureDeclaration, @@ -88,6 +88,18 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase): id="example", name="Example", version="1.2.3", + permissions=( + PermissionDefinition( + scope="example:records:read", + label="Read records", + description="Read example records.", + category="Records", + level="tenant", + module_id="example", + resource="records", + action="read", + ), + ), architecture=architecture, external_providers=(provider,), ), @@ -112,6 +124,19 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase): "default_authority_mode" ], ) + self.assertEqual( + { + "scope": "example:records:read", + "label": "Read records", + "description": "Read example records.", + "category": "Records", + "level": "tenant", + "resource": "records", + "action": "read", + "deprecated": False, + }, + entry["permissions"][0], + ) def test_selective_update_synthesizes_initial_entries_from_package_manifests(self) -> None: payload: dict[str, object] = { diff --git a/tools/release/govoplan_release/catalog_entry_synthesis.py b/tools/release/govoplan_release/catalog_entry_synthesis.py index 13fd9f2..93f3ce1 100644 --- a/tools/release/govoplan_release/catalog_entry_synthesis.py +++ b/tools/release/govoplan_release/catalog_entry_synthesis.py @@ -172,6 +172,23 @@ def manifest_catalog_entry( entry["dependencies"] = list(manifest.dependencies) if manifest.optional_dependencies: entry["optional_dependencies"] = list(manifest.optional_dependencies) + if manifest.permissions: + entry["permissions"] = [ + { + "scope": permission.scope, + "label": permission.label, + "description": permission.description, + "category": permission.category, + "level": permission.level, + "resource": permission.resource, + "action": permission.action, + "deprecated": permission.deprecated, + } + for permission in sorted( + manifest.permissions, + key=lambda item: item.scope, + ) + ] if manifest.architecture is not None: entry["architecture"] = manifest.architecture.to_dict() entry["information_governance"] = manifest.information_governance.to_dict()