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).
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user