Files
nexarch/mail/docs/ARC-06-PRUEFPROTOKOLL.md
sysops e19003b5d9 feat(mail): ARC-06 automatisierte Bucket-Provisionierung je Mandant
S3Driver (ARC-01) war strukturell bereits physisch getrennt: eine
Instanz kennt beim Konstruieren genau einen Bucketnamen, kein
Pfad-Präfix-Parameter, über den je ein anderes Bucket adressierbar
wäre. Was fehlte, war die automatisierte Provisionierung dieser
Trennung und der Nachweis dafür.

Neue Datei provision.go: BucketNameForTenant liefert den
deterministischen Bucketnamen je Mandant. ProvisionTenant legt in EINEM
Aufruf sowohl die Registry-Zeile in derselben tenants-Tabelle wie Core
TEN-01 (migrations/0001_tenant_registry.sql) als auch den Bucket an —
schlägt die Bucket-Anlage fehl, wird die Registry-Zeile automatisch
zurückgenommen, kein halb provisionierter Mandant. Core TEN-01 ist im
aktuellen Stand ein Grundgerüst ohne eigene aufrufbare
Provisionierungsfunktion — ProvisionTenant schreibt deshalb direkt über
den Registry-DSN in dieselbe Tabelle, dokumentiert im Prüfprotokoll.

Alle drei Pflichtprüfungen mit echten Nachweisen gegen eine reale
lokale MinIO-Instanz und Postgres durchgeführt: physische
Bucket-Trennung zweier Mandanten (ein in Mandant As Bucket
geschriebenes Objekt ist über Mandant Bs Driver nicht erreichbar, weil
es dort kein Objekt dieses Namens gibt, nicht weil ein Pfadfilter
greift); ein nie provisionierter Pseudo-Mandant scheitert auf
Bucket-Ebene (NoSuchBucket), bevor überhaupt eine Schlüsselsuche
stattfinden könnte; ein Provisionierungsaufruf legt Datenbank-Registry-
Zeile und Bucket nachweislich in einem Schritt an, inklusive
Rollback-Test bei fehlschlagender Bucket-Anlage.

go build/go vet/golangci-lint clean, gesamtes Mail-Modul
regressionsfrei getestet. Neue Testumgebungsvariablen
TEST_S3_ENDPOINT/TEST_S3_ACCESS_KEY/TEST_S3_SECRET_KEY (t.Skip ohne
sie, gleiche Konvention wie TEST_TENANT_DSN/TEST_MANTICORE_URL).
2026-09-01 14:06:23 +02:00

5.7 KiB

ARC-06 — Mandantentrennung im Objekt-Storage: Prüfprotokoll

