security(files): scrub connector credentials on deletion
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
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.files import CAPABILITY_FILES_ACCESS
|
||||
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
||||
@@ -26,6 +29,55 @@ from govoplan_files.backend.db import models as file_models # noqa: F401 - popu
|
||||
register_files_change_tracking()
|
||||
|
||||
|
||||
_files_table_retirement_provider = drop_table_retirement_provider(
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
@@ -115,7 +167,7 @@ def _files_router(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="files",
|
||||
name="Files",
|
||||
version="0.1.8",
|
||||
version="0.1.9",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
optional_dependencies=("campaigns",),
|
||||
provides_interfaces=(
|
||||
@@ -149,6 +201,7 @@ manifest = ModuleManifest(
|
||||
body=(
|
||||
"Users select only connector profiles visible in their current scope and import remote content into managed Files storage before another module uses it. "
|
||||
"Administrators define profiles, separate credential references, and ordered system/tenant/owner policies; deny rules win and profile responses never expose secrets. "
|
||||
"Deleting a database-managed profile or credential immediately scrubs Files-owned encrypted material and records a non-secret audit event in the same transaction; legacy non-owned external references are detached without provider calls. "
|
||||
"Operators control private-network access deployment-wide and must keep every remote connection pinned to a policy-validated DNS/IP answer. "
|
||||
"The built-in HTTP transport pins each connection and refuses redirects. Live S3 and SMB SDK access fails closed until S3 redirects and SMB DFS referrals can be revalidated and pinned. "
|
||||
"Successful imports store the connector id, provider, remote path and identity, source revision, and selected metadata as provenance on the managed file and its audit events."
|
||||
@@ -195,6 +248,9 @@ manifest = ModuleManifest(
|
||||
"Every network peer must be policy-validated and pinned at connection time.",
|
||||
"Redirects and protocol referrals must be rejected or independently revalidated and pinned.",
|
||||
"SDK transports that cannot provide those guarantees fail before client construction.",
|
||||
"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.",
|
||||
],
|
||||
"provenance_fields": [
|
||||
"connector_id",
|
||||
@@ -213,19 +269,7 @@ manifest = ModuleManifest(
|
||||
metadata=Base.metadata,
|
||||
script_location=str(Path(__file__).with_name("migrations") / "versions"),
|
||||
retirement_supported=True,
|
||||
retirement_provider=drop_table_retirement_provider(
|
||||
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",
|
||||
),
|
||||
retirement_provider=_files_retirement_provider,
|
||||
retirement_notes="Destructive retirement drops files-owned database tables after the installer captures a database snapshot.",
|
||||
),
|
||||
uninstall_guard_providers=(
|
||||
|
||||
Reference in New Issue
Block a user