feat: validate receipt-bound storage configuration

This commit is contained in:
2026-08-07 11:15:50 +02:00
parent 04e2bae190
commit cb37079465
5 changed files with 906 additions and 0 deletions
+22
View File
@@ -62,6 +62,28 @@ The main domain objects are:
| Campaign attachment use | Evidence connecting a campaign job or entry to an exact asset, version, blob, checksum, and stage | Retained for campaign execution evidence |
| Form evidence upload grant | A one-use, hash-only bearer grant tied to an exact Form instance/revision, purpose, custodian, size, and media-type policy | Issued for at most 15 minutes, consumed by one managed upload, then retained as evidence provenance |
## Deployment configuration packages
Files registers the `files.configuration` capability for `managed_storage`
fragments. Managed content storage is deployment-owned: the installer selects
local persistent storage, managed Garage, or an external S3-compatible service
and mounts a validated non-secret infrastructure capability receipt. Files
does not copy that endpoint or its credentials into module-owned tables.
Preflight compares the receipt's `files.storage` capability with the effective
runtime backend. It validates backend kind, sanitized S3 endpoint, bucket,
Garage management marker or external trust marker, an absolute persistent path
for local storage, and the presence of Files-owned `env:` secret references.
Secret values are never read into a plan, diagnostic, export, or fragment.
When runtime and receipt agree, the plan reports `skip`: the desired binding is
already effective, and repeated apply is a no-op. A mismatch blocks import and
explains which deployment setting must be reconciled. The provider deliberately
does not mutate process environment, migrate stored objects, probe remote
storage, or reinterpret an infrastructure replacement as safe. Use the Files
integrity and Ops checks after deployment and complete migration/recovery review
before changing an active backend.
## User tasks
The Files page is available at `/files`. Actions appear only when the current
@@ -0,0 +1,600 @@
from __future__ import annotations
from collections.abc import Mapping
import os
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit
from govoplan_core.core.configuration_packages import (
ConfigurationApplyResult,
ConfigurationDiagnostic,
ConfigurationExportResult,
ConfigurationExportSelection,
ConfigurationPackageFragment,
ConfigurationPlanItem,
ConfigurationPreflightContext,
ConfigurationPreflightResult,
ConfigurationProvider,
ConfigurationProviderDescription,
)
from govoplan_core.core.infrastructure_capabilities import (
InfrastructureCapability,
InfrastructureCapabilityReceipt,
)
from govoplan_files.backend.runtime import settings as runtime_settings
FILES_CONFIGURATION_CAPABILITY = "files.configuration"
MANAGED_STORAGE_FRAGMENT = "managed_storage"
_PAYLOAD_KEYS = frozenset(
{"capability_id", "expected_backend", "expected_source"}
)
class FilesConfigurationProvider(ConfigurationProvider):
module_id = "files"
def __init__(
self,
*,
settings: object | None = None,
environment: Mapping[str, str] | None = None,
) -> None:
self._settings = runtime_settings if settings is None else settings
self._environment = os.environ if environment is None else environment
def describe(self) -> ConfigurationProviderDescription:
return ConfigurationProviderDescription(
module_id=self.module_id,
fragment_types=(MANAGED_STORAGE_FRAGMENT,),
schema_refs={
MANAGED_STORAGE_FRAGMENT: "govoplan/files/configuration/managed-storage.v1"
},
exported_scopes=("system",),
)
def preflight(
self,
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
) -> ConfigurationPreflightResult:
if fragment.fragment_type != MANAGED_STORAGE_FRAGMENT:
return ConfigurationPreflightResult(
diagnostics=(_unsupported(fragment),),
plan=(_blocked_plan(fragment, "Fragment type is unsupported."),),
)
diagnostics, binding_ref = self._binding_diagnostics(fragment, context)
blocked = any(item.severity == "blocker" for item in diagnostics)
return ConfigurationPreflightResult(
diagnostics=tuple(diagnostics),
plan=(
ConfigurationPlanItem(
action="blocked" if blocked else "skip",
module_id="files",
fragment_type=MANAGED_STORAGE_FRAGMENT,
fragment_id=fragment.fragment_id or binding_ref,
summary=(
"Managed storage does not match the deployment receipt."
if blocked
else "Deployment-owned managed storage already matches the receipt; no module state is rewritten."
),
),
),
)
def apply(
self,
fragment: ConfigurationPackageFragment,
supplied_data: Mapping[str, Any],
context: ConfigurationPreflightContext,
) -> ConfigurationApplyResult:
del supplied_data
if fragment.fragment_type != MANAGED_STORAGE_FRAGMENT:
return ConfigurationApplyResult(diagnostics=(_unsupported(fragment),))
diagnostics, _binding_ref = self._binding_diagnostics(fragment, context)
return ConfigurationApplyResult(
diagnostics=tuple(
item for item in diagnostics if item.severity == "blocker"
)
)
def export(
self,
selection: ConfigurationExportSelection,
context: ConfigurationPreflightContext,
) -> ConfigurationExportResult:
del selection
receipt = context.infrastructure_receipt
if context.infrastructure_receipt_error:
return ConfigurationExportResult(
diagnostics=(
_receipt_error(
context.infrastructure_receipt_error,
object_ref="files.storage",
),
)
)
if receipt is None:
return ConfigurationExportResult(
diagnostics=(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_receipt_missing",
message="Files managed-storage export requires the deployment capability receipt.",
module_id="files",
object_ref="files.storage",
resolution="Mount the installer-generated receipt before exporting deployment configuration.",
),
)
)
capability = receipt.capability("files.storage")
if capability is None:
return ConfigurationExportResult(
diagnostics=(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_capability_missing",
message="The deployment receipt does not declare managed file storage.",
module_id="files",
object_ref="files.storage",
),
)
)
return ConfigurationExportResult(
fragments=(
ConfigurationPackageFragment(
module_id="files",
fragment_type=MANAGED_STORAGE_FRAGMENT,
fragment_id=f"{receipt.installation_id}:files.storage",
payload={
"capability_id": "files.storage",
"expected_backend": _expected_backend(capability.source),
"expected_source": capability.source,
},
),
)
)
def health(
self,
import_result: ConfigurationApplyResult,
context: ConfigurationPreflightContext,
) -> tuple[ConfigurationDiagnostic, ...]:
del context
return tuple(
item for item in import_result.diagnostics if item.severity == "blocker"
)
def _binding_diagnostics(
self,
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
) -> tuple[list[ConfigurationDiagnostic], str]:
diagnostics: list[ConfigurationDiagnostic] = []
payload = _files_fragment_payload(fragment, diagnostics)
if payload is None:
return diagnostics, fragment.fragment_id or "files.storage"
receipt_state = _files_receipt_capability(
fragment,
context,
payload,
diagnostics,
)
if receipt_state is None:
return diagnostics, fragment.fragment_id or "files.storage"
_receipt, capability, binding_ref = receipt_state
expected_backend = _expected_backend(capability.source)
diagnostics.extend(
_package_expectation_diagnostics(
payload,
capability,
expected_backend,
binding_ref,
)
)
active_backend = _normalized_backend(
getattr(self._settings, "file_storage_backend", "local")
)
if active_backend != expected_backend:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="files_storage_runtime_mismatch",
message=(
f"Files runtime uses {active_backend!r}, while the deployment receipt declares {expected_backend!r}."
),
module_id="files",
object_ref=binding_ref,
resolution="Reconcile deployment environment and receipt before starting or importing Files configuration.",
)
)
return diagnostics, binding_ref
diagnostics.extend(
self._runtime_storage_diagnostics(
expected_backend,
capability,
binding_ref,
)
)
if "files" not in capability.dependent_modules:
diagnostics.append(
ConfigurationDiagnostic(
severity="info",
code="infrastructure_consumer_not_declared",
message="Files is active but was not selected as a receipt consumer when the deployment plan was generated.",
module_id="files",
object_ref=binding_ref,
resolution="Regenerate the deployment plan so removal-impact inventory includes Files.",
)
)
return diagnostics, binding_ref
def _runtime_storage_diagnostics(
self,
expected_backend: str,
capability: InfrastructureCapability,
binding_ref: str,
) -> list[ConfigurationDiagnostic]:
if expected_backend == "s3":
return self._s3_diagnostics(
capability.endpoint,
capability.secret_refs,
capability.source,
binding_ref,
)
root = Path(
str(getattr(self._settings, "file_storage_local_root", "") or "")
)
if root.is_absolute():
return []
return [
ConfigurationDiagnostic(
severity="blocker",
code="files_storage_local_root_not_durable",
message="Receipt-bound local file storage requires an absolute deployment-managed path.",
module_id="files",
object_ref=binding_ref,
resolution="Set FILE_STORAGE_LOCAL_ROOT to the mounted persistent-volume path.",
)
]
def _s3_diagnostics(
self,
endpoint: Mapping[str, object],
secret_refs: tuple[str, ...],
source: str,
object_ref: str,
) -> list[ConfigurationDiagnostic]:
diagnostics: list[ConfigurationDiagnostic] = []
endpoint_url = str(
getattr(self._settings, "file_storage_s3_endpoint_url", "")
or getattr(self._settings, "s3_endpoint_url", "")
or ""
).strip()
runtime_endpoint = _redacted_endpoint(
endpoint_url,
default_port=int(endpoint.get("port") or 443),
)
receipt_endpoint = {
key: endpoint.get(key) for key in ("scheme", "host", "port") if endpoint.get(key) is not None
}
if receipt_endpoint and runtime_endpoint != receipt_endpoint:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="files_storage_endpoint_mismatch",
message="Files S3 endpoint does not match the sanitized deployment receipt endpoint.",
module_id="files",
object_ref=object_ref,
resolution="Reconcile FILE_STORAGE_S3_ENDPOINT_URL and the deployment plan before apply.",
)
)
bucket = str(
getattr(self._settings, "file_storage_s3_bucket", "")
or getattr(self._settings, "s3_bucket", "")
or ""
).strip()
if not bucket:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="files_storage_bucket_missing",
message="Files S3 storage has no configured bucket.",
module_id="files",
object_ref=object_ref,
resolution="Set FILE_STORAGE_S3_BUCKET in the deployment environment.",
)
)
for reference in secret_refs:
variable = reference.removeprefix("env:")
if not variable.startswith("FILE_STORAGE_"):
continue
if not str(self._environment.get(variable, "")).strip():
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="files_storage_secret_reference_unresolved",
message=f"Required storage secret reference {reference} is not available to the runtime.",
module_id="files",
object_ref=object_ref,
resolution="Provide the referenced environment secret without copying its value into the package.",
)
)
deployment_managed = bool(
getattr(
self._settings,
"file_storage_s3_deployment_managed",
False,
)
)
endpoint_trusted = bool(
getattr(self._settings, "file_storage_s3_endpoint_trusted", False)
)
if source == "installer-managed-garage" and not deployment_managed:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="files_storage_management_boundary_mismatch",
message="Garage is installer-managed in the receipt but not marked deployment-managed in Files runtime.",
module_id="files",
object_ref=object_ref,
resolution="Set FILE_STORAGE_S3_DEPLOYMENT_MANAGED=true for the installer-managed Garage endpoint.",
)
)
if source == "operator-supplied-s3" and not endpoint_trusted:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="files_storage_trust_boundary_missing",
message="External S3 storage must be explicitly marked as a trusted deployment endpoint.",
module_id="files",
object_ref=object_ref,
resolution="Review the endpoint and set FILE_STORAGE_S3_ENDPOINT_TRUSTED=true in deployment configuration.",
)
)
return diagnostics
def _files_fragment_payload(
fragment: ConfigurationPackageFragment,
diagnostics: list[ConfigurationDiagnostic],
) -> Mapping[str, Any] | None:
payload = fragment.payload
if not isinstance(payload, Mapping):
diagnostics.append(
_invalid(fragment, "Managed-storage payload must be an object.")
)
return None
unknown = sorted(set(payload) - _PAYLOAD_KEYS)
if not unknown:
return payload
secret_like = any(
marker in key.casefold()
for key in unknown
for marker in ("password", "secret", "token", "credential", "access_key")
)
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code=(
"files_configuration_secret_forbidden"
if secret_like
else "files_configuration_payload_invalid"
),
message=(
"Managed-storage fragments accept receipt references and non-secret expectations only."
if secret_like
else f"Managed-storage payload contains unsupported fields: {', '.join(unknown)}."
),
module_id="files",
object_ref=fragment.fragment_id or MANAGED_STORAGE_FRAGMENT,
resolution="Keep storage credentials in deployment environment references, never in a configuration package.",
)
)
return payload
def _files_receipt_capability(
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
payload: Mapping[str, Any],
diagnostics: list[ConfigurationDiagnostic],
) -> tuple[
InfrastructureCapabilityReceipt,
InfrastructureCapability,
str,
] | None:
if context.infrastructure_receipt_error:
diagnostics.append(
_receipt_error(
context.infrastructure_receipt_error,
object_ref=fragment.fragment_id or "files.storage",
)
)
return None
receipt = context.infrastructure_receipt
if receipt is None:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_receipt_missing",
message="Files managed-storage configuration requires the deployment capability receipt.",
module_id="files",
object_ref=fragment.fragment_id or "files.storage",
resolution="Mount the installer-generated receipt and rerun preflight.",
)
)
return None
binding_ref = fragment.fragment_id or f"{receipt.installation_id}:files.storage"
capability_id = _text(payload.get("capability_id")) or "files.storage"
if capability_id != "files.storage":
diagnostics.append(
_invalid(
fragment,
"Files managed-storage fragments must reference capability 'files.storage'.",
)
)
return None
capability = receipt.capability(capability_id)
if capability is None:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_capability_missing",
message="The deployment receipt does not declare managed file storage.",
module_id="files",
object_ref=binding_ref,
resolution="Regenerate the receipt from a deployment profile that declares managed storage.",
)
)
return None
state_diagnostic = _storage_capability_state_diagnostic(capability, binding_ref)
if state_diagnostic is not None:
diagnostics.append(state_diagnostic)
return None
return receipt, capability, binding_ref
def _storage_capability_state_diagnostic(
capability: InfrastructureCapability,
binding_ref: str,
) -> ConfigurationDiagnostic | None:
if capability.state == "unavailable":
return ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_capability_unavailable",
message="The deployment receipt states that managed file storage is unavailable.",
module_id="files",
object_ref=binding_ref,
resolution="Select local, managed Garage, or external S3 storage in the deployment profile.",
)
if capability.state == "available_unconfigured":
return ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_capability_unconfigured",
message="Managed file storage is available but has not been bound by the deployment runtime.",
module_id="files",
object_ref=binding_ref,
resolution="Complete the deployment-owned storage configuration before importing Files configuration.",
)
return None
def _package_expectation_diagnostics(
payload: Mapping[str, Any],
capability: InfrastructureCapability,
expected_backend: str,
binding_ref: str,
) -> list[ConfigurationDiagnostic]:
diagnostics: list[ConfigurationDiagnostic] = []
package_backend = (
_text(payload.get("expected_backend")) or expected_backend
).casefold()
if package_backend != expected_backend:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_backend_mismatch",
message="The package storage backend expectation conflicts with the deployment receipt.",
module_id="files",
object_ref=binding_ref,
resolution="Use the receipt backend or regenerate the deployment package after review.",
)
)
package_source = _text(payload.get("expected_source"))
if package_source and package_source != capability.source:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_source_mismatch",
message="The package storage source expectation conflicts with the deployment receipt.",
module_id="files",
object_ref=binding_ref,
resolution="Review the provider replacement and regenerate the package from the active receipt.",
)
)
return diagnostics
def _expected_backend(source: str) -> str:
return "local" if source == "host-local" else "s3"
def _normalized_backend(value: object) -> str:
clean = str(value or "local").strip().casefold()
if clean in {"local", "filesystem", "fs"}:
return "local"
if clean in {"s3", "garage"}:
return "s3"
return clean
def _redacted_endpoint(value: str, *, default_port: int) -> dict[str, object]:
try:
parsed = urlsplit(value)
host = parsed.hostname
if not parsed.scheme or not host:
return {"reference": "unresolved"}
port = parsed.port or default_port
except ValueError:
return {"reference": "unresolved"}
return {"scheme": parsed.scheme, "host": host, "port": port}
def _text(value: object) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None
def _receipt_error(error: str, *, object_ref: str) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_receipt_invalid",
message=f"The deployment capability receipt is invalid: {error}",
module_id="files",
object_ref=object_ref,
resolution="Repair or regenerate the deployment receipt before importing Files configuration.",
)
def _invalid(
fragment: ConfigurationPackageFragment,
message: str,
) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="files_configuration_payload_invalid",
message=message,
module_id="files",
object_ref=fragment.fragment_id or fragment.fragment_type,
resolution="Review the Files configuration-package fragment and rerun preflight.",
)
def _unsupported(fragment: ConfigurationPackageFragment) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="fragment_type_unsupported",
message=f"Files configuration does not support fragment type {fragment.fragment_type!r}.",
module_id="files",
object_ref=fragment.fragment_id or fragment.fragment_type,
)
def _blocked_plan(
fragment: ConfigurationPackageFragment,
summary: str,
) -> ConfigurationPlanItem:
return ConfigurationPlanItem(
action="blocked",
module_id="files",
fragment_type=fragment.fragment_type,
fragment_id=fragment.fragment_id,
summary=summary,
)
__all__ = ["FILES_CONFIGURATION_CAPABILITY", "FilesConfigurationProvider"]
+64
View File
@@ -48,6 +48,9 @@ from govoplan_core.core.search import SearchSourceProviderRegistration
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.configuration_provider import (
FILES_CONFIGURATION_CAPABILITY,
)
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.form_evidence import (
@@ -85,6 +88,15 @@ _files_table_retirement_provider = drop_table_retirement_provider(
)
def _configuration_provider(context: ModuleContext) -> object:
del context
from govoplan_files.backend.configuration_provider import (
FilesConfigurationProvider,
)
return FilesConfigurationProvider()
def _files_retirement_provider(session: object | None, module_id: str):
plan = _files_table_retirement_provider(session, module_id)
base_executor = plan.destroy_data_executor
@@ -568,6 +580,57 @@ manifest = ModuleManifest(
),
),
documentation=(
DocumentationTopic(
id="files.configuration-package.managed-storage",
title="Validate deployment-managed file storage",
summary="Compare Files runtime storage with the non-secret deployment receipt without rewriting infrastructure settings.",
body=(
"The Files configuration provider validates the files.storage capability against the effective local or S3 runtime. "
"It checks the backend, sanitized endpoint, bucket, trust or management marker, durable local path, and presence of referenced environment secrets. "
"Matching configuration is already effective and therefore reports skip on every apply. Drift blocks the package; Files never copies secret values, rewrites process environment, "
"or treats a storage replacement as an implicit migration. Run Files integrity and Ops checks after deployment changes."
),
layer="configured",
documentation_types=("admin",),
audience=("file_manager", "administrator", "operator"),
related_modules=("core", "ops"),
conditions=(
DocumentationCondition(
required_modules=("files", "access"),
any_scopes=("admin:settings:read", "system:settings:read"),
),
),
links=(
DocumentationLink(
label="Configuration packages",
href="/admin?section=configuration-packages",
kind="runtime",
),
DocumentationLink(
label="Files handbook",
href="govoplan-files/docs/FILES_HANDBOOK.md",
kind="repository",
),
),
translations={
"de": {
"title": "Bereitstellungsverwalteten Dateispeicher prüfen",
"summary": "Den Laufzeitspeicher von Files mit dem nicht geheimen Bereitstellungsnachweis vergleichen, ohne Infrastruktureinstellungen umzuschreiben.",
"body": (
"Der Files-Konfigurationsprovider prüft die Fähigkeit files.storage gegen die wirksame lokale oder S3-Laufzeitkonfiguration. "
"Geprüft werden Backend, bereinigter Endpunkt, Bucket, Vertrauens- oder Verwaltungskennzeichen, dauerhafter lokaler Pfad sowie das Vorhandensein referenzierter Umgebungsgeheimnisse. "
"Eine passende Konfiguration ist bereits wirksam und meldet deshalb bei jeder Anwendung skip. Abweichungen blockieren das Paket; Files kopiert keine Geheimwerte, verändert keine Prozessumgebung "
"und behandelt einen Speicherwechsel nicht als stillschweigende Migration. Nach Bereitstellungsänderungen sind die Integritäts- und Ops-Prüfungen auszuführen."
),
}
},
metadata={
"kind": "workflow",
"route": "/admin?section=configuration-packages",
"help_contexts": ["admin.configuration-packages", "files.admin.tenant-integrity"],
},
order=4,
),
DocumentationTopic(
id="files.quick-access-and-product-area",
title="Files in Records and documents and Quick Access",
@@ -1407,6 +1470,7 @@ manifest = ModuleManifest(
),
),
capability_factories={
FILES_CONFIGURATION_CAPABILITY: _configuration_provider,
CAPABILITY_FILES_ACCESS: lambda context: __import__(
"govoplan_files.backend.capabilities", fromlist=["access_capability"]
).access_capability(context),
+219
View File
@@ -0,0 +1,219 @@
from __future__ import annotations
from types import SimpleNamespace
import unittest
from govoplan_core.core.configuration_packages import (
ConfigurationPackageFragment,
ConfigurationPreflightContext,
)
from govoplan_core.core.infrastructure_capabilities import (
infrastructure_capability_receipt_from_mapping,
)
from govoplan_files.backend.configuration_provider import (
FILES_CONFIGURATION_CAPABILITY,
FilesConfigurationProvider,
)
from govoplan_files.backend.manifest import manifest
def _receipt(*, source: str = "host-local", state: str = "configured"):
endpoint = (
{"kind": "filesystem", "reference": "volume:files-data"}
if source == "host-local"
else {"scheme": "http", "host": "garage", "port": 3900}
)
secret_refs = (
[]
if source == "host-local"
else [
"env:FILE_STORAGE_S3_ACCESS_KEY_ID",
"env:FILE_STORAGE_S3_SECRET_ACCESS_KEY",
"env:GARAGE_RPC_SECRET",
]
)
return infrastructure_capability_receipt_from_mapping(
{
"schema_version": 1,
"installation_id": "files-provider-test",
"profile": "evaluation",
"capabilities": [
{
"id": "files.storage",
"label": "Managed file content storage",
"state": state,
"source": source,
"detail": "Deployment-owned storage binding.",
"endpoint": endpoint,
"secret_refs": secret_refs,
"dependent_modules": ["files"],
}
],
"post_install_tasks": [],
}
)
def _local_settings(**overrides):
values = {
"file_storage_backend": "local",
"file_storage_local_root": "/var/lib/govoplan/files",
}
values.update(overrides)
return SimpleNamespace(**values)
def _s3_settings(**overrides):
values = {
"file_storage_backend": "s3",
"file_storage_s3_endpoint_url": "http://garage:3900",
"file_storage_s3_bucket": "files",
"file_storage_s3_deployment_managed": True,
"file_storage_s3_endpoint_trusted": False,
}
values.update(overrides)
return SimpleNamespace(**values)
class FilesConfigurationProviderTests(unittest.TestCase):
def test_provider_is_registered(self) -> None:
self.assertIn(FILES_CONFIGURATION_CAPABILITY, manifest.capability_factories)
def test_matching_local_storage_is_an_idempotent_noop(self) -> None:
provider = FilesConfigurationProvider(
settings=_local_settings(),
environment={},
)
context = ConfigurationPreflightContext(
infrastructure_receipt=_receipt(),
)
fragment = ConfigurationPackageFragment(
module_id="files",
fragment_type="managed_storage",
fragment_id="files-storage",
payload={
"expected_backend": "local",
"expected_source": "host-local",
},
)
first = provider.preflight(fragment, context)
applied = provider.apply(fragment, {}, context)
second = provider.preflight(fragment, context)
self.assertEqual("skip", first.plan[0].action)
self.assertEqual("skip", second.plan[0].action)
self.assertEqual((), applied.diagnostics)
self.assertEqual({}, applied.created_refs)
self.assertEqual({}, applied.updated_refs)
def test_runtime_backend_mismatch_blocks_without_rewriting_settings(self) -> None:
settings = _local_settings()
provider = FilesConfigurationProvider(settings=settings, environment={})
context = ConfigurationPreflightContext(
infrastructure_receipt=_receipt(source="installer-managed-garage"),
)
fragment = ConfigurationPackageFragment(
module_id="files",
fragment_type="managed_storage",
payload={},
)
result = provider.preflight(fragment, context)
self.assertEqual("blocked", result.plan[0].action)
self.assertIn(
"files_storage_runtime_mismatch",
{item.code for item in result.diagnostics},
)
self.assertEqual("local", settings.file_storage_backend)
def test_garage_binding_requires_only_files_secret_references(self) -> None:
fragment = ConfigurationPackageFragment(
module_id="files",
fragment_type="managed_storage",
payload={},
)
context = ConfigurationPreflightContext(
infrastructure_receipt=_receipt(source="installer-managed-garage"),
)
missing = FilesConfigurationProvider(
settings=_s3_settings(),
environment={},
).preflight(fragment, context)
available = FilesConfigurationProvider(
settings=_s3_settings(),
environment={
"FILE_STORAGE_S3_ACCESS_KEY_ID": "reference-resolved",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "reference-resolved",
},
).preflight(fragment, context)
self.assertEqual("blocked", missing.plan[0].action)
self.assertEqual(
2,
sum(
item.code == "files_storage_secret_reference_unresolved"
for item in missing.diagnostics
),
)
self.assertEqual("skip", available.plan[0].action)
def test_operator_supplied_s3_requires_explicit_endpoint_trust(self) -> None:
receipt = _receipt(source="operator-supplied-s3", state="externally_supplied")
fragment = ConfigurationPackageFragment(
module_id="files",
fragment_type="managed_storage",
payload={},
)
context = ConfigurationPreflightContext(infrastructure_receipt=receipt)
environment = {
"FILE_STORAGE_S3_ACCESS_KEY_ID": "reference-resolved",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "reference-resolved",
}
blocked = FilesConfigurationProvider(
settings=_s3_settings(
file_storage_s3_deployment_managed=False,
file_storage_s3_endpoint_trusted=False,
),
environment=environment,
).preflight(fragment, context)
ready = FilesConfigurationProvider(
settings=_s3_settings(
file_storage_s3_deployment_managed=False,
file_storage_s3_endpoint_trusted=True,
),
environment=environment,
).preflight(fragment, context)
self.assertEqual("blocked", blocked.plan[0].action)
self.assertIn(
"files_storage_trust_boundary_missing",
{item.code for item in blocked.diagnostics},
)
self.assertEqual("skip", ready.plan[0].action)
def test_inline_storage_secret_is_rejected(self) -> None:
provider = FilesConfigurationProvider(
settings=_local_settings(),
environment={},
)
result = provider.preflight(
ConfigurationPackageFragment(
module_id="files",
fragment_type="managed_storage",
payload={"secret_access_key": "inline"},
),
ConfigurationPreflightContext(infrastructure_receipt=_receipt()),
)
self.assertEqual("blocked", result.plan[0].action)
self.assertIn(
"files_configuration_secret_forbidden",
{item.code for item in result.diagnostics},
)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -4,6 +4,7 @@ import unittest
STATIC_TOPIC_IDS = {
"files.configuration-package.managed-storage",
"files.quick-access-and-product-area",
"files.search.managed-content",
"files.workflow.organize-managed-files",