from __future__ import annotations from dataclasses import replace from pathlib import Path from sqlalchemy import inspect from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER from govoplan_core.core.encryption import CAPABILITY_ENCRYPTION_CONTENT_CIPHER from govoplan_core.core.files import ( CAPABILITY_FILES_ACCESS, CAPABILITY_FILES_ARTIFACT_STORE, ) from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard from govoplan_core.core.modules import ( DocumentationCondition, DocumentationLink, DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate, ) from govoplan_core.core.operations import OperationalCheckProviderRegistration from govoplan_core.core.provider_governance import ( ExternalProviderDeclaration, ExternalProviderStateProviderRegistration, ProviderBehaviorDeclaration, ProviderObjectDeclaration, declared_module_architecture, ) from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_files.backend.change_tracking import register_files_change_tracking from govoplan_files.backend.db import models as file_models # noqa: F401 - populate Files ORM metadata from govoplan_files.backend.documentation import documentation_topics from govoplan_files.backend.provider_state import ( REMOTE_STORAGE_PROVIDER_ID, remote_storage_provider_states, ) register_files_change_tracking() _files_table_retirement_provider = drop_table_retirement_provider( file_models.FileBlob, file_models.FileIntegrityScan, file_models.FileIntegrityFinding, file_models.FileFolder, file_models.FileAsset, file_models.FileVersion, file_models.FileShare, file_models.FileConnectorCredential, file_models.FileConnectorPolicy, file_models.FileConnectorProfile, file_models.FileConnectorSpace, file_models.CampaignAttachmentUse, label="Files", ) def _files_retirement_provider(session: object | None, module_id: str): plan = _files_table_retirement_provider(session, module_id) base_executor = plan.destroy_data_executor if base_executor is None: return plan def executor(execute_session: object, execute_module_id: str) -> None: if not hasattr(execute_session, "get_bind") or not hasattr(execute_session, "query"): raise RuntimeError("No database session is available for Files credential retirement.") live_inspector = inspect(execute_session.get_bind()) if any( live_inspector.has_table(table_name) for table_name in ( file_models.FileConnectorCredential.__tablename__, file_models.FileConnectorProfile.__tablename__, ) ): from govoplan_files.backend.storage.connector_credential_deletion import ( delete_connector_credentials_for_retirement, ) delete_connector_credentials_for_retirement(execute_session) base_executor(execute_session, execute_module_id) return replace( plan, destroy_data_warnings=( *plan.destroy_data_warnings, "Files-owned encrypted connector credentials are scrubbed and audited immediately before tables are dropped; legacy non-owned external references are detached without claiming provider-side deletion.", ), destroy_data_executor=executor, ) def _permission(scope: str, label: str, description: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Files", level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission("files:file:read", "View files", "List, search and preview managed files."), _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", "List, grant, update, expire, and revoke managed file shares."), _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."), ) ROLE_TEMPLATES = ( RoleTemplate( slug="file_manager", name="File manager", description="Manage tenant file spaces without campaign delivery rights.", permissions=( "files:file:read", "files:file:download", "files:file:upload", "files:file:organize", "files:file:share", "files:file:delete", ), ), RoleTemplate( slug="file_viewer", name="File viewer", description="Read and download permitted files.", permissions=("files:file:read", "files:file:download"), ), ) def _tenant_summary(session, tenant_id: str) -> dict[str, int]: from govoplan_files.backend.db.models import FileAsset, FileConnectorCredential, FileConnectorPolicy, FileConnectorProfile, FileConnectorSpace return { "files": session.query(FileAsset).filter(FileAsset.tenant_id == tenant_id).count(), "connector_credentials": session.query(FileConnectorCredential).filter(FileConnectorCredential.tenant_id == tenant_id).count(), "connector_policies": session.query(FileConnectorPolicy).filter(FileConnectorPolicy.tenant_id == tenant_id).count(), "connector_profiles": session.query(FileConnectorProfile).filter(FileConnectorProfile.tenant_id == tenant_id).count(), "connector_spaces": session.query(FileConnectorSpace).filter(FileConnectorSpace.tenant_id == tenant_id).count(), } def _tenant_summary_batch(session, tenant_ids) -> dict[str, dict[str, int]]: from sqlalchemy import func from govoplan_files.backend.db.models import FileAsset, FileConnectorCredential, FileConnectorPolicy, FileConnectorProfile, FileConnectorSpace ids = tuple(dict.fromkeys(str(tenant_id) for tenant_id in tenant_ids if tenant_id)) if not ids: return {} counts: dict[str, dict[str, int]] = { tenant_id: { "files": 0, "connector_credentials": 0, "connector_policies": 0, "connector_profiles": 0, "connector_spaces": 0, } for tenant_id in ids } models = ( ("files", FileAsset), ("connector_credentials", FileConnectorCredential), ("connector_policies", FileConnectorPolicy), ("connector_profiles", FileConnectorProfile), ("connector_spaces", FileConnectorSpace), ) for count_key, model in models: rows = ( session.query(model.tenant_id, func.count(model.id)) .filter(model.tenant_id.in_(ids)) .group_by(model.tenant_id) .all() ) for tenant_id, count in rows: counts[tenant_id][count_key] = int(count) return counts def _veto_group_delete(session, tenant_id: str, group_id: str) -> None: from govoplan_files.backend.db.models import FileAsset, FileConnectorSpace, FileFolder, FileShare owned_asset_count = session.query(FileAsset).filter(FileAsset.tenant_id == tenant_id, FileAsset.owner_group_id == group_id).count() owned_folder_count = session.query(FileFolder).filter(FileFolder.tenant_id == tenant_id, FileFolder.owner_group_id == group_id).count() owned_connector_space_count = session.query(FileConnectorSpace).filter( FileConnectorSpace.tenant_id == tenant_id, FileConnectorSpace.owner_group_id == group_id, ).count() shared_asset_count = session.query(FileShare).filter( FileShare.tenant_id == tenant_id, FileShare.target_type == "group", FileShare.target_id == group_id, ).count() if owned_asset_count or owned_folder_count or owned_connector_space_count or shared_asset_count: raise ValueError("Cannot remove the group while it owns files, folders, connector spaces or file shares.") def _files_router(context: ModuleContext): from govoplan_files.backend.runtime import configure_runtime configure_runtime(registry=context.registry, settings=context.settings) from govoplan_files.backend.router import router return router REMOTE_STORAGE_PROVIDER = ExternalProviderDeclaration( id=REMOTE_STORAGE_PROVIDER_ID, module_id="files", label="Remote file storage mirror", maturity="synchronize", operations=("discover", "search", "read", "synchronize", "preview"), objects=( ProviderObjectDeclaration( object_type="remote_folder", field_groups=("identity", "hierarchy", "display", "source_metadata"), authority_modes=("external_authoritative", "external_mirror"), default_authority_mode="external_mirror", ), ProviderObjectDeclaration( object_type="remote_file", field_groups=("identity", "content", "version", "source_metadata"), authority_modes=("external_authoritative", "external_mirror"), default_authority_mode="external_mirror", ), ), behavior=ProviderBehaviorDeclaration( revision_tokens="Remote path, provider revision, size, and content digest are retained on managed imports.", concurrency="A sync compares the frozen source reference and revision before creating a new managed version.", freshness="Provider state distinguishes software availability from an unobserved live remote binding.", health="Unsupported providers or missing optional transports fail closed; live health remains unknown without a probe.", max_read_items=5000, idempotency="Source profile, remote object reference, and revision/digest suppress duplicate managed versions.", retry="Operators repeat bounded browse/import after a classified transport failure; effects are not blindly retried.", timeout_seconds=30, conflicts="Managed-file conflict policy is explicit; the current provider never mutates the remote source.", outcome_unknown="An interrupted download is discarded unless its complete digest and managed version commit are confirmed.", outcome_unknown_supported=True, evidence="Managed versions retain connector profile, remote object identity, source revision, digest, and acquisition time.", audit_event_types=( "files.connector.accessed", "files.connector.imported", "files.connector.synced", ), correction="A later acquisition creates a new managed version and preserves prior provenance.", rollback="Remote reads require no remote rollback; incomplete local objects are reconciled as orphans.", compensation="A wrongly imported managed version can be retired under Files policy without deleting the source.", reconciliation="Re-read source metadata and digest, then compare the committed managed version and object-store inventory.", outage="Previously imported managed versions remain available while remote spaces report unknown or stale state.", classifications=("internal", "confidential", "personal"), purposes=("governed file acquisition", "managed evidence snapshot"), retention="Files retention applies to managed versions; external retention remains provider-owned.", secret_handling="Credentials remain encrypted or deployment-owned and never appear in provider state or provenance.", ), documentation_topic_ids=("files.governed-connectors-and-provenance",), ) manifest = ModuleManifest( id="files", name="Files", version="0.1.9", required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), optional_dependencies=("campaigns", "encryption"), provides_interfaces=( ModuleInterfaceProvider(name="files.access", version="0.1.6"), ModuleInterfaceProvider(name="files.campaign_attachments", version="0.1.6"), ModuleInterfaceProvider(name=CAPABILITY_FILES_ARTIFACT_STORE, version="0.1.14"), ), requires_interfaces=( ModuleInterfaceRequirement( name="campaigns.access", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name=CAPABILITY_ENCRYPTION_CONTENT_CIPHER, version_min="1.0.0", version_max_exclusive="2.0.0", optional=True, ), ), permissions=PERMISSIONS, route_factory=_files_router, role_templates=ROLE_TEMPLATES, tenant_summary_providers=(_tenant_summary,), tenant_summary_batch_providers=(_tenant_summary_batch,), delete_veto_providers={"group": (_veto_group_delete,)}, nav_items=(NavItem(path="/files", label="Files", icon="folder", required_any=("files:file:read",), order=40),), frontend=FrontendModule( module_id="files", package_name="@govoplan/files-webui", routes=( FrontendRoute( path="/files", component="FilesPage", required_any=("files:file:read",), order=40, ), ), nav_items=(NavItem(path="/files", label="Files", icon="folder", required_any=("files:file:read",), order=40),), view_surfaces=( ViewSurface(id="files.admin.system-connectors", module_id="files", kind="section", label="System file connections", order=75), ViewSurface(id="files.admin.tenant-connectors", module_id="files", kind="section", label="Tenant file connections", order=65), ViewSurface(id="files.admin.group-connectors", module_id="files", kind="section", label="Group file connections", order=65), ViewSurface(id="files.admin.user-connectors", module_id="files", kind="section", label="User file connections", order=65), ViewSurface(id="files.settings.connectors", module_id="files", kind="section", label="Personal file connections", order=20), ViewSurface(id="files.widget.spaces", module_id="files", kind="section", label="File spaces widget", order=35), ), ), documentation=( DocumentationTopic( id="files.workflow.organize-managed-files", title="Organize managed files and folders", summary="Create folders and rename, move, or copy accessible managed content with explicit conflict handling.", body=( "Organization stays inside governed personal or group spaces. Moves preserve the asset identity, while copies create new assets and versions that reuse immutable blob bytes. " "Every target conflict must be rejected, renamed, overwritten, or skipped explicitly." ), layer="configured", documentation_types=("user",), audience=("file_user", "file_manager", "process_participant"), order=42, conditions=( DocumentationCondition( required_modules=("files",), required_scopes=("files:file:read", "files:file:organize"), ), ), links=( DocumentationLink(label="Files", href="/files", kind="runtime"), DocumentationLink(label="Move or copy API", href="/api/v1/files/transfer", kind="api"), DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), ), unlocks=("Managed content can be placed at stable logical paths without bypassing space access.",), metadata={ "kind": "workflow", "route": "/files", "screen": "Files", "help_contexts": ["files.list"], "prerequisites": [ "You may view and organize managed files.", "You have write or owner access to every source item and destination space used by the operation; the global organize permission alone does not grant resource access.", ], "steps": [ "Open Files and select the personal or group space to organize.", "Create the required destination folders or select the files and folders to rename, move, or copy.", "Choose the destination and resolve each target conflict explicitly.", "Apply the operation and reopen the destination folder.", ], "outcome": "The selected content has the intended governed owner and logical path.", "verification": "Confirm each resulting path and owner; for a move, also confirm the old path is gone, and for a copy, confirm the source remains.", "related_topic_ids": [ "files.workflow.upload-managed-files", "files.workflow.find-and-download-files", "files.workflow.delete-managed-files", ], }, ), DocumentationTopic( id="files.workflow.find-and-download-files", title="Find and download managed files", summary="Search accessible managed content and download a current file version or a ZIP archive of a selection.", body=( "Files can be sorted and searched by logical path or name pattern. A download always uses the accessible current version; a multi-file selection can be generated as a temporary ZIP archive. " "There is no dedicated content-preview service yet." ), layer="configured", documentation_types=("user",), audience=("file_user", "file_manager", "process_participant"), order=43, conditions=( DocumentationCondition( required_modules=("files",), required_scopes=("files:file:read", "files:file:download"), ), ), links=( DocumentationLink(label="Files", href="/files", kind="runtime"), DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), ), unlocks=("Authorized users can retrieve governed current versions without gaining organization or sharing authority.",), metadata={ "kind": "workflow", "route": "/files", "screen": "Files", "help_contexts": ["files.list"], "prerequisites": ["You may view and download managed files in the relevant space."], "steps": [ "Open Files and select the relevant personal or group space.", "Navigate folders, sort the list, or use a path/name pattern to find the intended content.", "Review the displayed owner, path, size, checksum, and version details.", "Download one current file version, or select several files and choose Download ZIP.", ], "outcome": "The authorized current file bytes or generated archive are downloaded to the local device.", "verification": "Confirm the downloaded names and, where integrity matters, compare the file bytes with the displayed checksum.", "related_topic_ids": [ "files.workflow.organize-managed-files", "files.reference.snapshot-provenance-and-capabilities", ], }, ), DocumentationTopic( id="files.workflow.share-managed-files", title="Manage access to managed files", summary="List, grant, update, expire, or revoke direct read, write, and manage access without changing ownership.", body=( "File owners and file administrators can manage direct shares for users, groups, the tenant, and Campaign. Expired and revoked grants stop authorizing access immediately while independent active grants remain effective. " "The Files share dialog lists active and historical grants, and revocation is idempotent." ), layer="available", documentation_types=("user",), audience=("file_manager", "process_participant"), order=44, conditions=( DocumentationCondition( required_modules=("files",), required_scopes=("files:file:read", "files:file:share"), ), ), links=( DocumentationLink(label="Files", href="/files", kind="runtime"), DocumentationLink(label="List and manage shares", 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=("A supporting process can grant governed file access without changing file ownership.",), metadata={ "kind": "workflow", "route": "/files", "screen": "Files", "help_contexts": ["files.list"], "prerequisites": [ "You have the Files share permission and own the file, or administer file spaces for the active tenant.", "The intended user, group, tenant, or Campaign target exists and is active.", ], "steps": [ "Select one managed file, choose Manage shares, and review the effective and historical direct grants.", "Select the intended user, group, or tenant and choose read, write, or manage access plus an optional expiry.", "Grant or update the share without changing file ownership.", "Revoke a grant when it is no longer needed; repeated revocation is a no-op.", ], "limitations": [ "Campaign-target grants are normally created by the Campaign integration rather than selected manually in the Files dialog.", "A user may retain access through another active direct grant or ownership path after one share is revoked.", ], "outcome": "Direct access has the requested permission and lifetime while the managed asset keeps its owner.", "verification": "Test one intended and one denied path, then expire or revoke the grant and verify that only independent access paths remain.", "related_topic_ids": [ "files.workflow.find-and-download-files", "files.assurance.process-and-release-readiness", ], }, ), DocumentationTopic( id="files.workflow.delete-managed-files", title="Delete managed files and folders", summary="Soft-delete accessible managed files or a folder tree where current policy allows it.", body=( "Deletion hides the selected managed assets rather than hard-purging their stored evidence. Folder deletion is recursive by default and includes child folders and files; a non-recursive request fails for a non-empty folder. " "There is no self-service restore or hard-purge workflow today." ), layer="configured", documentation_types=("user",), audience=("file_user", "file_manager", "process_participant"), order=45, conditions=( DocumentationCondition( required_modules=("files",), required_scopes=("files:file:read", "files:file:delete"), ), ), links=( DocumentationLink(label="Files", href="/files", kind="runtime"), DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), ), unlocks=("Authorized users can remove obsolete content from active file views while preserving the current soft-delete boundary.",), metadata={ "kind": "workflow", "route": "/files", "screen": "Files", "help_contexts": ["files.list"], "prerequisites": [ "You may view and delete the selected managed content and have write or owner access to every affected asset or folder.", "You have reviewed the complete folder tree when deleting recursively.", ], "steps": [ "Open Files and select the files or folder to delete.", "Review the selection and, for a folder, all content below it.", "Confirm the delete action.", "Refresh or reopen the space and verify that the selected paths are no longer active.", ], "limitations": [ "Deletion is soft deletion, not a hard purge.", "There is no self-service restore or hard-purge workflow.", ], "outcome": "The selected content is hidden from active Files views under the current soft-delete model.", "verification": "Confirm the deleted paths no longer appear in the active space; do not treat the action as physical erasure.", "related_topic_ids": [ "files.workflow.organize-managed-files", "files.assurance.process-and-release-readiness", ], }, ), DocumentationTopic( id="files.governed-connectors-and-provenance", title="Govern file connections and credential deletion", summary="Keep endpoint profiles, reusable credentials, and inherited connector policy separate, and understand what DELETE removes immediately.", body=( "System, tenant, and one user/group/campaign leaf form the effective policy chain: deny rules win and every configured allow rule must match. " "Responses redact secret values and deployment references. Deleting a database-managed credential or profile immediately scrubs Files-owned encrypted material and private metadata in the same transaction as a non-secret audit event; dependent profiles are disabled, while legacy non-owned references are only detached and audited." ), layer="configured", documentation_types=("admin",), audience=("file_admin", "tenant_admin", "system_admin", "security_auditor"), order=50, conditions=( DocumentationCondition( required_modules=("files",), any_scopes=( "files:file:admin", "admin:settings:read", "admin:settings:write", "system:settings:read", "system:settings:write", ), ), ), links=( DocumentationLink(label="Tenant connector administration", href="/admin?section=tenant-file-connectors", kind="runtime"), DocumentationLink(label="System connector administration", href="/admin?section=system-file-connectors", kind="runtime"), DocumentationLink(label="Tenant connector policy API", href="/api/v1/files/connectors/policies/tenant", kind="api"), DocumentationLink(label="Connector credentials API", href="/api/v1/files/connectors/credentials", kind="api"), DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), ), related_modules=("access", "audit", "mail"), unlocks=("Scoped, explainable external-file access without exposing credentials to consuming modules.",), configuration_keys=( "GOVOPLAN_FILES_CONNECTOR_PROFILES_JSON", "GOVOPLAN_FILES_CONNECTOR_PROFILES_FILE", "GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST", "GOVOPLAN_CONNECTOR_CA_BUNDLE_ALLOWLIST", "MASTER_KEY_B64", ), metadata={ "kind": "reference", "route": "/admin?section=tenant-file-connectors", "screen": "File connections", "section": "Profiles, credentials, and effective connector policy", "security_invariants": [ "New API-managed external secret references fail closed until Files can prove ownership and provider-side deletion.", "Deletion and destructive retirement scrub Files-owned encrypted connector material before completion and emit non-secret audit evidence.", "Legacy non-owned external references are detached and audited, never sent to an arbitrary provider delete operation.", ], "related_topic_ids": [ "files.workflow.import-managed-snapshot", "files.reference.integrity-recovery-and-fail-closed-transports", "mail.profiles-and-policy", ], }, ), DocumentationTopic( id="files.reference.integrity-recovery-and-fail-closed-transports", title="Operate Files integrity, recovery, and connector transport safety", summary="Back up database evidence, blob ciphertext, and Encryption custody as one recovery unit, and keep unsupported SDK transports fail-closed.", body=( "Local durable storage is the operational baseline. Recover Files from a coordinated database/blob snapshot with the matching Encryption tables and original deployment master key, then run the bounded resumable integrity scan and verify representative protected and unprotected access paths. Protected scans verify stored ciphertext before decryption and then verify plaintext semantic evidence. Missing or mismatched blobs are quarantined; orphan objects are reported before dry-run-first, explicitly authorized cleanup. " "Arbitrary external S3 managed storage/connectors and SMB connectors fail closed until botocore redirects/endpoint discovery and SMB initial connections/DFS referrals support connection-time DNS/IP pinning. Installer-owned Garage storage is supported only at the exact deployment service endpoint with its explicit trust marker. Destructive module retirement drops database tables but does not remove backend blob objects." ), layer="configured", documentation_types=("admin",), audience=("operator", "system_admin", "security_auditor"), order=51, conditions=( DocumentationCondition( required_modules=("files",), any_scopes=( "files:file:admin", "admin:settings:read", "system:settings:read", "system:audit:read", ), ), ), links=( DocumentationLink(label="System file connections", href="/admin?section=system-file-connectors", kind="runtime"), DocumentationLink(label="Connector provider status", href="/api/v1/files/connectors/providers", kind="api"), DocumentationLink(label="Create an integrity scan", href="/api/v1/files/integrity/scans", kind="api"), DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), ), related_modules=("audit", "encryption", "ops"), unlocks=("Recoverable managed-file evidence without weakening outbound peer validation.",), configuration_keys=( "FILE_STORAGE_BACKEND", "FILE_STORAGE_LOCAL_ROOT", "FILE_STORAGE_LOCAL_FALLBACK_ROOTS", "FILE_STORAGE_S3_DEPLOYMENT_MANAGED", "FILE_UPLOAD_MAX_BYTES", "FILE_UPLOAD_ZIP_MAX_BYTES", "FILE_ARCHIVE_MAX_ENTRIES", "FILE_ARCHIVE_MAX_EXPANDED_BYTES", "FILE_ARCHIVE_MAX_EXPANSION_RATIO", "FILE_ARCHIVE_PREVIEW_TTL_SECONDS", "MASTER_KEY_B64", "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS", "GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES", "GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES", ), metadata={ "kind": "reference", "route": "/admin?section=system-file-connectors", "screen": "System file connections and deployment operations", "section": "Storage integrity, backup/recovery, and fail-closed transports", "recovery_unit": ["Files database rows", "Encryption envelope and wrapped-key rows", "managed blob namespace", "MASTER_KEY_B64", "deployment-owned connector configuration"], "verification": "After restore, complete a checksum-enabled integrity scan, resolve every missing/corrupt finding, approve or retain every reported orphan, verify authorized and denied access, and test one permitted pinned HTTP connector.", "related_topic_ids": [ "files.governed-connectors-and-provenance", "files.reference.snapshot-provenance-and-capabilities", ], }, ), DocumentationTopic( id="files.reference.snapshot-provenance-and-capabilities", title="Integrate through managed snapshots and Files capabilities", summary="Other modules consume stable Files capabilities or HTTP contracts and retain exact version evidence instead of importing Files internals.", body=( "Use files.access to explain resource access and files.campaign_attachments to freeze campaign inputs at an exact asset, version, blob, checksum, and source revision. " "Import external content before a governed use, preserve provenance on derived snapshots, and keep collaboration, provider sync, OAuth, remote mutation, and domain workflow state in their owning modules." ), layer="available", documentation_types=("admin", "user"), audience=("module_integrator", "file_admin", "campaign_admin", "process_designer"), order=52, conditions=( DocumentationCondition( required_modules=("files",), any_scopes=("files:file:read", "files:file:upload", "files:file:admin"), ), ), links=( DocumentationLink(label="Files", href="/files", kind="runtime"), DocumentationLink(label="Files API", href="/api/v1/files", kind="api"), DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), ), related_modules=("campaigns", "docs"), unlocks=("Campaign, report, template, workflow, and document modules can exchange governed input/output snapshots.",), metadata={ "kind": "reference", "route": "/files", "screen": "Files and module integration", "section": "Managed snapshot provenance and capability boundaries", "provided_interfaces": ["files.access@0.1.6", "files.campaign_attachments@0.1.6"], "provenance_fields": ["connector_id", "provider", "external_id", "external_path", "revision", "metadata"], "related_topic_ids": [ "files.workflow.import-managed-snapshot", "files.governed-connectors-and-provenance", "files.reference.integrity-recovery-and-fail-closed-transports", ], }, ), 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 self-service restore or hard purge, no enforced retention or legal hold, and no dedicated canonical audit event for every ordinary Files mutation. Share grant, change, expiry, and revocation operations do emit dedicated audit records. Record remaining 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=53, 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", "FILE_ARCHIVE_MAX_ENTRIES", "FILE_ARCHIVE_MAX_EXPANDED_BYTES", "FILE_ARCHIVE_MAX_EXPANSION_RATIO", "FILE_ARCHIVE_PREVIEW_TTL_SECONDS", "MASTER_KEY_B64", "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS", ), metadata={ "kind": "workflow", "route": "/files", "screen": "Files process and release assurance", "help_contexts": ["files.list"], "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 archive preview and confirmation, every required conflict strategy, organization, download, and soft deletion.", "Where connectors are configured, verify policy explanation and one pinned HTTP provider; verify installer-owned Garage when selected, and verify arbitrary external 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-managed-files", "files.workflow.upload-and-unpack-zip", "files.workflow.organize-managed-files", "files.workflow.find-and-download-files", "files.workflow.share-managed-files", "files.workflow.delete-managed-files", "files.workflow.import-managed-snapshot", "files.governed-connectors-and-provenance", "files.reference.integrity-recovery-and-fail-closed-transports", "files.reference.snapshot-provenance-and-capabilities", ], }, ), DocumentationTopic( id="files.reference.shared-storage-profile", title="Operate Files with shared object storage", summary="Choose local, host-shared, or S3-backed storage consistently with the runtime topology.", body="Core supplies the common local/S3 object-storage backend while Files owns file metadata and object-key semantics. Local storage is valid for one runtime process; a shared host volume supports same-host replicas; independent hosts require an explicitly trusted HTTPS S3-compatible endpoint. Restore PostgreSQL, objects, and the master key to one coordinated recovery point.", layer="configured", documentation_types=("admin",), audience=("file_admin", "operator", "system_admin"), order=54, conditions=( DocumentationCondition( required_modules=("files",), any_scopes=("files:file:admin", "system:settings:read"), ), ), links=( DocumentationLink(label="Files", href="/files", kind="runtime"), DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), ), related_modules=("ops", "campaigns"), configuration_keys=( "GOVOPLAN_STATE_PROFILE", "GOVOPLAN_INSTALLATION_ID", "FILE_STORAGE_BACKEND", "FILE_STORAGE_LOCAL_ROOT", "FILE_STORAGE_S3_ENDPOINT_URL", "FILE_STORAGE_S3_ENDPOINT_TRUSTED", "FILE_STORAGE_S3_DEPLOYMENT_MANAGED", ), metadata={ "kind": "reference", "route": "/ops", "screen": "Storage and runtime posture", "limitations": [ "Installer-managed Garage is single-node unless an external multi-node cluster is operated separately.", "The application does not create or verify production PostgreSQL/object/key backups.", ], "verification": "Run the Files storage round-trip check and a coordinated restore drill against the exact deployment topology.", }, ), DocumentationTopic( id="files.reference.generated-artifact-store", title="Store generated module artifacts", summary="Let optional producer modules persist generated output through the Files authority boundary.", body="The files.artifact_store capability accepts generated bytes plus bounded non-secret provenance, applies Files upload authorization, ownership, path, version, and blob-storage rules, and returns provider-neutral file/version references. Idempotency uses source provenance. Artifact acceptance does not prove printing, mailing, or another external effect.", layer="available", documentation_types=("admin", "user"), audience=("file_admin", "operator", "module_admin", "integrator"), order=55, conditions=( DocumentationCondition( required_modules=("files",), any_scopes=("files:file:upload", "files:file:admin"), ), ), links=( DocumentationLink(label="Files", href="/files", kind="runtime"), DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"), DocumentationLink(label="Generated artifact contract", href="govoplan-files/docs/GENERATED_ARTIFACT_STORE.md", kind="repository"), ), related_modules=("templates", "campaigns", "reporting"), metadata={"kind": "reference", "route": "/files"}, ), ), documentation_providers=(documentation_topics,), migration_spec=MigrationSpec( module_id="files", metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=_files_retirement_provider, retirement_notes="Destructive retirement drops files-owned database tables after the installer captures a database snapshot.", ), uninstall_guard_providers=( persistent_table_uninstall_guard( file_models.FileBlob, file_models.FileFolder, file_models.FileAsset, file_models.FileVersion, file_models.FileShare, file_models.FileConnectorCredential, file_models.FileConnectorPolicy, file_models.FileConnectorProfile, file_models.FileConnectorSpace, file_models.CampaignAttachmentUse, label="Files", ), ), capability_factories={ CAPABILITY_FILES_ACCESS: lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["access_capability"]).access_capability(context), CAPABILITY_FILES_ARTIFACT_STORE: lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["artifact_store_capability"]).artifact_store_capability(context), "files.campaign_attachments": lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context), }, operational_check_providers=( OperationalCheckProviderRegistration( module_id="files", check_id="files.managed_storage_roundtrip", provider=lambda: __import__( "govoplan_files.backend.operational_checks", fromlist=["managed_storage_roundtrip_check"], ).managed_storage_roundtrip_check(), cache_seconds=60, ), ), external_providers=(REMOTE_STORAGE_PROVIDER,), external_provider_state_providers=( ExternalProviderStateProviderRegistration( module_id="files", provider_id=REMOTE_STORAGE_PROVIDER_ID, provider=remote_storage_provider_states, ), ), architecture=declared_module_architecture( layer="content_records_evidence", kind="domain", maturity="vertical_slice", documentation_ref="docs/FILES_HANDBOOK.md", test_ref="tests/test_storage_backends.py", known_limits=("Multi-node object-storage recovery evidence and every remote connector profile are not reference-ready.",), supported_authority_modes=( "native_authoritative", "external_authoritative", "external_mirror", ), owned_concepts=("file asset", "file version", "folder", "share", "connector space"), non_owned_concepts=("record disposition", "campaign attachment rule", "external storage object"), target_tested_providers=(REMOTE_STORAGE_PROVIDER_ID,), recovery_docs=("docs/FILES_HANDBOOK.md",), security_docs=("docs/CONNECTOR_BOUNDARY.md",), operations_docs=("docs/FILES_HANDBOOK.md",), ), ) def get_manifest() -> ModuleManifest: return manifest