From 1401c78c8a886e59331cd95a85efa402b601dfff Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 21 Jul 2026 17:13:53 +0200 Subject: [PATCH] docs: add files workflow assurance topics --- src/govoplan_files/backend/manifest.py | 130 ++++++++++++++++++++++++- tests/test_manifest_documentation.py | 41 ++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/src/govoplan_files/backend/manifest.py b/src/govoplan_files/backend/manifest.py index aecd7c8..7a0f87f 100644 --- a/src/govoplan_files/backend/manifest.py +++ b/src/govoplan_files/backend/manifest.py @@ -97,7 +97,7 @@ PERMISSIONS = ( _permission("files:file:download", "Download files", "Download managed files and generated archives."), _permission("files:file:upload", "Upload files", "Upload new managed file versions."), _permission("files:file:organize", "Organize files", "Create folders, rename, move or copy managed files."), - _permission("files:file:share", "Share files", "Grant or update managed file shares; revocation is tracked separately."), + _permission("files:file:share", "Share files", "Grant or update managed file shares; revocation is not available yet."), _permission("files:file:delete", "Delete files", "Delete or hide managed files and folders where policy allows it."), _permission("files:file:admin", "Administer file spaces", "Administer all file spaces in the tenant."), ) @@ -194,6 +194,65 @@ manifest = ModuleManifest( nav_items=(NavItem(path="/files", label="Files", icon="folder", required_any=("files:file:read",), order=40),), ), documentation=( + DocumentationTopic( + id="files.workflow.upload-organize-and-share", + title="Upload, organize, and share managed files", + summary="Put files in a personal or group space, organize them with explicit conflict handling, and grant or update access through a supported integration or API client.", + body=( + "Work in My files or an accessible group space. Uploads, folders, renames, moves, and copies stay inside governed managed storage and require an explicit choice when a target path already exists. " + "A caller with share permission can grant or update read, write, or manage access for a user, group, tenant, or campaign without changing ownership. The Files page does not yet provide a general share editor, and the API has no share-revocation route; sharing is currently performed by a supporting workflow or API client." + ), + layer="configured", + documentation_types=("user",), + audience=("file_user", "file_manager", "process_participant"), + order=39, + conditions=( + DocumentationCondition( + required_modules=("files",), + required_scopes=( + "files:file:read", + "files:file:upload", + "files:file:organize", + "files:file:share", + ), + ), + ), + links=( + DocumentationLink(label="Files", href="/files", kind="runtime"), + DocumentationLink(label="Upload API", href="/api/v1/files/upload", kind="api"), + DocumentationLink(label="Move or copy API", href="/api/v1/files/transfer", kind="api"), + DocumentationLink(label="Grant or update a share", href="/api/v1/files/{file_id}/shares", kind="api"), + DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), + ), + related_modules=("campaigns",), + unlocks=("Users and connected processes can exchange governed managed files without changing file ownership.",), + metadata={ + "kind": "workflow", + "route": "/files", + "screen": "Files", + "prerequisites": [ + "Files is installed and you may read, upload, organize, and share managed files.", + "You can access the destination personal or group space.", + "A supporting workflow or API client is available when a user, group, tenant, or campaign share must be granted or updated.", + ], + "steps": [ + "Open Files, choose My files or an accessible group space, and open the intended destination folder.", + "Create folders where needed, then upload or drag files into the destination.", + "Resolve every name conflict explicitly by rejecting, renaming, overwriting, or skipping the affected item.", + "Use rename, move, or copy to place the managed items at their intended logical paths.", + "Review the current version, checksum, owner, and path before granting access.", + "Through the supporting workflow or API, grant or update the required read, write, or manage share; do not promise revocation because no revoke route exists yet.", + "Have the intended recipient verify access, and verify that an unrelated account remains denied.", + ], + "outcome": "The content is stored as governed managed assets in the intended space and the requested access has been granted or updated without changing ownership.", + "verification": "Reopen the files to confirm owner, logical path, current version, and checksum, then test one intended and one denied access path. A share that must be removed requires an explicit future revocation capability.", + "related_topic_ids": [ + "files.workflow.import-managed-snapshot", + "files.assurance.process-and-release-readiness", + "files.reference.snapshot-provenance-and-capabilities", + ], + }, + ), DocumentationTopic( id="files.workflow.import-managed-snapshot", title="Import an external file as a governed snapshot", @@ -397,6 +456,75 @@ manifest = ModuleManifest( ], }, ), + DocumentationTopic( + id="files.assurance.process-and-release-readiness", + title="Assure a Files-backed process and release", + summary="Check a process against the implemented Files boundary, exercise permitted and denied paths, and retain evidence before approving a release or operational use.", + body=( + "A process owner must distinguish implemented controls from planned capabilities before relying on Files. A release is not ready until all package and manifest versions align and representative authorization, upload limits, conflict handling, download, deletion, connector, and recovery paths have been exercised. " + "Current limitations include no general share-management UI or share revocation, no self-service restore or hard purge, no enforced retention or legal hold, and no dedicated canonical audit event for every ordinary Files mutation. Record these limitations in the process assessment instead of treating soft deletion or change-sequence entries as stronger evidence." + ), + layer="configured", + documentation_types=("admin", "user"), + audience=("process_owner", "release_manager", "file_admin", "operator", "security_auditor"), + order=44, + conditions=( + DocumentationCondition( + required_modules=("files",), + any_scopes=( + "files:file:read", + "files:file:admin", + "admin:settings:read", + "system:settings:read", + "system:audit:read", + ), + ), + ), + links=( + DocumentationLink(label="Files", href="/files", kind="runtime"), + DocumentationLink(label="Files API", href="/api/v1/files", kind="api"), + DocumentationLink(label="Connector provider status", href="/api/v1/files/connectors/providers", kind="api"), + DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), + ), + related_modules=("campaigns", "audit", "ops"), + unlocks=("Process owners and release managers can approve Files use against explicit controls, evidence, and known gaps.",), + configuration_keys=( + "FILE_STORAGE_BACKEND", + "FILE_STORAGE_LOCAL_ROOT", + "FILE_UPLOAD_MAX_BYTES", + "FILE_UPLOAD_ZIP_MAX_BYTES", + "MASTER_KEY_B64", + "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS", + ), + metadata={ + "kind": "workflow", + "route": "/files", + "screen": "Files process and release assurance", + "prerequisites": [ + "A named process owner has defined the intended users, data classification, retention expectations, and integrations.", + "A candidate release is installed on a clean database and an upgrade copy with aligned Python, root package, WebUI package, and module-manifest versions.", + "Representative permitted and denied accounts, bounded test files, and a coordinated database/blob/key recovery set are available.", + ], + "steps": [ + "Compare the process requirements with the handbook's implemented/planned boundary and record every unsupported requirement or compensating control.", + "Confirm version alignment, apply migrations on clean and upgrade databases, and run the repository plus meta-repository security and static-analysis gates.", + "Exercise allowed and denied personal, group, and share access with representative accounts.", + "Exercise bounded upload and ZIP handling, every required conflict strategy, organization, download, and soft deletion.", + "Where connectors are configured, verify policy explanation and one pinned HTTP provider; verify live S3 and SMB access still fails closed.", + "Restore a coordinated database/blob/key backup and compare representative downloaded bytes with their recorded SHA-256 checksums.", + "Record the tested versions, results, known limitations, evidence locations, residual risks, owner, and approval decision.", + ], + "outcome": "The process or release has an explicit approval record tied to aligned versions, representative evidence, known limitations, and owned residual risks.", + "verification": "A reviewer can reproduce the recorded allow/deny, integrity, connector, and recovery checks and can trace each unmet requirement to a documented limitation or accepted compensating control.", + "related_topic_ids": [ + "files.workflow.upload-organize-and-share", + "files.workflow.import-managed-snapshot", + "files.governed-connectors-and-provenance", + "files.reference.integrity-recovery-and-fail-closed-transports", + "files.reference.snapshot-provenance-and-capabilities", + ], + }, + ), ), migration_spec=MigrationSpec( module_id="files", diff --git a/tests/test_manifest_documentation.py b/tests/test_manifest_documentation.py index 4b96664..6210bbe 100644 --- a/tests/test_manifest_documentation.py +++ b/tests/test_manifest_documentation.py @@ -4,10 +4,12 @@ import unittest TOPIC_IDS = { + "files.workflow.upload-organize-and-share", "files.workflow.import-managed-snapshot", "files.governed-connectors-and-provenance", "files.reference.integrity-recovery-and-fail-closed-transports", "files.reference.snapshot-provenance-and-capabilities", + "files.assurance.process-and-release-readiness", } HANDBOOK_HREF = "govoplan-files/docs/FILES_HANDBOOK.md" @@ -52,6 +54,28 @@ class FilesManifestDocumentationTests(unittest.TestCase): self.assertIn("never mutate the remote source", topic.body) self.assertIn("/api/v1/files/connectors/profiles/{profile_id}/import", {link.href for link in topic.links}) + def test_managed_file_workflow_covers_upload_organization_and_current_share_boundary(self) -> None: + topic = self.topic("files.workflow.upload-organize-and-share") + + self.assertEqual(("user",), topic.documentation_types) + self.assertEqual("workflow", topic.metadata["kind"]) + self.assertEqual( + { + "files:file:read", + "files:file:upload", + "files:file:organize", + "files:file:share", + }, + set(topic.conditions[0].required_scopes), + ) + for key in ("prerequisites", "steps", "outcome", "verification"): + self.assertTrue(topic.metadata[key]) + self.assertIn("does not yet provide a general share editor", topic.body) + self.assertIn("no share-revocation route", topic.body) + self.assertIn("/api/v1/files/upload", {link.href for link in topic.links}) + self.assertIn("/api/v1/files/transfer", {link.href for link in topic.links}) + self.assertIn("/api/v1/files/{file_id}/shares", {link.href for link in topic.links}) + def test_admin_topic_covers_policy_redaction_and_atomic_credential_deletion(self) -> None: topic = self.topic("files.governed-connectors-and-provenance") @@ -93,6 +117,23 @@ class FilesManifestDocumentationTests(unittest.TestCase): self.assertIn("exact asset, version, blob, checksum", topic.body) self.assertIn("collaboration", topic.body) + def test_process_and_release_assurance_exposes_gates_evidence_and_limits(self) -> None: + topic = self.topic("files.assurance.process-and-release-readiness") + + self.assertEqual(("admin", "user"), topic.documentation_types) + self.assertEqual("workflow", topic.metadata["kind"]) + self.assertIn("process_owner", topic.audience) + self.assertIn("release_manager", topic.audience) + self.assertIn("versions align", topic.body) + self.assertIn("no general share-management UI or share revocation", topic.body) + self.assertIn("no enforced retention or legal hold", topic.body) + for key in ("prerequisites", "steps", "outcome", "verification"): + self.assertTrue(topic.metadata[key]) + self.assertTrue(any("meta-repository security" in step for step in topic.metadata["steps"])) + self.assertTrue(any("fails closed" in step for step in topic.metadata["steps"])) + self.assertTrue(any("SHA-256" in step for step in topic.metadata["steps"])) + self.assertIn(HANDBOOK_HREF, {link.href for link in topic.links if link.kind == "repository"}) + def test_internal_related_topic_ids_resolve(self) -> None: for topic in self.topics.values(): for related_id in topic.metadata.get("related_topic_ids", []):