From bc589a3d4f10b3aa8bf1afa35997ccd16e813d10 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 7 Aug 2026 01:59:35 +0200 Subject: [PATCH] feat(modules): show permissions before installation --- src/govoplan_admin/backend/api/v1/schemas.py | 12 +++++++++++ src/govoplan_admin/backend/manifest.py | 2 +- tests/test_catalog_plan.py | 11 ++++++++++ webui/src/api/admin.ts | 12 +++++++++++ .../features/admin/ModuleManagementPanel.tsx | 21 +++++++++++++++++++ webui/src/i18n/generatedTranslations.ts | 4 ++++ 6 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/govoplan_admin/backend/api/v1/schemas.py b/src/govoplan_admin/backend/api/v1/schemas.py index f1a144b..847d40e 100644 --- a/src/govoplan_admin/backend/api/v1/schemas.py +++ b/src/govoplan_admin/backend/api/v1/schemas.py @@ -411,6 +411,17 @@ class ModulePackageArtifactIdentity(BaseModel): source_commit: str | None = None +class ModulePackagePermissionItem(BaseModel): + scope: str + label: str + description: str + category: str + level: Literal["system", "tenant"] + resource: str + action: str + deprecated: bool = False + + class ModulePackageCatalogItem(BaseModel): module_id: str name: str @@ -423,6 +434,7 @@ class ModulePackageCatalogItem(BaseModel): availability: Literal["available", "withdrawn"] = "available" availability_reason: str | None = None configuration_requirements: list[str] = Field(default_factory=list) + permissions: list[ModulePackagePermissionItem] = Field(default_factory=list) release_notes_url: str | None = None source: ModulePackageCatalogSource | None = None artifact_integrity: dict[str, ModulePackageArtifactIdentity] = Field(default_factory=dict) diff --git a/src/govoplan_admin/backend/manifest.py b/src/govoplan_admin/backend/manifest.py index 8b4a6e0..d724752 100644 --- a/src/govoplan_admin/backend/manifest.py +++ b/src/govoplan_admin/backend/manifest.py @@ -147,7 +147,7 @@ manifest = ModuleManifest( summary="Move a reviewed module package plan through preflight, maintenance-gated queueing, daemon execution, and durable run evidence.", body=( "The Modules administration surface projects one operator workflow: save a package plan, resolve preflight findings, enter maintenance mode with the required authority, queue a supervised installer request, and inspect the matching run record. " - "When no deployment-specific catalog is configured, the package directory discovers the signed public GovOPlaN stable catalog. Operators can search and filter available, installed, update, blocked, and withdrawn entries; each row exposes the signed source revision, artifact digest, release notes, and configuration requirements supplied by the catalog. Missing dependencies, incompatible named interfaces, unsupported update windows, and withdrawn releases block plan creation. Selecting an eligible entry copies its exact signed registry identities into the plan; artifact download and digest verification happen only in the trusted installer. " + "When no deployment-specific catalog is configured, the package directory discovers the signed public GovOPlaN stable catalog. Operators can search and filter available, installed, update, blocked, and withdrawn entries; each row exposes the signed source revision, artifact digest, release notes, configuration requirements, and manifest-declared permission scopes supplied by the catalog. Permission disclosure supports review but never grants a scope. Missing dependencies, incompatible named interfaces, unsupported update windows, and withdrawn releases block plan creation. Selecting an eligible entry copies its exact signed registry identities into the plan; artifact download and digest verification happen only in the trusted installer. " "The stage indicator is derived from the saved plan timestamp, the latest matching request, and its run; an older request is never presented as evidence for a newer plan. " "Disabled queue actions name the earliest blocker, the person who can resolve it, and the plan surface where work continues. Package mutation remains outside the FastAPI process and recovery evidence remains durable in the installer ledger. Shared deployments require an immutable image rollout rather than node-local mutation. Tenant entitlement and user/View visibility remain policy settings, not package lifecycle operations." ), diff --git a/tests/test_catalog_plan.py b/tests/test_catalog_plan.py index bc6ca7b..e3b0527 100644 --- a/tests/test_catalog_plan.py +++ b/tests/test_catalog_plan.py @@ -141,6 +141,16 @@ class CatalogPlanItemTests(unittest.TestCase): "name": "Calendar", "version": "0.2.0", "action": "install", + "permissions": [{ + "scope": "calendar:event:read", + "label": "Read events", + "description": "Read calendar events.", + "category": "Calendar", + "level": "tenant", + "resource": "event", + "action": "read", + "deprecated": False, + }], "dependencies": ["access"], "current_version_min": "0.1.5", "current_version_max_exclusive": "0.2.0", @@ -169,6 +179,7 @@ class CatalogPlanItemTests(unittest.TestCase): self.assertFalse(item.plan_allowed) self.assertEqual("blocked", item.catalog_state) self.assertTrue(item.update_available) + self.assertEqual("calendar:event:read", item.permissions[0].scope) self.assertEqual(3, len(item.compatibility_reasons)) self.assertTrue(any("update window" in reason for reason in item.compatibility_reasons)) self.assertTrue(any("Required module access" in reason for reason in item.compatibility_reasons)) diff --git a/webui/src/api/admin.ts b/webui/src/api/admin.ts index d4613fa..a91986f 100644 --- a/webui/src/api/admin.ts +++ b/webui/src/api/admin.ts @@ -443,6 +443,17 @@ export type ModulePackageArtifactIdentity = { source_commit?: string | null; }; +export type ModulePackagePermissionItem = { + scope: string; + label: string; + description: string; + category: string; + level: "system" | "tenant"; + resource: string; + action: string; + deprecated: boolean; +}; + export type ModulePackageCatalogItem = { module_id: string; name: string; @@ -455,6 +466,7 @@ export type ModulePackageCatalogItem = { availability: "available" | "withdrawn"; availability_reason?: string | null; configuration_requirements: string[]; + permissions: ModulePackagePermissionItem[]; release_notes_url?: string | null; source?: ModulePackageCatalogSource | null; artifact_integrity: Record; diff --git a/webui/src/features/admin/ModuleManagementPanel.tsx b/webui/src/features/admin/ModuleManagementPanel.tsx index b595f9a..e456e51 100644 --- a/webui/src/features/admin/ModuleManagementPanel.tsx +++ b/webui/src/features/admin/ModuleManagementPanel.tsx @@ -580,6 +580,16 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai )} {item.description &&

{item.description}

} {item.configuration_requirements.length > 0 &&

i18n:govoplan-admin.configuration_requirements.7c8f1011 {item.configuration_requirements.join(", ")}

} + {item.permissions.length > 0 &&
+ {i18nMessage("i18n:govoplan-admin.declared_permissions.7c8f1015", { value0: item.permissions.length })} +

i18n:govoplan-admin.installing_does_not_grant_permissions.7c8f1016

+
+ {permissionCategoryLabels(item).map((label) => {label})} +
+
+ {item.permissions.map((permission) => {permission.scope} {permission.label}{permission.deprecated ? " (deprecated)" : ""})} +
+
} {item.migration_notes &&

{item.migration_notes}

} {item.bridge_notes &&

{item.bridge_notes}

} {item.recovery_notes &&

{item.recovery_notes}

} @@ -954,6 +964,17 @@ function requiredInterfacesLabel(item: ModulePackageCatalogItem): string { }).join(", "); } +function permissionCategoryLabels(item: ModulePackageCatalogItem): string[] { + const counts = new Map(); + item.permissions.forEach((permission) => { + const key = `${permission.category} · ${permission.level}`; + counts.set(key, (counts.get(key) ?? 0) + 1); + }); + return [...counts.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, count]) => `${key}: ${count}`); +} + function migrationSafetyLabel(value: ModulePackageCatalogItem["migration_safety"] | ModuleInstallTargetItem["migration_safety"]): string { if (value === "requires_review") return "i18n:govoplan-admin.requires_review.8ba0b8c6"; if (value === "forward_only") return "i18n:govoplan-admin.forward_only.6c107a46"; diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index dc0a8e3..8850845 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -16,6 +16,8 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.release_notes.7c8f1012": "Release notes", "i18n:govoplan-admin.withdrawn.7c8f1013": "Withdrawn", "i18n:govoplan-admin.update_available.7c8f1014": "Update available", + "i18n:govoplan-admin.declared_permissions.7c8f1015": "Declared permissions ({value0})", + "i18n:govoplan-admin.installing_does_not_grant_permissions.7c8f1016": "Installing this module does not grant these permissions.", "i18n:govoplan-admin.administration_data_is_loading.6bf3c001": "Administration data is loading.", "i18n:govoplan-admin.an_administration_operation_is_in_progress.6bf3c002": "An administration operation is in progress.", "i18n:govoplan-admin.system_administration_write_permission_is_required.6bf3c003": "System administration write permission is required.", @@ -466,6 +468,8 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.release_notes.7c8f1012": "Versionshinweise", "i18n:govoplan-admin.withdrawn.7c8f1013": "Zurückgezogen", "i18n:govoplan-admin.update_available.7c8f1014": "Aktualisierung verfügbar", + "i18n:govoplan-admin.declared_permissions.7c8f1015": "Deklarierte Berechtigungen ({value0})", + "i18n:govoplan-admin.installing_does_not_grant_permissions.7c8f1016": "Die Installation dieses Moduls vergibt diese Berechtigungen nicht.", "i18n:govoplan-admin.administration_data_is_loading.6bf3c001": "Administrationsdaten werden geladen.", "i18n:govoplan-admin.an_administration_operation_is_in_progress.6bf3c002": "Eine Administrationsaktion wird gerade ausgeführt.", "i18n:govoplan-admin.system_administration_write_permission_is_required.6bf3c003": "Eine Schreibberechtigung für die Systemadministration ist erforderlich.",