diff --git a/features/INDEX.md b/features/INDEX.md index 6d48924..8447af8 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -33,7 +33,8 @@ | 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-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 | -## Next Available ID: PROJ-21 +## Next Available ID: PROJ-22 diff --git a/features/PROJ-21-multi-tenancy.md b/features/PROJ-21-multi-tenancy.md new file mode 100644 index 0000000..d02ba33 --- /dev/null +++ b/features/PROJ-21-multi-tenancy.md @@ -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:" → 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-/ + +3. config.yml erweitern: + - smtp.tenant_routing: "domain" | "default:" + - 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)