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).
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 vonS3Driver(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 derselbentenants-Tabelle wie Core TEN-01 (migrations/0001_tenant_registry.sqlim 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
- Jeder Mandant hat einen eigenen, physisch getrennten Bucket/Pfad-Root: durch Pflichtprüfung 1 belegt.
- 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). - 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.