Provide managed generated artifact storage

This commit is contained in:
2026-08-02 12:38:15 +02:00
parent 359c4e9570
commit b993d8e31a
6 changed files with 228 additions and 2 deletions
+87 -1
View File
@@ -8,6 +8,11 @@ from sqlalchemy import or_
from govoplan_core.core.access import AccessDecisionProvenance, PrincipalRef
from govoplan_core.core.files import FileAccessProvider
from govoplan_core.core.files import (
ManagedArtifactRef,
ManagedArtifactStore,
ManagedArtifactWriteRequest,
)
from govoplan_core.core.modules import ModuleContext
from govoplan_core.security.module_permissions import scopes_grant_compatible
from govoplan_files.backend.db.models import FileAsset, FileFolder, FileShare
@@ -20,7 +25,11 @@ from govoplan_files.backend.storage.campaign_attachments import (
share_assets_with_campaign,
)
from govoplan_files.backend.storage.campaign_usage import record_campaign_attachment_uses_for_jobs
from govoplan_files.backend.storage.files import current_version_and_blob
from govoplan_files.backend.storage.files import (
create_file_asset,
current_version_and_blob,
sync_file_asset_from_source,
)
from govoplan_files.backend.storage.paths import normalize_folder
from govoplan_files.backend.storage.share_state import effective_file_share_clause
@@ -60,6 +69,83 @@ def campaign_capability(context: ModuleContext) -> FilesCampaignCapability:
return FilesCampaignCapability()
class FilesArtifactStore(ManagedArtifactStore):
def store_artifact(
self,
session: object,
principal: object,
*,
request: ManagedArtifactWriteRequest,
) -> ManagedArtifactRef:
if not hasattr(session, "query") or not hasattr(session, "flush"):
raise TypeError("Files artifact storage requires a SQLAlchemy session.")
if not hasattr(principal, "has") or not principal.has("files:file:upload"):
raise PermissionError("Managed artifact storage requires files:file:upload.")
user = getattr(principal, "user", None)
user_id = str(getattr(user, "id", "") or "")
tenant_id = str(getattr(principal, "tenant_id", "") or "")
if not user_id or not tenant_id:
raise PermissionError("Managed artifact storage requires a tenant user principal.")
metadata = dict(request.metadata)
if request.idempotency_key:
metadata["source_provenance"] = {
"source_type": "generated_artifact",
"connector_id": "files.artifact_store",
"provider": str(metadata.get("producer_module") or "platform"),
"external_id": request.idempotency_key,
"revision": str(metadata.get("output_sha256") or "") or None,
}
stored, _action, _previous_version_id = sync_file_asset_from_source(
session,
tenant_id=tenant_id,
owner_type="user",
owner_id=user_id,
user_id=user_id,
filename=request.filename,
data=request.payload,
metadata=metadata,
folder=request.folder,
content_type=request.content_type,
conflict_strategy="rename",
is_admin=principal.has("files:file:admin"),
)
else:
stored = create_file_asset(
session,
tenant_id=tenant_id,
owner_type="user",
owner_id=user_id,
user_id=user_id,
filename=request.filename,
data=request.payload,
folder=request.folder,
content_type=request.content_type,
description=request.description,
metadata=metadata,
conflict_strategy="rename",
is_admin=principal.has("files:file:admin"),
)
return ManagedArtifactRef(
file_asset_id=stored.asset.id,
file_version_id=stored.version.id,
filename=stored.version.filename_at_upload,
display_path=stored.asset.display_path,
content_type=stored.version.content_type or request.content_type,
size_bytes=stored.version.size_bytes,
sha256=stored.version.checksum_sha256,
provenance={
"module": "files",
"owner_type": stored.asset.owner_type,
"managed": True,
},
)
def artifact_store_capability(context: ModuleContext) -> FilesArtifactStore:
configure_runtime(registry=context.registry, settings=context.settings)
return FilesArtifactStore()
class FilesAccessService(FileAccessProvider):
def explain_resource_provenance(
self,
+29 -1
View File
@@ -7,7 +7,10 @@ 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
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,
@@ -280,6 +283,7 @@ manifest = ModuleManifest(
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(
@@ -792,6 +796,29 @@ manifest = ModuleManifest(
"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(
@@ -819,6 +846,7 @@ manifest = ModuleManifest(
),
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=(