Compare commits

..
6 Commits
Author SHA1 Message Date
zemion 2baa8f2657 feat: promote files to a stable product destination
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 17:58:33 +02:00
zemion 6176e9f40e feat: inventory storage infrastructure dependencies
Module Package Release / publish-packages (push) Successful in 12s
2026-08-24 14:56:33 +02:00
zemion 11b9b7c4c6 fix(webui): bind file credentials and deletion to help
Module Package Release / publish-packages (push) Successful in 13s
2026-08-24 11:36:40 +02:00
zemion ab08c73777 docs: complete German structured documentation
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 01:15:33 +02:00
zemion 74c26090d7 docs(files): complete German reference coverage
Module Package Release / publish-packages (push) Successful in 11s
2026-08-23 21:09:37 +02:00
zemion 62047e7a20 docs(files): document pinned SMB validation 2026-08-21 23:20:15 +02:00
15 changed files with 848 additions and 37 deletions
+14 -6
View File
@@ -139,12 +139,20 @@ cd /mnt/DATA/git/govoplan-files/dev/connectors
```
The script reads `.env` from this directory when present and seeds tiny WebDAV,
Nextcloud, SMB, and MinIO fixtures. WebDAV and Nextcloud are browsed and imported
through the pinned connector transport. SMB and S3 product access deliberately
fails closed until their SDK transports support peer pinning, so their fixtures
are retained for transport development and reported as expected optional skips.
The `--require-smb` and `--require-s3` switches are useful only while developing
that transport support and currently make the smoke check fail by design.
Nextcloud, SMB, and MinIO fixtures. WebDAV and Nextcloud use the pinned HTTP
transport. SMB uses the Files-owned `smbprotocol` connection cache so initial
connections, reconnects, aliases, and DFS referral targets are pinned. S3 uses
pinned botocore pools for retries, redirects, discovery, and aliases. Every
socket destination is revalidated when it opens, including when private-network
access is enabled for these local fixtures.
SMB and S3 are optional by default: a missing provider dependency or unavailable
target is reported as `SKIP`. Use `--require-smb` and `--require-s3` to turn such
provider failures into a non-zero smoke result during deployment validation. The
bundled Samba service is a deterministic single-share target, not a DFS
topology. Referral and reconnect behavior is covered by the transport contract
tests; deployments using DFS must additionally run the required SMB smoke check
against their actual referral topology and record the resolved peers.
SMB smoke checks need the optional Python dependency in the environment running
the script:
+18
View File
@@ -85,6 +85,14 @@ 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.
The Files infrastructure dependency provider makes that review concrete. Its
authorized, non-secret Ops inventory reports the active runtime binding and
aggregates persisted `FileBlob` rows by storage backend with blob counts and
byte totals. A host apply that changes `files.storage` requires a fresh,
complete inventory from the same installation and shows the migration and
checksum-verification action before any service is replaced. Object keys,
tenant identifiers and storage credentials are not exported.
## User tasks
The Files page is available at `/files`. Actions appear only when the current
@@ -1034,6 +1042,16 @@ attempt. Botocore retries, redirects, endpoint discovery, and aliases, plus SMB
reconnects and DFS referrals, must pass through the pinned factories. A changed
or unsupported SDK seam must fail before a usable client/session is returned.
SMB deployments use `smbprotocol>=1.13`; signing is required by default and SMB
encryption can be required per connector profile. Private targets additionally
require the governed private-network setting, but that setting never bypasses
per-connection address validation. The bundled development Samba target checks
live initial browse/import behavior with `smoke.py --require-smb`. It is a
single-share fixture rather than a DFS topology: reconnect and referral handling
is enforced by the transport contract suite, while operators using DFS must run
the same required smoke check against their institutional referral topology and
record the initial and referred peers as deployment evidence.
### Imported evidence and sync
Given a permitted WebDAV file is imported, its managed response and audit event
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/files-webui",
"version": "0.1.20",
"version": "0.1.25",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
@@ -26,7 +26,7 @@
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9",
"lucide-react": "^1.23.0",
"@govoplan/core-webui": "^0.1.18"
"@govoplan/core-webui": "^0.1.44"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
+2 -2
View File
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-files"
version = "0.1.20"
version = "0.1.25"
description = "GovOPlaN files module with backend and WebUI integration."
readme = "README.md"
requires-python = ">=3.12"
license = { file = "LICENSE" }
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.20",
"govoplan-core>=0.1.44",
"defusedxml>=0.7,<1",
"pyzipper>=0.3.6,<1",
"python-multipart>=0.0.31,<1",
@@ -6,6 +6,8 @@ from pathlib import Path
from typing import Any
from urllib.parse import urlsplit
from sqlalchemy import func, select
from govoplan_core.core.configuration_packages import (
ConfigurationApplyResult,
ConfigurationDiagnostic,
@@ -21,19 +23,30 @@ from govoplan_core.core.configuration_packages import (
from govoplan_core.core.infrastructure_capabilities import (
InfrastructureCapability,
InfrastructureCapabilityReceipt,
InfrastructureDependency,
InfrastructureDependencyProvider,
)
from govoplan_core.db.session import get_database
from govoplan_files.backend.db.models import FileBlob
from govoplan_files.backend.runtime import settings as runtime_settings
FILES_CONFIGURATION_CAPABILITY = "files.configuration"
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY = (
"infrastructure.dependency_inventory.files"
)
MANAGED_STORAGE_FRAGMENT = "managed_storage"
_PAYLOAD_KEYS = frozenset(
{"capability_id", "expected_backend", "expected_source"}
)
class FilesConfigurationProvider(ConfigurationProvider):
class FilesConfigurationProvider(
ConfigurationProvider,
InfrastructureDependencyProvider,
):
module_id = "files"
capability_ids = ("files.storage",)
def __init__(
self,
@@ -166,6 +179,61 @@ class FilesConfigurationProvider(ConfigurationProvider):
item for item in import_result.diagnostics if item.severity == "blocker"
)
def infrastructure_dependencies(self) -> tuple[InfrastructureDependency, ...]:
active_backend = _normalized_backend(
getattr(self._settings, "file_storage_backend", "local")
)
dependencies = [
InfrastructureDependency(
capability_id="files.storage",
module_id="files",
dependency_type="runtime_storage_binding",
dependency_ref=f"files-storage:{active_backend}",
state="runtime_binding",
scope="system",
summary=(
"The active Files runtime is bound to this deployment storage backend."
),
metrics={},
required_action=(
"Provision and verify the replacement backend before rebinding the Files runtime."
),
)
]
with get_database().session() as session:
rows = session.execute(
select(
FileBlob.storage_backend,
func.count(FileBlob.id),
func.coalesce(func.sum(FileBlob.size_bytes), 0),
)
.group_by(FileBlob.storage_backend)
.order_by(FileBlob.storage_backend)
)
for backend, blob_count, size_bytes in rows:
normalized_backend = _normalized_backend(str(backend or "local"))
dependencies.append(
InfrastructureDependency(
capability_id="files.storage",
module_id="files",
dependency_type="stored_blob_set",
dependency_ref=f"file-blobs:{normalized_backend}",
state="data_present",
scope="all-tenants",
summary=(
"Persisted Files blob metadata references content in this storage backend."
),
metrics={
"blob_count": int(blob_count or 0),
"content_bytes": int(size_bytes or 0),
},
required_action=(
"Copy and checksum-verify every referenced blob, switch the runtime binding, and retain rollback evidence before removing or replacing storage."
),
)
)
return tuple(dependencies)
def _binding_diagnostics(
self,
fragment: ConfigurationPackageFragment,
@@ -0,0 +1,132 @@
from __future__ import annotations
from dataclasses import replace
from typing import Iterable
from govoplan_core.core.modules import DocumentationTopic
_TRANSLATIONS = {
"files.tabular-content": {
"title": "Verwaltete CSV- und XLSX-Versionen als gesteuerte Datenquellen verwenden",
"summary": "Exakte autorisierte Dateiversionen für Connectors bereitstellen, ohne Files-Kontrollen zu umgehen.",
"body": (
"Files listet nur CSV- und XLSX-Dateien auf, die für die aktuelle Person im Mandanten sichtbar sind. Das Öffnen erfordert zusätzlich die Download-Berechtigung und eine exakte unveränderliche Versionsreferenz. Größenobergrenzen werden vor dem Speicherzugriff geprüft; Prüfsumme, Quarantäne, Verschlüsselungshülle, Löschung, Eigentum und Freigaben bleiben in Files maßgeblich. Connectors erhält Metadaten und verifizierte Bytes über die Core-Fähigkeit und importiert weder Files-Modelle noch Speicherschlüssel oder Verschlüsselungsinterna. Eine neuere aktuelle Version ersetzt eine gebundene Quellversion nie still; Connectors meldet die Änderung zur ausdrücklichen Prüfung."
),
},
"files.records.exact-version-source": {
"title": "Eine exakte Dateiversion in einer eAkte ablegen",
"summary": "Records nach aktueller Zugriffs- und Integritätsprüfung eine unveränderliche Files-Versionsreferenz übergeben.",
"body": (
"Wenn Records aktiv ist, stellt Files exakte Identitäten verwalteter FileVersion-Objekte über den anbieterneutralen Aktenquellenvertrag bereit. Die Ablage prüft aktiven Mandanten, aktuelle Files-Berechtigung, Eigentums- oder Freigabezugriff, unveränderliche Versionsidentität und Integrität des verwalteten Blobs. Records erhält Dateiname, Pfad, Version, Digest, Medientyp, Größe, Schutzstatus und Aufruflink; Files bleibt Eigentümer der Bytes."
),
},
"files.search.managed-content": {
"title": "Verwaltete Dateien und Ordner durchsuchen",
"summary": "Dateinamen, logische Pfade und Beschreibungen für die berechtigungsbewusste Plattformsuche bereitstellen.",
"body": (
"Wenn Search installiert ist, trägt Files verwaltete Dateien und Ordner zu dessen abgeleitetem Index bei. Jedes Ergebnis ist auf den Mandanten begrenzt und prüft vor der Ausgabe aktuelles Eigentum, Gruppenmitgliedschaft, direkte Freigaben, Ablauf, Widerruf, Löschung und Files-Berechtigungen erneut. Bestätigte Datei- und Freigabeänderungen werden über die Plattform-Event-Outbox übertragen; eine Administration kann den abgeleiteten Index neu aufbauen, ohne führende Files-Daten zu verändern."
),
},
"files.forms-runtime.managed-evidence": {
"title": "Formularanhänge als verwaltete Nachweise erfassen",
"summary": "Kurzlebige Upload-Freigaben ausstellen und verwaltete Dateiversionen an genau eine Formulareinreichung binden.",
"body": (
"Fordert Forms Runtime einen Dokumentnachweis an, stellt Files einer aktiven, mandantengleichen verwahrenden Person eine zweckgebundene Freigabe aus. Das Bearer-Token wird einmal angezeigt, nur als SHA-256-Digest gespeichert, läuft spätestens nach 15 Minuten ab und kann genau eine verwaltete Dateiversion erzeugen. Bei der endgültigen Einreichung werden exaktes Formular, Freigabe, Asset, Version, Prüfsumme, Löschstatus und Files-Integritätsgrenze erneut geprüft. Öffentliche Erfassung erhält dadurch keine allgemeinen Files-Berechtigungen; ein Dokument für eine Einreichung kann nicht still für eine andere wiederverwendet werden."
),
},
"files.workflow.organize-managed-files": {
"title": "Verwaltete Dateien und Ordner organisieren",
"summary": "Ordner anlegen und zugängliche Inhalte mit ausdrücklicher Konfliktbehandlung umbenennen, verschieben oder kopieren.",
"body": (
"Die Organisation bleibt in gesteuerten persönlichen oder Gruppenbereichen. Verschieben erhält die Asset-Identität; Kopieren erzeugt neue Assets und Versionen, die unveränderliche Blob-Bytes wiederverwenden. Jeder Zielkonflikt muss ausdrücklich abgelehnt, durch Umbenennen gelöst, überschrieben oder übersprungen werden."
),
},
"files.workflow.find-and-download-files": {
"title": "Verwaltete Dateien finden und herunterladen",
"summary": "Zugängliche Inhalte suchen und eine aktuelle Dateiversion oder ein ZIP einer Auswahl herunterladen.",
"body": (
"Files kann nach logischem Pfad oder Namensmuster sortiert und durchsucht werden. Ein Download verwendet immer die zugängliche aktuelle Version; aus einer Mehrfachauswahl kann ein temporäres ZIP-Archiv erzeugt werden. Einen eigenen Dienst zur Inhaltsvorschau gibt es derzeit nicht."
),
},
"files.workflow.share-managed-files": {
"title": "Zugriff auf verwaltete Dateien steuern",
"summary": "Direkte Lese-, Schreib- und Verwaltungsfreigaben auflisten, vergeben, ändern, befristen oder widerrufen, ohne das Eigentum zu ändern.",
"body": (
"Dateieigentümer und Files-Administrationen verwalten direkte Freigaben für Personen, Gruppen, den Mandanten und Campaign. Abgelaufene oder widerrufene Freigaben berechtigen sofort nicht mehr; unabhängige aktive Freigaben bleiben wirksam. Der Freigabedialog zeigt aktive und historische Einträge, und ein Widerruf ist idempotent. Die Zugriffserklärung prüft standardmäßig die angemeldete Person. Erlaubt Policy ausgewählte Fremdprüfungen, kann derselbe zentrale Dialog eine andere aktive Person im Mandanten bewerten; diese Diagnose wird im Audit protokolliert und gewährt keinen Zugriff."
),
},
"files.workflow.delete-managed-files": {
"title": "Verwaltete Dateien und Ordner löschen",
"summary": "Zugängliche Dateien oder einen Ordnerbaum nach aktueller Richtlinie weich löschen.",
"body": (
"Löschen blendet ausgewählte verwaltete Assets aus, statt gespeicherte Nachweise physisch zu vernichten. Ordner werden standardmäßig einschließlich ihrer Unterordner und Dateien rekursiv gelöscht; eine nicht rekursive Anforderung scheitert bei einem nicht leeren Ordner. Eine autorisierte Wiederherstellung aktiviert dasselbe Asset mit Versionen, Provenienz, Ordnerbaum oder Connector-Bereich erneut, sofern Pfad oder Bezeichnung frei ist. Die unwiderrufliche Vernichtung ist ein eigener Administrationsablauf mit Aufbewahrungs- und Legal-Hold-Prüfungen."
),
},
"files.workflow.restore-retain-and-purge": {
"title": "Verwaltete Dateien wiederherstellen, aufbewahren und vernichten",
"summary": "Weich gelöschte Ressourcen wiederherstellen und irreversible Löschung mit Aufbewahrung, Legal Hold, Vorschau, Freigabe und Recovery-Nachweis steuern.",
"body": (
"Wiederherstellen erhält ursprüngliche Asset-Identität, Versionen, Blob-Referenzen und Connector-Provenienz. Eine Aufbewahrungsadministration kann mit Begründung und Revisionsprüfung ein Aufbewahrungsdatum oder Legal Hold setzen. Eine physische Vernichtung benötigt eigene Berechtigung, aktuellen unveränderlichen Vorschau-Hash, Idempotenzschlüssel, die wörtliche Bestätigung PURGE und eine Freigabereferenz. Aktive Aufbewahrung, Legal Hold, Freigaben sowie Campaign- und Form-Nachweise blockieren sie. Zuerst werden berechtigte Datenbankeinträge entfernt und Blobs freigegeben; eine getrennte begrenzte Garbage Collection prüft unter derselben verteilten Blob-Sperre wie Uploads alle FileVersion-Referenzen erneut, bevor Bytes und Metadaten gelöscht werden. Jede irreversible Aktion erscheint im Core-Recovery-Ledger und Audit-Nachweis."
),
},
"files.privacy.data-subject-requests": {
"title": "Files-Daten in einer Datenschutzanfrage prüfen",
"summary": "Sichere Files-Metadaten ermitteln und Entscheidungen über Aufbewahrung, Nachweise und Byte-Löschung ausdrücklich behandeln.",
"body": (
"Der Files-DSAR-Anbieter sucht nur im wirksamen Mandanten und verlangt eine direkte Mitgliedschaft oder namensraumbezogene Files-Benutzerreferenz. Er exportiert begrenzte Datei-, Versions-, Ordner-, Freigabe-, Nachweis-, Connector-Konfigurations- und Integritätsmetadaten ohne Dateibytes, Speicherorte, Tokens, Passwörter, Geheimnisreferenzen oder verschlüsselte Zugangsdaten. Pläne dürfen eine aktive Freigabe für die betroffene Person widerrufen oder eine veränderbare Akteursreferenz lösen. Legal Hold, aktive Aufbewahrung, Form- und Campaign-Nachweise, Konfigurationshistorie und Integritätsnachweise bleiben mit Begründung erhalten. Dateiinhalte, Eigentum, Namen und Pfade benötigen manuelle Prüfung. Freigegebene physische Löschung muss den getrennt autorisierten Files-Vernichtungs- und Blob-Garbage-Collection-Ablauf nutzen, damit DSAR keine Nachweisblocker, Freigabe, Audit- oder Recovery-Kontrollen umgeht."
),
},
"files.reference.integrity-recovery-and-fail-closed-transports": {
"title": "Files-Integrität, Recovery und ausfallsichere Connector-Transporte betreiben",
"summary": "Datenbanknachweise, Blob-Chiffretexte und Encryption-Verwahrung als eine Recovery-Einheit sichern und jeden SDK-verwalteten Gegenpunkt binden.",
"body": (
"Dauerhafter lokaler Speicher ist die betriebliche Basis. Files wird aus einem abgestimmten Datenbank-/Blob-Snapshot mit passenden Encryption-Tabellen und ursprünglichem Deployment-Hauptschlüssel wiederhergestellt; anschließend ist der begrenzte fortsetzbare Integritätsscan auszuführen und geschützter sowie ungeschützter Zugriff stichprobenartig zu prüfen. Scan- und Befundaktionen benötigen die angezeigte Revision. Geschützte Scans prüfen erst den Chiffretext, dann nach Entschlüsselung den semantischen Klartextnachweis. Unter PostgreSQL sichern lease-gebundene Core-Recovery-Absichten Objektauswirkungen ab; Abweichungen werden quarantänisiert und bleiben in Ops sichtbar. SQLite verwendet für Entwicklung eine prozesslokale Sperre und ist kein Produktions-Recovery-Profil; nach hartem Prozessverlust ist ein Integritätsscan erforderlich. Fehlende oder abweichende Blobs werden quarantänisiert, verwaiste Objekte vor einer ausdrücklich autorisierten Bereinigung zunächst nur gemeldet. Physische Vernichtung und Blob-Garbage-Collection prüfen ihre irreversiblen Wirkungen getrennt. S3-Schreibvorgänge nutzen bedingte Effekte und digestbasierte Vorwärts-Recovery; S3- und SMB-Transporte binden Wiederholungen, Umleitungen, Aliasse und erkannte Endpunkte und wenden die Richtlinie für private Netze vor jeder Verbindung erneut an. Fehlt eine verifizierbare Transportnaht, wird geschlossen abgebrochen. Destruktive Modulstilllegung entfernt Datenbanktabellen, nicht jedoch Blob-Objekte im Backend."
),
},
"files.reference.snapshot-provenance-and-capabilities": {
"title": "Snapshot-Provenienz und Files-Fähigkeiten integrieren",
"summary": "Stabile Files-Fähigkeiten oder HTTP-Verträge verwenden und exakte Versionsnachweise bewahren, statt Files-Interna zu importieren.",
"body": (
"files.access erklärt Ressourcenzugriff; files.campaign_attachments friert Campaign-Eingaben auf exaktes Asset, Version, Blob, Prüfsumme und Quellrevision ein. Externe Inhalte müssen vor einer gesteuerten Nutzung importiert und ihre Provenienz auf abgeleiteten Snapshots erhalten werden. Zusammenarbeit, Provider-Synchronisierung, OAuth, externe Änderungen und fachlicher Workflow-Zustand bleiben in den jeweils verantwortlichen Modulen."
),
},
"files.assurance.process-and-release-readiness": {
"title": "Files-gestützte Prozesse und Releases absichern",
"summary": "Einen Prozess gegen die implementierte Files-Grenze prüfen, erlaubte und abgelehnte Pfade erproben und vor Freigabe Nachweise bewahren.",
"body": (
"Prozessverantwortliche müssen implementierte Kontrollen von geplanten Fähigkeiten unterscheiden. Ein Release ist erst bereit, wenn Paket- und Manifestversionen übereinstimmen und repräsentative Pfade für Autorisierung, Upload-Grenzen, Konflikte, Download, Löschung, Connectoren und Recovery erprobt wurden. Wiederherstellung, Aufbewahrung, Legal Hold, gesteuerte Vernichtung, Blob-Garbage-Collection, Freigabelebenszyklus und ausdrückliches S3-Zurückschreiben erzeugen eigene Policy-, Recovery- oder Audit-Nachweise. Gewöhnliche Organisationsänderungen stützen sich weiterhin vor allem auf die Files-Änderungssequenz statt auf ein eigenes kanonisches Audit-Ereignis. Verbleibende Grenzen sind in der Prozessbewertung zu dokumentieren und Nachweisarten dürfen nicht gleichgesetzt werden."
),
},
"files.reference.shared-storage-profile": {
"title": "Files mit einem gemeinsamen Speicherprofil betreiben",
"summary": "Lokalen, hostweit gemeinsamen oder S3-basierten Speicher passend zur Laufzeittopologie wählen.",
"body": (
"Core stellt das gemeinsame lokale/S3-Objektspeicher-Backend bereit; Files verantwortet Dateimetadaten und Objektschlüssel. Lokaler Speicher eignet sich für einen Laufzeitprozess, ein gemeinsames Host-Volume für Replikate auf demselben Host. Unabhängige Hosts benötigen einen ausdrücklich vertrauenswürdigen HTTPS-S3-kompatiblen Endpunkt. PostgreSQL, Objekte und Hauptschlüssel müssen auf denselben abgestimmten Recovery-Zeitpunkt wiederhergestellt werden."
),
},
"files.reference.generated-artifact-store": {
"title": "Erzeugte Modulausgaben speichern",
"summary": "Optionalen Erzeugermodulen erlauben, Ausgaben über die Autoritätsgrenze von Files dauerhaft abzulegen.",
"body": (
"Die Fähigkeit files.artifact_store nimmt erzeugte Bytes mit begrenzter, nicht geheimer Provenienz entgegen, wendet Upload-Autorisierung, Eigentums-, Pfad-, Versions- und Blob-Speicherregeln von Files an und liefert anbieterneutrale Datei-/Versionsreferenzen. Idempotenz beruht auf der Quellprovenienz. Die Annahme eines Artefakts beweist weder Druck, Versand noch eine andere externe Wirkung."
),
},
}
def localize_documentation_topics(
topics: Iterable[DocumentationTopic],
) -> tuple[DocumentationTopic, ...]:
localized: list[DocumentationTopic] = []
for topic in topics:
german = _TRANSLATIONS.get(topic.id)
if german is None:
localized.append(topic)
continue
translations = {
locale: dict(value) for locale, value in topic.translations.items()
}
translations["de"] = {**translations.get("de", {}), **german}
localized.append(replace(topic, translations=translations))
return tuple(localized)
@@ -0,0 +1,458 @@
"""German translations for public structured documentation metadata."""
from __future__ import annotations
from typing import Any
GERMAN_STRUCTURED_TRANSLATIONS: dict[str, dict[str, Any]] = {'files.assurance.process-and-release-readiness': {'outcome': 'Der Prozess oder die '
'Veröffentlichung hat eine explizite '
'Genehmigungsaufzeichnung, die an '
'abgestimmte Versionen, '
'repräsentative Nachweise, bekannte '
'Einschränkungen und eigene '
'Restrisiken gebunden ist.',
'prerequisites': ['Ein benannter '
'Prozessbesitzer hat die '
'beabsichtigten Benutzer, '
'Datenklassifizierung, '
'Aufbewahrungserwartungen und '
'Integrationen definiert.',
'Ein Kandidaten-Release wird '
'in einer sauberen Datenbank '
'und einer Upgrade-Kopie mit '
'ausgerichteten Python-, '
'Root-Paket-, WebUI-Paket- '
'und Modulmanifestversionen '
'installiert.',
'Repräsentative zugelassene '
'und verweigerte Konten, '
'begrenzte Testdateien und '
'ein koordiniertes '
'Datenbank-/Blob-/Key-Wiederherstellungsset '
'sind verfügbar.'],
'steps': ['Vergleichen Sie die '
'Prozessanforderungen mit der '
'implementierten / geplanten Grenze '
'des Handbuchs und notieren Sie jede '
'nicht unterstützte Anforderung oder '
'Kompensationskontrolle.',
'Bestätigen Sie die '
'Versionsausrichtung, wenden Sie '
'Migrationen auf bereinigte und '
'aktualisierte Datenbanken an und '
'führen Sie das Repository sowie '
'Meta-Repository-Sicherheits- und '
'Statikanalyse-Gates aus.',
'Übung erlaubt und verweigert '
'persönlichen, Gruppen und teilen Sie '
'den Zugriff mit repräsentativen '
'Konten.',
'Übung begrenzte Archivvorschau und '
'Bestätigung, jede erforderliche '
'Konfliktstrategie, Organisation, '
'Download und weiche Löschung.',
'Wenn Konnektoren konfiguriert sind, '
'überprüfen Sie die '
'Richtlinienerklärung und einen '
'angehängten HTTP-Provider; '
'überprüfen Sie die '
'Installateur-eigene Garage, wenn Sie '
'ausgewählt sind, und üben Sie dann '
'konfigurierte S3-Retries/Aliases und '
'SMB-Reconnect/Referral-Ziele unter '
'der Bereitstellungsrichtlinie für '
'das private Netzwerk aus, um zu '
'bestätigen, dass eine inkompatible '
'SDK-Transportnaht nicht geschlossen '
'ist.',
'Stellen Sie ein koordiniertes '
'Datenbank-/Blob-/Key-Backup wieder '
'her und vergleichen Sie '
'repräsentative heruntergeladene '
'Bytes mit den aufgezeichneten '
'SHA-256-Prüfsummen.',
'Notieren Sie die getesteten '
'Versionen, Ergebnisse, bekannte '
'Einschränkungen, Evidenzorte, '
'Restrisiken, Eigentümer und '
'Genehmigungsentscheidung.'],
'verification': 'Ein Überprüfer kann die '
'aufgezeichneten '
'Berechtigungs-/Verweigerungs-, '
'Integritäts-, Konnektor- und '
'Wiederherstellungsüberprüfungen '
'reproduzieren und jede nicht '
'erfüllte Anforderung auf eine '
'dokumentierte Einschränkung '
'oder akzeptierte '
'Kompensationskontrolle '
'zurückführen.'},
'files.forms-runtime.managed-evidence': {'limitations': ['Das erste Signaturprofil ist eine '
'authentifizierte Bestätigung; '
'fortgeschrittene und qualifizierte '
'elektronische Signaturen erfordern '
'einen separaten Anbieter.'],
'outcome': 'Der Entwurf verweist auf eine '
'unveränderliche verwaltete Dateiversion mit '
'genauen Prüfsummennachweisen.',
'prerequisites': ['Formulare Laufzeit und Dateien sind '
'aktiviert, und das Aufnahmeprofil hat '
'eine aktive Benutzerdepotbank für den '
'gleichen Mandant.'],
'steps': ['Fordern Sie eine Beweisbewilligung für '
'Dateien von der editierbaren Formularinstanz '
'an, laden Sie sie über die zurückgegebene '
'einmalige Trägerroute hoch und fügen Sie dann '
'die zurückgegebene Beweisreferenz zum Entwurf '
'hinzu.'],
'verification': 'Reichen Sie das Formular ein und '
'vergewissern Sie sich, dass geänderte, '
'gelöschte, unter Quarantäne gestellte, '
'mieterübergreifende und '
'gebührenübergreifende Referenzen '
'abgelehnt werden.'},
'files.governed-connectors-and-provenance': {'limitations': ['Die manuelle '
'Ordner-Synchronisierung plant keine '
'zukünftigen Runs oder mutiert, '
'löscht, umbenennt, verschiebt oder '
'ändert ACLs für entfernte Inhalte.',
'Ein erfolgreicher Teildurchlauf ist '
'keine vollständige Abdeckung, wenn '
'die Antwort verkürzt wird oder '
'Überprüfungsergebnisse enthält.'],
'outcome': 'Zulässige Konnektordateien werden zu '
'verwalteten Assets oder Versionen, '
'während jede Ausnahme explizit und '
'überprüfbar bleibt.',
'prerequisites': ['Ein sichtbarer aktiver '
'Verbindungsraum verwendet '
'manuelle Synchronisierung und '
'Links zu einem zugänglichen '
'verwalteten Benutzer- oder '
'Gruppenraum.',
'Der Betreiber hat '
'file:file:upload und die '
'effektive Konnektorrichtlinie '
'erlaubt die angeforderten Root- '
'und Source-Pfade.'],
'steps': ['Öffnen Sie den Ordner des verknüpften '
'Verbinders, wählen Sie den Ordner Sync '
'und überprüfen Sie das verwaltete Ziel.',
'Halten Sie den sicheren Sprung '
'standardmäßig oder wählen Sie explizit '
'umbenennen, ablehnen oder überschreiben '
'für nicht verwandte Zielkollisionen.',
'Legen Sie die Auswahl der begrenzten '
'Dateianzahl und des Unterordners fest, '
'führen Sie die Synchronisierung aus und '
'überprüfen Sie jedes eingegebene '
'Elementergebnis und jede '
'Abkürzungswarnung.',
'Setzen Sie einen verkürzten Lauf aus '
'einem schmaleren entfernten Ordner fort; '
'konfigurieren Sie die Planung separat, '
'wenn eine Hintergrundoperation '
'erforderlich ist.'],
'verification': 'Vergleichen Sie die '
'Antwortzusammenfassung und die '
'Ergebnisse pro Element mit dem '
'Auditereignis '
'files.connector.folder synced und '
'prüfen Sie dann die Herkunft der '
'erstellten, aktualisierten und '
'unveränderten repräsentativen '
'Assets.'},
'files.privacy.data-subject-requests': {'limitations': ['E-Mail-, Konto- oder '
'Identitätsselektoren allein können nicht '
'von Dateien aufgelöst werden, da Dateien '
'nicht im Besitz des '
'Access-Verzeichnisses sind; geben Sie '
'die bestätigte Mitgliedschaftsreferenz '
'an.',
'Der Anbieter bettet keine rohen '
'Dateiinhalte in den JSON-Export ein und '
'führt niemals eine physische '
'Blob-Löschung als DSAR-Nebenwirkung '
'durch.'],
'outcome': 'Dateieigene Betreffreferenzen werden '
'überprüft oder entfernt, ohne den '
'zurückgehaltenen Inhalt oder Nachweise '
'stillschweigend zu zerstören.',
'prerequisites': ['Die Anfrage wurde autorisiert und '
'enthält eine direkte '
'Mandantenmitgliedschaft oder eine '
'Betreffreferenz Dateien.',
'Der Datenschutzprüfer kann den '
'Zugriffsexport von der Löschbehörde '
'und der Löschbehörde für Dateien '
'unterscheiden.'],
'steps': ['Führen Sie die Anbietersuche aus und '
'bestätigen Sie die vollständige Abdeckung der '
'Dateiberichte anstelle eines ausgefallenen '
'oder abwesenden Anbieters.',
'Überprüfen Sie Datei-/Versionsmetadaten, '
'Gründe für die Aufbewahrung von Nachweisen und '
'den Quellpfad für die manuelle '
'Inhaltsüberprüfung.',
'Generieren Sie den Löschplan und führen Sie '
'nur die genehmigten reversiblen '
'Aktienrücknahme- oder Subjektreferenzaktionen '
'aus.',
'Für die genehmigte Byte-Löschung lösen Sie '
'jeden Lifecycle-Blocker auf und verwenden Sie '
'Files Purge Preview, Execution und Blob '
'Garbage Collection separat.'],
'verification': 'Bestätigen Sie, dass jeder '
'Dateidatensatz eine Aufbewahrungs-, '
'Überprüfungs-, Widerrufs- oder '
'Loslösungsdisposition hat und prüfen Sie '
'jede separate Bereinigung durch seine '
'Audit- und Wiederherstellungsnachweise.'},
'files.records.exact-version-source': {'outcome': 'Records behält eine exakt geregelte Referenz, '
'während Files Byte Authority bleibt.',
'prerequisites': ['Die genaue Dateiversion existiert und '
'übergibt das aktuelle Files Integrity '
'Gate.',
'Sie haben derzeit die Datei Lesezugriff '
'und Records Einreichung Behörde.'],
'steps': ['Wählen Sie die genaue verwaltete Dateiversion '
'aus, die zum institutionellen Datensatz gehört.',
'Wählen Sie den Zieldatensatz und geben Sie den '
'Zugangszweck und den Anmeldegrund an.',
'Bestätigen Sie die Einreichung; Dateien '
'überprüft den aktuellen Zugriff erneut und löst '
'die genaue Version auf.',
'Öffnen Sie die Datensatzchronologie und '
'überprüfen Sie die Versionsidentität und den '
'SHA-256 Digest.']},
'files.reference.integrity-recovery-and-fail-closed-transports': {'verification': 'Führen Sie '
'nach der '
'Wiederherstellung '
'einen '
'Checksummen-fähigen '
'Integritätsscan '
'aus, lösen Sie '
'jedes fehlende '
'/ korrupte '
'Finden, '
'genehmigen '
'oder behalten '
'Sie jedes '
'gemeldete '
'Waisenkind, '
'inspizieren '
'Sie die '
'Wiederherstellungsvorgänge '
'von Dateien in '
'Ops, '
'überprüfen Sie '
'den '
'autorisierten '
'und '
'verweigerten '
'Zugriff und '
'testen Sie '
'konfigurierte '
'HTTP-, S3- und '
'SMB-Verbindungen '
'mit ihrer '
'aufgezeichneten '
'Zieltopologie.'},
'files.reference.shared-storage-profile': {'limitations': ['Installer-verwaltete Garage ist '
'Single-Node, es sei denn, ein '
'externer Multi-Node-Cluster wird '
'separat betrieben.',
'Die Anwendung erstellt oder '
'verifiziert keine '
'PostgreSQL/Objekt/Schlüssel-Backups.'],
'verification': 'Führen Sie die '
'Dateispeicher-Round-Trip-Prüfung und '
'eine koordinierte '
'Wiederherstellungsübung gegen die '
'genaue Bereitstellungstopologie aus.'},
'files.tabular-content': {'consequence_classes': {'fail_closed': 'Nicht verfügbare, '
'überdimensionierte, unter '
'Quarantäne gestellte oder nicht '
'autorisierte Inhalte werden '
'nicht analysiert oder in der '
'Vorschau angezeigt.',
'pin_exact_version': 'Eine Quelle behält ihre '
'überprüfte '
'unveränderliche '
'Dateiversion, bis sie '
'explizit aktualisiert '
'wird.',
'preserve_file_controls': 'Zugriff auf Dateien, '
'Integrität, '
'Verschlüsselung, '
'Aufbewahrung und '
'rechtliche Kontrolle '
'bleiben '
'autoritativ.'}},
'files.workflow.delete-managed-files': {'limitations': ['Löschung ist eine weiche Löschung, keine '
'harte Säuberung.',
'Die Wiederherstellung schlägt fehl, wenn '
'eine aktive Datei, ein Ordner oder ein '
'Verbindungsraum bereits den Pfad oder '
'das Label verwendet.'],
'outcome': 'Der ausgewählte Inhalt wird aus aktiven '
'Dateiansichten unter dem aktuellen '
'Soft-Löschmodell ausgeblendet.',
'prerequisites': ['Sie können die ausgewählten '
'verwalteten Inhalte anzeigen und '
'löschen und haben Schreib- oder '
'Eigentümerzugriff auf jedes betroffene '
'Asset oder jeden Ordner.',
'Sie haben den gesamten Ordnerbaum beim '
'rekursiven Löschen überprüft.'],
'steps': ['Öffnen Sie Dateien und wählen Sie die Dateien '
'oder den Ordner zum Löschen aus.',
'Überprüfen Sie die Auswahl und, für einen '
'Ordner, alle Inhalte darunter.',
'Bestätigen Sie die Löschaktion.',
'Erfrischen oder öffnen Sie den Raum und '
'überprüfen Sie, ob die ausgewählten Pfade '
'nicht mehr aktiv sind.'],
'verification': 'Bestätigen Sie, dass die gelöschten '
'Pfade nicht mehr im aktiven Raum '
'erscheinen; behandeln Sie die Aktion '
'nicht als physisches Löschen.'},
'files.workflow.find-and-download-files': {'outcome': 'Die autorisierten aktuellen Dateibytes '
'oder das generierte Archiv werden auf das '
'lokale Gerät heruntergeladen.',
'prerequisites': ['Sie können verwaltete Dateien im '
'entsprechenden Bereich anzeigen und '
'herunterladen.'],
'steps': ['Öffnen Sie Dateien und wählen Sie den '
'entsprechenden Personen- oder Gruppenraum '
'aus.',
'Navigieren Sie in Ordnern, sortieren Sie '
'die Liste oder verwenden Sie ein Pfad- / '
'Namensmuster, um den beabsichtigten Inhalt '
'zu finden.',
'Überprüfen Sie den angezeigten Besitzer, '
'Pfad, Größe, Prüfsumme und Versionsdetails.',
'Laden Sie eine aktuelle Dateiversion '
'herunter oder wählen Sie mehrere Dateien '
'aus und wählen Sie ZIP herunterladen.'],
'verification': 'Bestätigen Sie die heruntergeladenen '
'Namen und vergleichen Sie, wo '
'Integrität wichtig ist, die '
'Dateibytes mit der angezeigten '
'Prüfsumme.'},
'files.workflow.organize-managed-files': {'outcome': 'Der ausgewählte Inhalt hat den '
'beabsichtigten verwalteten Eigentümer und '
'logischen Pfad.',
'prerequisites': ['Sie können verwaltete Dateien '
'anzeigen und organisieren.',
'Sie haben Schreib- oder '
'Eigentümerzugriff auf alle von der '
'Operation verwendeten Quellelemente '
'und Zielbereiche; die globale '
'Organisationsberechtigung allein '
'gewährt keinen Ressourcenzugriff.'],
'steps': ['Öffnen Sie Dateien und wählen Sie den '
'persönlichen oder Gruppenraum zum '
'Organisieren aus.',
'Erstellen Sie die erforderlichen Zielordner '
'oder wählen Sie die Dateien und Ordner aus, '
'um sie umzubenennen, zu verschieben oder zu '
'kopieren.',
'Wählen Sie das Ziel aus und lösen Sie jeden '
'Zielkonflikt explizit.',
'Wenden Sie die Operation an und öffnen Sie '
'den Zielordner erneut.'],
'verification': 'Bestätigen Sie jeden resultierenden '
'Pfad und Besitzer; für eine Bewegung '
'bestätigen Sie auch, dass der alte '
'Pfad verschwunden ist, und für eine '
'Kopie bestätigen Sie, dass die Quelle '
'verbleibt.'},
'files.workflow.restore-retain-and-purge': {'limitations': ['Automatische zeitbasierte '
'Bereinigungsplanung ist nicht '
'enthalten; ein autorisierter '
'handelnde Person startet Vorschau, '
'Ausführung und Blob-Sammlung.',
'Der Verschlüsselungsumschlag eines '
'geschützten Blobs folgt der eigenen '
'Aufbewahrungs- und '
'Schlüssel-Verwahrungsrichtlinie des '
'Verschlüsselungsmoduls.'],
'outcome': 'Berechtigte Metadaten und nicht '
'referenzierte Bytes werden mit separat '
'überprüfbaren Richtlinien-, Audit- und '
'Wiederherstellungsnachweisen gelöscht.',
'prerequisites': ['Die handelnde Person hat die '
'eindeutige Wiederherstellungs-, '
'Aufbewahrungs- oder '
'Bereinigungsberechtigung, die für '
'die beabsichtigte Aktion '
'erforderlich ist.',
'Ein Bereinigungsziel ist bereits '
'sanft gelöscht und alle abhängigen '
'Nachweise oder Aktien wurden '
'rechtmäßig gelöst.'],
'steps': ['Festlegen oder Überprüfen von '
'Aufbewahrungs- und Haltezustand unter '
'Verwendung der angezeigten '
'Lifecycle-Revision.',
'Überprüfen Sie zum Wiederherstellen, ob '
'der ursprüngliche Pfad oder das '
'ursprüngliche Label frei ist, und stellen '
'Sie die gleiche Ressource wieder her.',
'Erstellen Sie zum Löschen eine begrenzte '
'Purge-Vorschau und lösen Sie jeden '
'benannten Blocker auf.',
'Führen Sie die passende Vorschau mit einem '
'idempotency-Schlüssel, PURGE-Bestätigung '
'und Genehmigungsreferenz aus.',
'Führen Sie eine begrenzte '
'Blob-Garbage-Sammlung aus und prüfen Sie '
'alle Wiederherstellungs- oder '
'Ergebnis-unbekannten Operationen in Ops.'],
'verification': 'Überprüfen Sie, ob die '
'Asset-/Versionszeilen fehlen, der '
'Blob bleibt referenziert, nicht '
'referenzierte Bytes fehlen nach GC '
'und jede Operation hat eine gültige '
'Wiederherstellungsnachweiskette.'},
'files.workflow.share-managed-files': {'limitations': ['Kampagnenziel-Zuschüsse werden '
'normalerweise durch die '
'Kampagnenintegration erstellt und nicht '
'manuell im Dateidialog ausgewählt.',
'Ein Benutzer kann den Zugriff über einen '
'anderen aktiven direkten Zuschuss oder '
'Besitzpfad behalten, nachdem eine Aktie '
'widerrufen wurde.',
'Ausgewählte benutzererklärungen erfordern '
'die richtlinienberechtigung und sind nur '
'diagnosen; sie geben sich nicht als '
'benutzer aus oder gewähren dateizugriff.'],
'outcome': 'Direkter Zugriff hat die angeforderte '
'Berechtigung und Lebensdauer, während das '
'verwaltete Asset seinen Eigentümer behält.',
'prerequisites': ['Sie haben die Freigabeberechtigung für '
'Dateien und besitzen die Datei oder '
'verwalten Dateiräume für den aktiven '
'Mandanten.',
'Das beabsichtigte Benutzer-, Gruppen-, '
'Mandant- oder Kampagnenziel existiert '
'und ist aktiv.'],
'steps': ['Wählen Sie eine verwaltete Datei aus, wählen '
'Sie Shares verwalten und überprüfen Sie die '
'effektiven und historischen direkten Zuschüsse.',
'Wählen Sie den beabsichtigten Benutzer, die '
'Gruppe oder den Mandanten aus und wählen Sie '
'den Zugriff lesen, schreiben oder verwalten '
'sowie einen optionalen Ablauf.',
'Geben oder aktualisieren Sie die Freigabe, ohne '
'den Dateibesitz zu ändern.',
'Widerrufen Sie einen Zuschuss, wenn er nicht '
'mehr benötigt wird; wiederholter Widerruf ist '
'ein No-Op.'],
'verification': 'Testen Sie einen beabsichtigten und einen '
'verweigerten Pfad, verfallen oder '
'widerrufen Sie die Gewährung und stellen '
'Sie sicher, dass nur noch unabhängige '
'Zugangspfade verbleiben.'}}
+51 -7
View File
@@ -1,5 +1,8 @@
from __future__ import annotations
from govoplan_core.core.modules import with_documentation_structured_translations
from govoplan_files.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS
from dataclasses import replace
from pathlib import Path
@@ -34,7 +37,9 @@ from govoplan_core.core.modules import (
ModuleManifest,
NavItem,
PermissionDefinition,
ProductAvailabilityExplanation,
ProductAreaContribution,
ProductSurfaceContribution,
QuickAccessTool,
RoleTemplate,
)
@@ -52,9 +57,13 @@ 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,
FILES_INFRASTRUCTURE_DEPENDENCY_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.german_documentation import (
localize_documentation_topics,
)
from govoplan_files.backend.dsar_provider import FILES_DSAR_CAPABILITY
from govoplan_files.backend.form_evidence import (
CAPABILITY_FORM_EVIDENCE_FILES,
@@ -452,7 +461,7 @@ def _dsar_provider(context: ModuleContext) -> object:
manifest = ModuleManifest(
id="files",
name="Files",
version="0.1.20",
version="0.1.25",
required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
@@ -599,6 +608,31 @@ manifest = ModuleManifest(
order=30,
),
),
product_surfaces=(
ProductSurfaceContribution(
id="records.files",
module_id="files",
label="i18n:govoplan-core.product_surface.files",
description="i18n:govoplan-core.product_surface.files_description",
icon="folder",
entry_path="/documents",
route_path="/files",
surface_ids=("files.nav.files", "files.route.files"),
presentations=("task", "reader"),
search_source_ids=("files.objects",),
help_context_ids=("files.list",),
documentation_topic_ids=("files.quick-access-and-product-area",),
required_any=("files:file:read",),
order=10,
unavailable=ProductAvailabilityExplanation(
reason="authorization",
title="i18n:govoplan-core.product_surface.unavailable",
description="i18n:govoplan-core.product_surface.unavailable_description",
resolution="i18n:govoplan-core.product_surface.unavailable_resolution",
responsible_role="i18n:govoplan-core.access_administrator",
),
),
),
quick_access_tools=(
QuickAccessTool(
id="files.recent",
@@ -617,7 +651,7 @@ manifest = ModuleManifest(
),
),
),
documentation=(
documentation=localize_documentation_topics((
DocumentationTopic(
id="files.tabular-content",
title="Use managed CSV and XLSX versions as governed data sources",
@@ -714,7 +748,8 @@ manifest = ModuleManifest(
"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."
"or treats a storage replacement as an implicit migration. Before files.storage changes, Files reports the active runtime binding plus persisted blob counts and byte totals per backend "
"through the non-secret Ops dependency inventory. Missing, stale, or incomplete inventory blocks host apply. Run Files integrity and Ops checks before and after deployment changes."
),
layer="configured",
documentation_types=("admin",),
@@ -746,7 +781,8 @@ manifest = ModuleManifest(
"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."
"und behandelt einen Speicherwechsel nicht als stillschweigende Migration. Vor einer Änderung von files.storage meldet Files die aktive Laufzeitbindung sowie gespeicherte Blob-Anzahlen und Byte-Summen je Backend "
"im nicht geheimen Ops-Abhängigkeitsinventar. Ein fehlendes, veraltetes oder unvollständiges Inventar blockiert die Host-Anwendung. Die Integritäts- und Ops-Prüfungen sind vor und nach der Bereitstellungsänderung auszuführen."
),
}
},
@@ -762,7 +798,8 @@ manifest = ModuleManifest(
title="Files in Records and documents and Quick Access",
summary="Use managed files in the Records and documents area and keep a compact file surface beside current work.",
body=(
"Files contributes its authorized workspace to Records and documents. When Quick Access is enabled, the owner-rendered "
"Files contributes its authorized workspace to the stable Files destination at /documents in Records and documents. "
"The owner route /files remains available through All available tools and as a compatible deep link. When Quick Access is enabled, the owner-rendered "
"selector shows at most seven recently changed files and can return one exact file-version reference to the current task. "
"Accounts with upload permission can launch the full Files upload dialog into a freshly reauthorized managed space. Opening "
"either path causes Files to re-run its own provider, space, folder, object, and scope checks; completion and cancellation are "
@@ -792,7 +829,8 @@ manifest = ModuleManifest(
"title": "Dateien in Akten und Dokumente sowie im Schnellzugriff",
"summary": "Verwaltete Dateien im Produktbereich Akten und Dokumente und optional neben der aktuellen Arbeit verwenden.",
"body": (
"Files ordnet seinen berechtigten Arbeitsbereich Akten und Dokumente zu. Ist der Schnellzugriff aktiviert, erscheint "
"Files ordnet seinen berechtigten Arbeitsbereich dem stabilen Produktziel Dateien unter /documents in Akten und Dokumente zu. "
"Der Eigentümerpfad /files bleibt unter Alle verfügbaren Werkzeuge und als kompatibler Direktlink erreichbar. Ist der Schnellzugriff aktiviert, erscheint "
"die vom Modul gerenderte Auswahl von höchstens sieben zuletzt geänderten berechtigten Dateien rechts neben der aktuellen "
"Seite und kann einen exakten Dateiversionsverweis zurückgeben. Mit Upload-Berechtigung lässt sich der vollständige "
"Upload-Dialog in einem erneut berechtigungsgeprüften verwalteten Bereich öffnen. Files prüft Anbieter, Bereich, Ordner, "
@@ -1750,7 +1788,7 @@ manifest = ModuleManifest(
related_modules=("templates", "campaigns", "reporting"),
metadata={"kind": "reference", "route": "/files"},
),
),
)),
documentation_providers=(documentation_topics,),
migration_spec=MigrationSpec(
module_id="files",
@@ -1778,6 +1816,7 @@ manifest = ModuleManifest(
),
capability_factories={
FILES_CONFIGURATION_CAPABILITY: _configuration_provider,
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY: _configuration_provider,
CAPABILITY_FILES_ACCESS: lambda context: __import__(
"govoplan_files.backend.capabilities", fromlist=["access_capability"]
).access_capability(context),
@@ -1876,5 +1915,10 @@ manifest = ModuleManifest(
)
manifest = with_documentation_structured_translations(
manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS
)
def get_manifest() -> ModuleManifest:
return manifest
+69
View File
@@ -1,8 +1,12 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
import tempfile
import unittest
from sqlalchemy import create_engine
from govoplan_core.core.configuration_packages import (
ConfigurationPackageFragment,
ConfigurationPreflightContext,
@@ -10,10 +14,14 @@ from govoplan_core.core.configuration_packages import (
from govoplan_core.core.infrastructure_capabilities import (
infrastructure_capability_receipt_from_mapping,
)
from govoplan_core.db.base import Base
from govoplan_core.db.session import configure_database, reset_database
from govoplan_files.backend.configuration_provider import (
FILES_CONFIGURATION_CAPABILITY,
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
FilesConfigurationProvider,
)
from govoplan_files.backend.db.models import FileBlob
from govoplan_files.backend.manifest import manifest
@@ -78,6 +86,67 @@ def _s3_settings(**overrides):
class FilesConfigurationProviderTests(unittest.TestCase):
def test_provider_is_registered(self) -> None:
self.assertIn(FILES_CONFIGURATION_CAPABILITY, manifest.capability_factories)
self.assertIn(
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
manifest.capability_factories,
)
def test_inventory_reports_runtime_binding_and_persisted_blob_aggregate(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-files-inventory-") as root:
database_path = Path(root) / "files.sqlite3"
engine = create_engine(f"sqlite:///{database_path}")
Base.metadata.create_all(engine, tables=(FileBlob.__table__,))
configure_database(
f"sqlite:///{database_path}",
engine=engine,
dispose_previous=True,
)
try:
with engine.begin() as connection:
connection.execute(
FileBlob.__table__.insert(),
[
{
"id": "blob-1",
"tenant_id": "tenant-1",
"storage_backend": "local",
"storage_key": "tenant-1/a",
"checksum_sha256": "a" * 64,
"size_bytes": 7,
"protection_discriminator": "plaintext",
"ref_count": 1,
"integrity_status": "unchecked",
},
{
"id": "blob-2",
"tenant_id": "tenant-1",
"storage_backend": "local",
"storage_key": "tenant-1/b",
"checksum_sha256": "b" * 64,
"size_bytes": 11,
"protection_discriminator": "plaintext",
"ref_count": 1,
"integrity_status": "unchecked",
},
],
)
provider = FilesConfigurationProvider(
settings=_local_settings(),
environment={},
)
dependencies = provider.infrastructure_dependencies()
finally:
reset_database()
engine.dispose()
self.assertEqual(
["runtime_storage_binding", "stored_blob_set"],
[item.dependency_type for item in dependencies],
)
blob_set = dependencies[1]
self.assertEqual(2, blob_set.metrics["blob_count"])
self.assertEqual(18, blob_set.metrics["content_bytes"])
def test_matching_local_storage_is_an_idempotent_noop(self) -> None:
provider = FilesConfigurationProvider(
+7
View File
@@ -0,0 +1,7 @@
from govoplan_files.backend.manifest import get_manifest
def test_static_documentation_has_complete_german_reference_copy() -> None:
for topic in get_manifest().documentation:
german = topic.translations.get("de", {})
assert all(german.get(field, "").strip() for field in ("title", "summary", "body")), topic.id
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/files-webui",
"version": "0.1.20",
"version": "0.1.25",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -28,7 +28,7 @@
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9",
"lucide-react": "^1.23.0",
"@govoplan/core-webui": "^0.1.18"
"@govoplan/core-webui": "^0.1.44"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
@@ -1061,7 +1061,7 @@ export default function FileConnectorSettingsPanel({
)}
<div className="file-connector-settings-section file-connector-policy-locks">
<ToggleSwitch label="i18n:govoplan-files.lower_connection_limits.4f9838bc" checked={policyDraft.allowLowerConnectionLimits} disabled={saving || !canWrite} onChange={(allowLowerConnectionLimits) => patchPolicyDraft({ allowLowerConnectionLimits })} />
<ToggleSwitch label="i18n:govoplan-files.lower_credential_limits.ec2b72bc" checked={policyDraft.allowLowerCredentialLimits} disabled={saving || !canWrite} onChange={(allowLowerCredentialLimits) => patchPolicyDraft({ allowLowerCredentialLimits })} />
<ToggleSwitch label="i18n:govoplan-files.lower_credential_limits.ec2b72bc" helpContextId="files.connector.policy" helpModuleId="files" checked={policyDraft.allowLowerCredentialLimits} disabled={saving || !canWrite} onChange={(allowLowerCredentialLimits) => patchPolicyDraft({ allowLowerCredentialLimits })} />
<ToggleSwitch label="i18n:govoplan-files.lower_provider_limits.5816270a" checked={policyDraft.allowLowerProviderLimits} disabled={saving || !canWrite} onChange={(allowLowerProviderLimits) => patchPolicyDraft({ allowLowerProviderLimits })} />
<ToggleSwitch label="i18n:govoplan-files.lower_path_limits.1abcccb1" checked={policyDraft.allowLowerPathLimits} disabled={saving || !canWrite} onChange={(allowLowerPathLimits) => patchPolicyDraft({ allowLowerPathLimits })} />
<ToggleSwitch label="i18n:govoplan-files.lower_endpoint_limits.3fd781d5" checked={policyDraft.allowLowerUrlLimits} disabled={saving || !canWrite} onChange={(allowLowerUrlLimits) => patchPolicyDraft({ allowLowerUrlLimits })} />
@@ -1101,14 +1101,14 @@ export default function FileConnectorSettingsPanel({
<p>Choose where this credential can be used and how it signs in.</p>
</header>
<FormGrid columns={2} collapseAt="standard" className="">
<FormField label="i18n:govoplan-files.credential_id.9432a6e1">
<FormField label="i18n:govoplan-files.credential_id.9432a6e1" helpContextId="files.connector.credentials" helpModuleId="files">
<input className={!editingCredentialId && !credentialDraft.id.trim() ? "field-input-missing" : undefined} aria-invalid={!editingCredentialId && !credentialDraft.id.trim() || undefined} value={credentialDraft.id} disabled={Boolean(editingCredentialId) || saving} onChange={(event) => patchCredentialDraft({ id: event.target.value })} placeholder={`${scopeType}-webdav-credentials`} />
</FormField>
<FormField label="i18n:govoplan-files.label.74341e3c">
<input className={!credentialDraft.label.trim() ? "field-input-missing" : undefined} aria-invalid={!credentialDraft.label.trim() || undefined} value={credentialDraft.label} disabled={saving} onChange={(event) => patchCredentialDraft({ label: event.target.value })} placeholder="i18n:govoplan-files.govoplan_webdav_credentials.bc696d69" />
</FormField>
{credentialAttachProfileId &&
<FormField label="i18n:govoplan-files.connection_credential.178babe0">
<FormField label="i18n:govoplan-files.connection_credential.178babe0" helpContextId="files.connector.credentials" helpModuleId="files">
<input value={credentialAttachedProfile ? `${credentialAttachedProfile.label} (${credentialAttachedProfile.id})` : credentialAttachProfileId} disabled readOnly />
</FormField>
}
@@ -1130,8 +1130,8 @@ export default function FileConnectorSettingsPanel({
/>
</FormField>
</div>
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION}>
<select value={credentialDraft.credentialMode} disabled={saving} onChange={(event) => patchCredentialDraft({ credentialMode: event.target.value as CredentialMode })}>
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION} helpContextId="files.connector.credentials" helpModuleId="files">
<select data-help-context-id="files.connector.credentials" data-help-module-id="files" value={credentialDraft.credentialMode} disabled={saving} onChange={(event) => patchCredentialDraft({ credentialMode: event.target.value as CredentialMode })}>
<option value="none">i18n:govoplan-files.none.6eef6648</option>
<option value="anonymous">i18n:govoplan-files.anonymous.9bed5104</option>
<option value="basic">i18n:govoplan-files.username_password.e8ba8896</option>
@@ -1142,8 +1142,8 @@ export default function FileConnectorSettingsPanel({
</FormGrid>
<div className="file-connector-settings-section">
<ToggleSwitch label="i18n:govoplan-files.enabled.df174a3f" checked={credentialDraft.enabled} disabled={saving} onChange={(enabled) => patchCredentialDraft({ enabled })} />
{editingCredentialId && <ToggleSwitch label="i18n:govoplan-files.clear_saved_password.7442260d" checked={credentialDraft.clearPassword} disabled={saving} onChange={(clearPassword) => patchCredentialDraft({ clearPassword })} />}
{editingCredentialId && <ToggleSwitch label="i18n:govoplan-files.clear_saved_token.a9c670aa" checked={credentialDraft.clearToken} disabled={saving} onChange={(clearToken) => patchCredentialDraft({ clearToken })} />}
{editingCredentialId && <ToggleSwitch label="i18n:govoplan-files.clear_saved_password.7442260d" helpContextId="files.connector.credentials" helpModuleId="files" checked={credentialDraft.clearPassword} disabled={saving} onChange={(clearPassword) => patchCredentialDraft({ clearPassword })} />}
{editingCredentialId && <ToggleSwitch label="i18n:govoplan-files.clear_saved_token.a9c670aa" helpContextId="files.connector.credentials" helpModuleId="files" checked={credentialDraft.clearToken} disabled={saving} onChange={(clearToken) => patchCredentialDraft({ clearToken })} />}
</div>
</section>
@@ -1179,8 +1179,8 @@ export default function FileConnectorSettingsPanel({
disabled={saving}
showPassword={false} />
<FormGrid columns={2} collapseAt="standard" className="">
<FormField label="i18n:govoplan-files.secret_reference.04ed2221">
<input value={credentialDraft.secretRef} disabled={saving} onChange={(event) => patchCredentialDraft({ secretRef: event.target.value })} />
<FormField label="i18n:govoplan-files.secret_reference.04ed2221" helpContextId="files.connector.credentials" helpModuleId="files">
<input data-help-context-id="files.connector.credentials" data-help-module-id="files" value={credentialDraft.secretRef} disabled={saving} onChange={(event) => patchCredentialDraft({ secretRef: event.target.value })} />
</FormField>
</FormGrid>
</>
@@ -1304,8 +1304,8 @@ export default function FileConnectorSettingsPanel({
<FormField label="i18n:govoplan-files.base_path.6a4867ca">
<input value={draft.basePath} disabled={saving} onChange={(event) => patchDraft({ basePath: event.target.value })} placeholder="GovOPlaN" />
</FormField>
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION}>
<select value={draft.credentialMode} disabled={saving} onChange={(event) => patchDraft({ credentialMode: event.target.value as CredentialMode })}>
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION} helpContextId="files.connector.credentials" helpModuleId="files">
<select data-help-context-id="files.connector.credentials" data-help-module-id="files" value={draft.credentialMode} disabled={saving} onChange={(event) => patchDraft({ credentialMode: event.target.value as CredentialMode })}>
<option value="none">i18n:govoplan-files.none.6eef6648</option>
<option value="anonymous">i18n:govoplan-files.anonymous.9bed5104</option>
<option value="basic">i18n:govoplan-files.username_password.e8ba8896</option>
@@ -1313,8 +1313,8 @@ export default function FileConnectorSettingsPanel({
<option value="secret_ref">i18n:govoplan-files.secret_reference.04ed2221</option>
</select>
</FormField>
<FormField label="i18n:govoplan-files.credential.8bede3ea">
<select value={profileCredentialSelectValue} disabled={saving || profileCredentialOptions.length === 0} onChange={(event) => patchDraft({ credentialProfileId: event.target.value })}>
<FormField label="i18n:govoplan-files.credential.8bede3ea" helpContextId="files.connector.credentials" helpModuleId="files">
<select data-help-context-id="files.connector.credentials" data-help-module-id="files" value={profileCredentialSelectValue} disabled={saving || profileCredentialOptions.length === 0} onChange={(event) => patchDraft({ credentialProfileId: event.target.value })}>
{profileCredentialOptions.length === 0 && <option value="">i18n:govoplan-files.no_saved_credential.30a5a951</option>}
{profileCredentialOptions.map((credential) => <option key={credential.id} value={credential.id}>{credential.label}</option>)}
</select>
+7 -1
View File
@@ -2293,7 +2293,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
<Button onClick={() => openTransferDialog("copy")} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><Copy size={16} aria-hidden="true" /> i18n:govoplan-files.copy.af74f7c5</Button>
{hasSelection && <Button onClick={openRenameDialog} disabled={Boolean(organizeBlocker)} disabledReason={organizeBlocker}>{selectedEntryCount === 1 ? "i18n:govoplan-files.rename.d3f4cb89" : "i18n:govoplan-files.bulk_rename.7dcaa624"}</Button>}
<Button onClick={() => accessExplainableTarget && void openAccessExplanation(accessExplainableTarget)} disabled={Boolean(accessExplanationBlocker)} disabledReason={accessExplanationBlocker}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</Button>
<Button variant="danger" onClick={() => void deleteSelected()} disabled={Boolean(deleteBlocker)} disabledReason={deleteBlocker}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>
<Button variant="danger" helpContextId="files.list" helpModuleId="files" onClick={() => void deleteSelected()} disabled={Boolean(deleteBlocker)} disabledReason={deleteBlocker}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>
{activeSpaceIsConnector &&
<Button
variant="danger"
@@ -2793,8 +2793,12 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
{archivePreview.requires_password &&
<FormField
label="Archive password"
helpContextId="files.list"
helpModuleId="files"
help="The password stays in this dialog and is sent only while inspecting or importing this archive.">
<PasswordField
helpContextId="files.list"
helpModuleId="files"
value={archivePassword}
onValueChange={setArchivePassword}
disabled={busy}
@@ -2857,6 +2861,8 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
</Button>
{archivePreview.requires_password &&
<Button
helpContextId="files.list"
helpModuleId="files"
onClick={() => activeDialogTarget && void loadArchivePreview(archiveFile, activeDialogTarget, { preserveSelection: true })}
disabled={busy || !archivePassword}>
<RefreshCw size={15} aria-hidden="true" /> Verify password
@@ -264,7 +264,7 @@ export function FileContextMenu({
<button type="button" role="menuitem" onClick={onMove} disabled={!hasSelection || !canOrganize}><MoveRight size={15} aria-hidden="true" /> i18n:govoplan-files.move.8a74a26e</button>
<button type="button" role="menuitem" onClick={onCopy} disabled={!hasSelection || !canOrganize}><Copy size={15} aria-hidden="true" /> i18n:govoplan-files.copy.92556c6d</button>
<button type="button" role="menuitem" onClick={onExplainAccess} disabled={!canExplainAccess}><KeyRound size={15} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</button>
{showDelete && <button type="button" role="menuitem" className="danger" onClick={onDelete} disabled={!canDelete}><Trash2 size={15} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</button>}
{showDelete && <button type="button" role="menuitem" className="danger" data-help-context-id="files.list" data-help-module-id="files" onClick={onDelete} disabled={!canDelete}><Trash2 size={15} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</button>}
</div>);
}
+3 -2
View File
@@ -8,6 +8,7 @@ import {
type PlatformWebModule,
type QuickAccessToolsUiCapability
} from "@govoplan/core-webui";
import { generatedTranslations as productSurfaceTranslations } from "@govoplan/core-webui/outcome-product-surface-translations";
import { FolderTree } from "./features/files/components/FileManagerComponents";
import FileConnectorSettingsPanel from "./features/files/FileConnectorSettingsPanel";
import ManagedFileChooser from "./features/files/components/ManagedFileChooser";
@@ -22,8 +23,8 @@ const FileIntegrityPanel = lazy(() => import("./features/files/FileIntegrityPane
const fileRead = ["files:file:read"];
const translations = {
en: generatedTranslations.en,
de: generatedTranslations.de
en: { ...generatedTranslations.en, ...productSurfaceTranslations.en },
de: { ...generatedTranslations.de, ...productSurfaceTranslations.de }
};
const fileDashboardWidgets: DashboardWidgetsUiCapability = {
widgets: [