Files
nexarch/mail/docs/ARC-06-PRUEFPROTOKOLL.md
T
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

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.