# 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-`). - `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 `S3Driver`s 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.