docs(PROJ-21): Architekturplan Multi-Tenancy
Vollständiger Plan für Mandanten-Fähigkeit: Rollenmodell, DB-Schema, API-Änderungen, SMTP-Routing, Frontend, Migrations-Strategie und Umsetzungsphasen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -33,7 +33,8 @@
|
|||||||
| PROJ-18 | E-Mail Integritätsprüfung | Deployed | [PROJ-18](PROJ-18-integritaetspruefung.md) | 2026-03-14 |
|
| PROJ-18 | E-Mail Integritätsprüfung | Deployed | [PROJ-18](PROJ-18-integritaetspruefung.md) | 2026-03-14 |
|
||||||
| PROJ-19 | Mailpiler → archivmail Migrationstool | Deployed | [PROJ-19](PROJ-19-import-piler.md) | 2026-03-17 |
|
| PROJ-19 | Mailpiler → archivmail Migrationstool | Deployed | [PROJ-19](PROJ-19-import-piler.md) | 2026-03-17 |
|
||||||
| PROJ-20 | Nutzer-Löschung & E-Mail-Verbleib (GoBD-konform) | Deployed | [PROJ-20](PROJ-20-nutzer-loeschung.md) | 2026-03-17 |
|
| PROJ-20 | Nutzer-Löschung & E-Mail-Verbleib (GoBD-konform) | Deployed | [PROJ-20](PROJ-20-nutzer-loeschung.md) | 2026-03-17 |
|
||||||
|
| PROJ-21 | Multi-Mandanten-Fähigkeit (Multi-Tenancy) | Planned | [PROJ-21](PROJ-21-multi-tenancy.md) | 2026-03-17 |
|
||||||
|
|
||||||
<!-- Add features above this line -->
|
<!-- Add features above this line -->
|
||||||
|
|
||||||
## Next Available ID: PROJ-21
|
## Next Available ID: PROJ-22
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
---
|
||||||
|
id: PROJ-21
|
||||||
|
title: Multi-Mandanten-Fähigkeit (Multi-Tenancy)
|
||||||
|
status: Planned
|
||||||
|
created: 2026-03-17
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Das System soll mehrere Kunden (Mandanten/Tenants) auf einer einzigen Instanz betreiben können. Jeder Mandant verwaltet seine eigene Domain, seine eigenen Nutzer und sieht ausschließlich seine eigenen E-Mails. Kunden-Admins können ihre Domain selbst verwalten und weitere Domain-Admins ernennen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollenmodell (neu)
|
||||||
|
|
||||||
|
| Rolle | Bisherig | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `superadmin` | — (neu) | Systemweite Kontrolle: Mandanten anlegen/löschen, alle Daten einsehen, technische Konfiguration |
|
||||||
|
| `domain_admin` | `admin` (umbenannt/erweitert) | Verwaltet Nutzer, Domains und IMAP-Konten **innerhalb des eigenen Mandanten** |
|
||||||
|
| `auditor` | `auditor` | Lese-Zugriff auf Audit-Log und E-Mails des eigenen Mandanten |
|
||||||
|
| `user` | `user` | Sucht und liest E-Mails des eigenen Mandanten |
|
||||||
|
|
||||||
|
> Bisherige `admin`-Konten werden bei der Migration zu `domain_admin` umgestellt.
|
||||||
|
> Ein initiales `superadmin`-Konto wird beim ersten Start angelegt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Datenbankschema-Änderungen
|
||||||
|
|
||||||
|
### Neue Tabelle: `tenants`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE tenants (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) UNIQUE NOT NULL, -- Anzeigename (z.B. "Mustermann GmbH")
|
||||||
|
slug VARCHAR(100) UNIQUE NOT NULL, -- URL-sicherer Bezeichner (z.B. "mustermann")
|
||||||
|
active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tenant_domains (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
|
domain VARCHAR(255) UNIQUE NOT NULL, -- z.B. "mustermann.de"
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Änderungen an bestehenden Tabellen
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- users: Mandant zuweisen
|
||||||
|
ALTER TABLE users ADD COLUMN tenant_id BIGINT REFERENCES tenants(id);
|
||||||
|
-- superadmin: tenant_id = NULL (systemweit)
|
||||||
|
-- alle anderen: tenant_id REQUIRED
|
||||||
|
|
||||||
|
-- emails: Mandant zuweisen
|
||||||
|
ALTER TABLE emails ADD COLUMN tenant_id BIGINT REFERENCES tenants(id);
|
||||||
|
CREATE INDEX idx_emails_tenant ON emails(tenant_id);
|
||||||
|
|
||||||
|
-- imap_accounts: erbt Mandant über owner → user.tenant_id (kein direktes Feld nötig)
|
||||||
|
|
||||||
|
-- audit_log: Mandant-Kontext
|
||||||
|
ALTER TABLE audit_log ADD COLUMN tenant_id BIGINT REFERENCES tenants(id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Xapian-Index
|
||||||
|
|
||||||
|
Jeder Mandant bekommt ein **eigenes Xapian-Verzeichnis**:
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/archivmail/xapian/
|
||||||
|
tenant-1/
|
||||||
|
tenant-2/
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Begründung: Vollständige Datenisolation, kein Risiko von Datenlecks durch falsche Filterung, einfaches Löschen eines Mandanten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Komponenten-Änderungen
|
||||||
|
|
||||||
|
### 1. `internal/userstore`
|
||||||
|
|
||||||
|
- Neues Feld `TenantID *int64` in `User`-Struct
|
||||||
|
- `Create`, `List`, `Delete` erhalten `tenantID`-Parameter
|
||||||
|
- Neue Methode: `ListByTenant(tenantID int64)`
|
||||||
|
- Neue Methode: `CountAdminsByTenant(tenantID int64)`
|
||||||
|
|
||||||
|
### 2. `internal/storage`
|
||||||
|
|
||||||
|
- `Save(raw, date, tenantID)` — speichert `tenant_id` in `emails`-Tabelle
|
||||||
|
- `Search`, `GetMail` etc. filtern immer nach `tenant_id`
|
||||||
|
- Physische Dateien bleiben im gemeinsamen Store (Dedup funktioniert weiter via SHA-256)
|
||||||
|
|
||||||
|
### 3. `internal/index`
|
||||||
|
|
||||||
|
- `New(path, batchSize, backend)` → `NewForTenant(basePath, tenantID, batchSize, backend)`
|
||||||
|
- Index-Manager verwaltet einen Index-Pool: `map[int64]Indexer`
|
||||||
|
- Lazy-Loading: Index wird beim ersten Zugriff geöffnet
|
||||||
|
|
||||||
|
### 4. `internal/smtpd`
|
||||||
|
|
||||||
|
- Nach `DATA`-Empfang: Empfänger-Domain aus `To:`-Header extrahieren
|
||||||
|
- Domain gegen `tenant_domains`-Tabelle auflösen → `tenant_id` bestimmen
|
||||||
|
- Falls keine Domain passt: konfigurierbare Fallback-Strategie (ablehnen oder default-Mandant)
|
||||||
|
|
||||||
|
### 5. `internal/api`
|
||||||
|
|
||||||
|
**Neue Middleware: `tenantFromSession`**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Jeder Request erhält den Mandant aus der Session injiziert.
|
||||||
|
// superadmin: kann per Header X-Tenant-ID einen Mandant imitieren.
|
||||||
|
func (s *Server) tenantMiddleware(next http.HandlerFunc) http.HandlerFunc
|
||||||
|
```
|
||||||
|
|
||||||
|
**Neue Routen (nur superadmin):**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/tenants Mandant anlegen
|
||||||
|
GET /api/tenants Alle Mandanten auflisten
|
||||||
|
GET /api/tenants/{id} Mandant-Details
|
||||||
|
PATCH /api/tenants/{id} Mandant bearbeiten (name, active)
|
||||||
|
DELETE /api/tenants/{id} Mandant löschen (mit allen Daten)
|
||||||
|
POST /api/tenants/{id}/domains Domain hinzufügen
|
||||||
|
DELETE /api/tenants/{id}/domains/{did} Domain entfernen
|
||||||
|
GET /api/tenants/{id}/users Nutzer eines Mandanten
|
||||||
|
```
|
||||||
|
|
||||||
|
**Bestehende Routen: tenant-aware**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/users → nur Nutzer des eigenen Mandanten (domain_admin)
|
||||||
|
POST /api/users → Nutzer im eigenen Mandanten anlegen (domain_admin)
|
||||||
|
GET /api/search → filtert automatisch nach tenant_id der Session
|
||||||
|
GET /api/mails/{id} → Zugriff nur wenn email.tenant_id == session.tenant_id
|
||||||
|
GET /api/audit → filtert nach tenant_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. `internal/auth` (JWT)
|
||||||
|
|
||||||
|
JWT-Claims erweitern:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sub": "username",
|
||||||
|
"role": "domain_admin",
|
||||||
|
"tenant_id": 3,
|
||||||
|
"tenant_slug": "mustermann",
|
||||||
|
"exp": 1234567890
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`superadmin`-Token hat `tenant_id: null`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zugriffsmatrix
|
||||||
|
|
||||||
|
| Aktion | superadmin | domain_admin | auditor | user |
|
||||||
|
|--------|:---:|:---:|:---:|:---:|
|
||||||
|
| Mandanten verwalten | ✓ | — | — | — |
|
||||||
|
| Eigene Nutzer verwalten | ✓ | ✓ | — | — |
|
||||||
|
| Eigene Domains verwalten | ✓ | ✓ | — | — |
|
||||||
|
| E-Mails lesen (eigener Mandant) | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| E-Mails anderer Mandanten | ✓ | — | — | — |
|
||||||
|
| Audit-Log (eigener Mandant) | ✓ | ✓ | ✓ | — |
|
||||||
|
| Audit-Log aller Mandanten | ✓ | — | — | — |
|
||||||
|
| Systemkonfiguration | ✓ | — | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SMTP-Routing (E-Mail-Eingang)
|
||||||
|
|
||||||
|
```
|
||||||
|
Eingehende E-Mail (RCPT TO: user@mustermann.de)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Domain-Lookup: SELECT tenant_id FROM tenant_domains WHERE domain = 'mustermann.de'
|
||||||
|
│
|
||||||
|
├─ Gefunden → tenant_id = 3 → Save(raw, tenant_id=3)
|
||||||
|
│
|
||||||
|
└─ Nicht gefunden → Fallback-Konfiguration:
|
||||||
|
"reject" → 550 No such domain
|
||||||
|
"default:<id>" → Default-Mandant (für Single-Tenant-Migration)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend-Änderungen
|
||||||
|
|
||||||
|
### Login-Seite
|
||||||
|
- Optional: Mandant-Auswahl (Dropdown oder automatisch per E-Mail-Domain)
|
||||||
|
- Kein Mandant-Feld nötig wenn Domain-Erkennung aktiviert
|
||||||
|
|
||||||
|
### Admin-Bereich (superadmin)
|
||||||
|
- Neue Tab-Seite `/admin/tenants`: Mandanten-Übersicht, anlegen, deaktivieren
|
||||||
|
- Pro Mandant: Domains, Nutzeranzahl, E-Mail-Anzahl, Letzter Import
|
||||||
|
|
||||||
|
### Admin-Bereich (domain_admin)
|
||||||
|
- Gleiche UI wie heute, aber nur eigene Nutzer/Domains sichtbar
|
||||||
|
- Kein Zugriff auf System-Tabs (Dienste, Systemstats)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrations-Strategie (bestehende Installation)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Migrations-Script ausführen:
|
||||||
|
- Tabellen tenant + tenant_domains anlegen
|
||||||
|
- Default-Mandant anlegen: name="default", slug="default"
|
||||||
|
- Alle bestehenden users → tenant_id = default-Mandant
|
||||||
|
- Alle bestehenden emails → tenant_id = default-Mandant
|
||||||
|
- Alle bestehenden admin-Nutzer → Rolle bleibt admin (= domain_admin des default-Mandanten)
|
||||||
|
- Einen superadmin-Nutzer anlegen (Passwort per CLI-Flag)
|
||||||
|
|
||||||
|
2. Xapian-Index verschieben:
|
||||||
|
- /var/archivmail/xapian/ → /var/archivmail/xapian/tenant-<id>/
|
||||||
|
|
||||||
|
3. config.yml erweitern:
|
||||||
|
- smtp.tenant_routing: "domain" | "default:<id>"
|
||||||
|
- default_tenant_id: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Umsetzungsreihenfolge (Phasen)
|
||||||
|
|
||||||
|
| Phase | Inhalt | Abhängigkeit |
|
||||||
|
|---|---|---|
|
||||||
|
| **Phase 1** | DB-Schema: `tenants`, `tenant_domains`, Migration | — |
|
||||||
|
| **Phase 2** | `userstore` + `storage` tenant-aware, JWT erweitern | Phase 1 |
|
||||||
|
| **Phase 3** | API-Middleware + alle bestehenden Handler tenant-gefiltert | Phase 2 |
|
||||||
|
| **Phase 4** | Xapian: pro-Tenant-Index, Index-Manager | Phase 2 |
|
||||||
|
| **Phase 5** | SMTP: Domain → Tenant-Routing | Phase 1 |
|
||||||
|
| **Phase 6** | Neue API-Routen (Tenant-Management) | Phase 3 |
|
||||||
|
| **Phase 7** | Frontend: superadmin-UI, domain_admin-Beschränkungen | Phase 6 |
|
||||||
|
| **Phase 8** | Migrations-Script + CLI-Befehl `archivmail migrate-tenants` | alle |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offene Entscheidungen (vor Implementierung klären)
|
||||||
|
|
||||||
|
1. **Login ohne Mandanten-Angabe**: Erkennung automatisch per E-Mail-Domain, oder Mandant-Auswahl im Login?
|
||||||
|
2. **E-Mail-Dedup über Mandanten hinweg**: Gleiche E-Mail bei zwei Mandanten — eine Datei oder zwei? (Empfehlung: eine Datei, zwei DB-Einträge mit unterschiedlicher `tenant_id`)
|
||||||
|
3. **Superadmin-Konto**: Eigener Nutzer ohne Mandant, oder Mandant "system"?
|
||||||
|
4. **Domain-Admin darf weitere Domain-Admins ernennen**: Ja (wie geplant) — aber nicht `superadmin`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
|
||||||
|
- [ ] Mandant A kann keine E-Mails von Mandant B sehen (auch nicht durch manipulierte API-Aufrufe)
|
||||||
|
- [ ] Domain-Admin kann Nutzer anlegen/bearbeiten, aber keine anderen Mandanten sehen
|
||||||
|
- [ ] Superadmin kann alle Mandanten einsehen und zwischen ihnen wechseln
|
||||||
|
- [ ] SMTP-Eingang ordnet E-Mails korrekt dem Mandanten per Empfänger-Domain zu
|
||||||
|
- [ ] Bestehende Single-Tenant-Installation lässt sich ohne Datenverlust migrieren
|
||||||
|
- [ ] Löschen eines Mandanten entfernt alle zugehörigen Daten (users, emails, imap, index)
|
||||||
Reference in New Issue
Block a user