Datum: 2026-09-01 Host: 192.168.1.131 (Build/Test/Lint), rsync + ssh Paket: mail/internal/storage (provision.go, neu) Testinfrastruktur: echte lokale MinIO-Instanz (http://localhost:9000, S3-kompatibel), echte lokale Postgres-Instanz (TEST_TENANT_DSN)

Umsetzung

S3Driver (ARC-01) war strukturell bereits physisch getrennt ausgelegt: eine S3Driver-Instanz kennt beim Konstruieren GENAU EINEN Bucketnamen (driver.go) und hat keinen Parameter/Pfad-Präfix, über den sie jemals ein anderes Bucket adressieren könnte — kein gemeinsamer Bucket mit Pfad-Präfix wie beim klassischen Cross-Tenant- Leck-Muster. Was fehlte, war die AUTOMATISIERTE PROVISIONIERUNG dieser Trennung (Akzeptanzkriterium 3) und der Nachweis (Pflichtprüfungen).

Neue Datei provision.go:

  • BucketNameForTenant(tenantSlug) — die eine Stelle, die den deterministischen Bucketnamen berechnet (nexarch-mail-<slug>).
  • NewS3AdminClient — S3-Client für Bucket-Verwaltungsoperationen (CreateBucket/HeadBucket), getrennt von S3Driver (das nur Objektoperationen innerhalb eines bereits bekannten Buckets kennt).
  • ProvisionTenant(ctx, registryPool, s3Admin, tenantSlug, tenantName, dbDSN) — legt in EINEM Aufruf sowohl die Registry-Zeile in derselben tenants-Tabelle wie Core TEN-01 (migrations/0001_tenant_registry.sql im Repository-Root) als auch den physisch getrennten Bucket an. Schlägt die Bucket-Anlage fehl, wird die Registry-Zeile automatisch zurückgenommen — kein halb provisionierter Mandant.

Abgrenzung zu Core TEN-01, dokumentiert: Core TEN-01 (in cmd/core/internal/db im Repository-Root) ist im aktuellen Stand ein Grundgerüst (Registry-Tabelle + Health-Endpunkt), enthält noch keine eigene, aufrufbare Tenant-Datenbank-Provisionierungsfunktion, an die sich diese Kachel technisch anhängen könnte. ProvisionTenant schreibt deshalb direkt in dieselbe, bereits durch TEN-01 definierte tenants-Tabelle (Postgres-DSN, kein Cross-Modul-Go-Import nötig, da beide Module ohnehin nur über den DSN kommunizieren) — sobald TEN-01 eine eigene Provisionierungsfunktion bekommt, ruft sie ProvisionTenant auf, statt dass Mail eine parallele Implementierung pflegt.

Pflichtprüfung 1: Test bestätigt physische Bucket-Trennung zweier Mandanten

TestProvisionTenant_CreatesPhysicallySeparateBuckets: zwei Mandanten provisioniert, unterschiedliche Bucketnamen bestätigt. Ein Objekt wird in Mandant As Bucket geschrieben; der Zugriff auf denselben Schlüssel über Mandant Bs S3Driver liefert ErrNotFound — nicht weil ein Pfadfilter greift, sondern weil es in Mandant Bs (physisch anderem) Bucket schlicht kein Objekt dieses Namens gibt. Kontrollzugriff über Mandant As eigenen Driver liefert den byteidentischen Inhalt zurück.

Ergebnis: BESTANDEN (echte MinIO-Instanz, reale S3-API-Aufrufe).

Pflichtprüfung 2: Simulierter Zugriffsversuch ohne Tenant-Kontext schlägt fehl, weil kein Bucket referenzierbar ist, nicht weil ein Pfadfilter greift

TestAccessWithoutTenantContext_FailsBecauseNoBucketReferenceable: HeadBucket auf den (nie provisionierten) Bucketnamen eines erfundenen Pseudo-Mandanten liefert einen echten S3-API-Fehler auf BUCKET-Ebene (NotFound/NoSuchBucket) — bevor überhaupt eine Schlüsselsuche innerhalb eines (in diesem Fall nicht existenten) Buckets stattfinden könnte. Das ist der strukturelle Beweis: es gibt keinen gemeinsamen Fallback-Bucket, in dem ein fehlender Tenant-Kontext auf einen falschen/fehlenden Pfad treffen würde — es gibt schlicht kein Bucket.

Ergebnis: BESTANDEN (echte MinIO-Instanz).

Pflichtprüfung 3: Provisionierungs-Test legt für einen neuen Mandanten Datenbank UND Bucket in einem Schritt an

TestProvisionTenant_CreatesRegistryRowAndBucketInOneStep: EIN Aufruf von ProvisionTenant — danach existiert sowohl die Registry-Zeile (SELECT ... FROM tenants WHERE slug = ... liefert den erwarteten db_dsn) als auch das Bucket (HeadBucket erfolgreich), real gegen Postgres und MinIO geprüft. Ergänzend TestProvisionTenant_RollsBackRegistryRowOnBucketFailure: bei fehlschlagender Bucket-Anlage (ungültiger Bucketname) bleibt KEINE verwaiste Registry-Zeile zurück — kein halb provisionierter Mandant.

Ergebnis: BESTANDEN (echte MinIO- und Postgres-Instanz, inkl. Fehlerpfad).

Akzeptanzkriterien

  1. Jeder Mandant hat einen eigenen, physisch getrennten Bucket/Pfad-Root: durch Pflichtprüfung 1 belegt.
  2. Ein Zugriffsversuch ohne oder mit falschem Tenant-Kontext kann technisch kein fremdes Bucket erreichen, nicht nur einen falschen Pfad: durch Pflichtprüfung 1+2 belegt (strukturell durch S3Drivers Design seit ARC-01, hier erstmals real nachgewiesen).
  3. Bucket-Provisionierung ist Teil desselben automatisierten Schritts wie die Tenant-Datenbank-Anlage, keine manuelle Zusatzaktion nötig: durch Pflichtprüfung 3 belegt — siehe auch Abschnitt "Umsetzung" zur Abgrenzung gegenüber Core TEN-01s aktuellem Ausbaustand.

Build/Vet/Lint/Test — Gesamtmodul

go build ./...    → OK
go vet ./...      → OK
golangci-lint run ./... → 0 issues
go test ./... -p 1 (TEST_TENANT_DSN, TEST_MANTICORE_URL, TEST_S3_ENDPOINT/TEST_S3_ACCESS_KEY/TEST_S3_SECRET_KEY gesetzt) → alle Pakete ok

Keine Regression in den bestehenden Paketen. Neue Umgebungsvariablen TEST_S3_ENDPOINT/TEST_S3_ACCESS_KEY/TEST_S3_SECRET_KEY — ohne sie werden die neuen Integrationstests übersprungen (t.Skip), gleiche Konvention wie TEST_TENANT_DSN/TEST_MANTICORE_URL.

Ergebnis

ARC-06 erfüllt alle Akzeptanzkriterien mit echten, ausgeführten Nachweisen gegen eine reale MinIO- und Postgres-Instanz. Freigeschaltet zusammen mit SRC-11: QA-04.