From fba4117b0c1669b079af056e9f8ed4f178704682 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 17 Aug 2026 17:42:26 +0200 Subject: [PATCH] feat: add contextual retention guidance --- src/govoplan_policy/backend/manifest.py | 84 ++++++++++++++++++- tests/test_policy_module_contract.py | 38 ++++++++- .../test-interface-pattern-language.mjs | 4 + .../policy/RetentionPoliciesPanel.tsx | 11 ++- 4 files changed, 127 insertions(+), 10 deletions(-) diff --git a/src/govoplan_policy/backend/manifest.py b/src/govoplan_policy/backend/manifest.py index 8929045..2cb8522 100644 --- a/src/govoplan_policy/backend/manifest.py +++ b/src/govoplan_policy/backend/manifest.py @@ -171,23 +171,54 @@ manifest = ModuleManifest( id="policy.hierarchy-overrides-and-retention", title="Administer policy hierarchy and overrides", summary="Policy evaluates versioned system, tenant, group, and user rules for retention and module-owned governed actions.", - body="Administrators can simulate effective retention before saving a lower-level change. Explicit overrides retain source and provenance information and are evaluated through typed capabilities for definitions, Views, function assignments, distribution channels, scheduling privacy, cross-module reporting, and retention. Templates and inherited definitions keep their upstream ceilings when reused or derived.", + body=( + "Retention fields govern separate data classes: raw campaign JSON, generated EML artifacts, stored report details, mock-mailbox records, and audit details. A blank system day limit keeps the class indefinitely; a blank lower-scope value inherits its parent. Lower scopes may only shorten an allowed limit or reduce audit detail. Disabling raw campaign JSON makes it immediately eligible for redaction when retention is applied. Audit detail level controls how new audit details are recorded, while audit-detail retention redacts eligible historical detail but preserves the audit record and a bounded retention marker. The lower-level switch controls whether child scopes may narrow that specific field. Inspect the effective value and source path before saving." + ), documentation_types=("admin",), audience=("policy_admin", "tenant_admin", "system_admin"), related_modules=( + "audit", + "campaign", "dataflow", + "mail", + "reporting", "workflow_engine", "views", "idm", "dist_lists", "scheduling", - "reporting", ), + translations={ + "de": { + "title": "Richtlinienhierarchie und Aufbewahrung verwalten", + "summary": "Policy wertet versionierte System-, Mandanten-, Gruppen- und Benutzerregeln für Aufbewahrung sowie modulbezogene Steuerungsentscheidungen aus.", + "body": "Die Felder steuern getrennte Datenklassen: Kampagnen-Rohdaten im JSON-Format, erzeugte EML-Dateien, gespeicherte Berichtsdetails, Einträge im Testpostfach und Auditdetails. Ein leeres Tageslimit auf Systemebene bedeutet unbegrenzte Aufbewahrung; auf tieferen Ebenen wird der Elternwert geerbt. Tiefere Ebenen dürfen ein erlaubtes Limit nur verkürzen oder Auditdetails weiter reduzieren. Wenn die Speicherung von Kampagnen-Rohdaten deaktiviert wird, werden diese bei der nächsten Ausführung sofort zur Schwärzung vorgemerkt. Die Auditdetailstufe steuert neue Auditdetails; die Aufbewahrungsfrist für Auditdetails schwärzt historische Details, erhält aber den Auditdatensatz und einen begrenzten Aufbewahrungsnachweis. Der Schalter für tiefere Ebenen bestimmt, ob Kindebenen genau dieses Feld weiter einschränken dürfen. Prüfen Sie vor dem Speichern den effektiven Wert und seinen Quellenpfad.", + } + }, metadata={ "kind": "workflow", "route": "/admin?section=system-retention", "screen": "Retention administration", - "help_contexts": ["policy.retention", "privacy.retention"], + "help_contexts": [ + "policy.retention", + "privacy.retention", + "policy.admin.system-retention", + "policy.admin.tenant-retention", + "policy.admin.group-retention", + "policy.admin.user-retention", + "policy.retention.target", + "policy.retention.action.reload-targets", + "policy.retention.action.reload", + "policy.retention.action.save", + "policy.retention.field.store-raw-campaign-json", + "policy.retention.field.raw-campaign-json-retention-days", + "policy.retention.field.generated-eml-retention-days", + "policy.retention.field.stored-report-detail-retention-days", + "policy.retention.field.mock-mailbox-retention-days", + "policy.retention.field.audit-detail-retention-days", + "policy.retention.field.audit-detail-level", + "policy.retention.field.allow-lower-level-limits", + ], "prerequisites": [ "Policy and Access are enabled.", "The actor may read policy settings at the selected scope.", @@ -202,6 +233,53 @@ manifest = ModuleManifest( "verification": "Reload the policy, confirm its source path, and compare the dry-run or applied outcome table with audit evidence.", }, ), + DocumentationTopic( + id="policy.retention-execution-and-recovery", + title="Preview and apply retention safely", + summary="A dry run reports eligible records without changing them; applying retention redacts details and deletes eligible generated artifacts.", + body=( + "Save and reload the intended system policy before execution. Run a dry run first and review every reported data class and count. Apply retention only when those counts match the approved policy and recovery evidence is current. An applied run redacts eligible raw campaign JSON, stored report summaries, reporting details, and audit details; it deletes eligible generated EML and mock-mailbox artifacts. The application cannot restore deleted content. Generated EML deletion uses the Campaign recovery boundary, while the applied run and bounded outcome counts remain in audit evidence. Treat provider failures, recovery blocks, missing artifacts, or unexpected counts as a stop condition and investigate before another run." + ), + documentation_types=("admin",), + audience=("system_admin", "policy_admin", "privacy_officer"), + related_modules=("audit", "campaign", "mail", "reporting"), + translations={ + "de": { + "title": "Aufbewahrung sicher prüfen und anwenden", + "summary": "Ein Probelauf meldet betroffene Datensätze ohne Änderung; die Anwendung schwärzt Details und löscht vorgemerkte erzeugte Artefakte.", + "body": "Speichern und laden Sie die beabsichtigte Systemrichtlinie vor der Ausführung neu. Führen Sie zuerst einen Probelauf aus und prüfen Sie jede Datenklasse und Anzahl. Wenden Sie die Aufbewahrung nur an, wenn die Zahlen der genehmigten Richtlinie entsprechen und die Wiederherstellungsnachweise aktuell sind. Ein angewendeter Lauf schwärzt vorgemerkte Kampagnen-Rohdaten, gespeicherte Berichtsdetails und Auditdetails; vorgemerkte EML-Dateien und Testpostfach-Artefakte werden gelöscht. Die Anwendung kann gelöschte Inhalte nicht wiederherstellen. Die EML-Löschung verwendet die Wiederherstellungsgrenze des Campaign-Moduls; der Lauf und begrenzte Ergebniszahlen bleiben als Auditnachweis erhalten. Anbieterfehler, blockierte Wiederherstellung, fehlende Artefakte oder unerwartete Zahlen sind ein Abbruchgrund und müssen vor einem weiteren Lauf untersucht werden.", + } + }, + metadata={ + "kind": "workflow", + "route": "/admin?section=system-retention", + "screen": "Retention execution", + "help_contexts": [ + "policy.retention.execution", + "policy.retention.action.dry-run", + "policy.retention.action.apply", + "policy.retention.confirm-apply", + "policy.retention.outcome", + ], + "prerequisites": [ + "The actor may write system settings.", + "The intended system retention policy is saved and reloaded.", + "Recovery evidence is current for generated artifacts.", + ], + "steps": [ + "Run a dry run and review each data class and outcome count.", + "Stop if providers fail, recovery is blocked, or counts are unexpected.", + "Confirm the destructive run only after policy and recovery review.", + "Compare the applied outcome with audit evidence.", + ], + "outcome": "Eligible details are redacted and eligible generated artifacts are deleted with bounded outcome and audit evidence.", + "limitations": [ + "The application cannot restore deleted EML or mock-mailbox content.", + "A dry run is a preview and does not reserve the reported set against concurrent changes.", + ], + "verification": "Review the latest outcome, provider failure and recovery counts, then locate the retention_policy.run audit record.", + }, + ), ), migration_spec=MigrationSpec( module_id="policy", diff --git a/tests/test_policy_module_contract.py b/tests/test_policy_module_contract.py index 337e19f..3b82bf8 100644 --- a/tests/test_policy_module_contract.py +++ b/tests/test_policy_module_contract.py @@ -65,14 +65,44 @@ class PolicyModuleContractTests(unittest.TestCase): if item.id == "policy.hierarchy-overrides-and-retention" ) - self.assertEqual( - ["policy.retention", "privacy.retention"], - topic.metadata["help_contexts"], + self.assertTrue( + { + "policy.retention", + "privacy.retention", + "policy.retention.action.save", + "policy.retention.field.store-raw-campaign-json", + "policy.retention.field.generated-eml-retention-days", + "policy.retention.field.audit-detail-level", + "policy.retention.field.allow-lower-level-limits", + }.issubset(topic.metadata["help_contexts"]) ) self.assertEqual("workflow", topic.metadata["kind"]) self.assertIn("/admin", topic.metadata["route"]) + self.assertEqual({"title", "summary", "body"}, set(topic.translations["de"])) + self.assertIn("Quellenpfad", topic.translations["de"]["body"]) - def test_view_policy_administration_contract_is_documented_and_exposed(self) -> None: + execution_topic = next( + item + for item in manifest.documentation + if item.id == "policy.retention-execution-and-recovery" + ) + self.assertEqual( + { + "policy.retention.execution", + "policy.retention.action.dry-run", + "policy.retention.action.apply", + "policy.retention.confirm-apply", + "policy.retention.outcome", + }, + set(execution_topic.metadata["help_contexts"]), + ) + self.assertIn( + "nicht wiederherstellen", execution_topic.translations["de"]["body"] + ) + + def test_view_policy_administration_contract_is_documented_and_exposed( + self, + ) -> None: topic = next( item for item in manifest.documentation diff --git a/webui/scripts/test-interface-pattern-language.mjs b/webui/scripts/test-interface-pattern-language.mjs index d6a700f..b36ce07 100644 --- a/webui/scripts/test-interface-pattern-language.mjs +++ b/webui/scripts/test-interface-pattern-language.mjs @@ -8,6 +8,10 @@ const panel = readFileSync(resolve(webuiRoot, "src/features/policy/RetentionPoli assert.match(panel, / void loadTargets()} disabled={loadingTargets} disabledReason={loadingTargets ? "Policy targets are already loading." : undefined} @@ -224,16 +226,17 @@ export default function RetentionPoliciesPanel({ settings, scopeType, canWrite }
} >

Run the saved effective retention policy against retained platform data.

- - + +
{retentionResult && ( - +
Operation
@@ -259,6 +262,8 @@ export default function RetentionPoliciesPanel({ settings, scopeType, canWrite }