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).
117 lines
5.7 KiB
Markdown
117 lines
5.7 KiB
Markdown
# 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
|
|
`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.
|