Frontend: Dienste-Tab (Systemdienste starten/stoppen) ist jetzt nur noch für superadmin sichtbar. domain_admin sieht: Dashboard, Benutzer, Audit-Log, Import, LDAP (eigener Mandant). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
id, title, status, created
| id | title | status | created |
|---|---|---|---|
| PROJ-21 | Multi-Mandanten-Fähigkeit (Multi-Tenancy) | Deployed | 2026-03-17 |
Phase 1 implementiert (2026-03-17)
internal/tenantstore/store.go— DB-Schema (tenants,tenant_domains,tenant_ldap); Migration:tenant_idzuusers+audit_log; CRUD + Domain-Management +GetByDomainfür SMTP-Routinginternal/api/ldap_tenants.go— 8 Tenant-API-Routen (LIST/CREATE/GET/PATCH/DELETE Tenants + Domain-Verwaltung); wired viaSetTenants()cmd/archivmail/main.go—tenantstore.New()+srv.SetTenants()src/lib/api.ts—Tenant,TenantDomain, alle 7 API-Funktionensrc/app/admin/page.tsx— Mandanten-Tab mit Tabelle, Domain-Dialog, Löschen-Bestätigung
Phase 2, 3, 5, 8 implementiert (2026-03-17)
Phase 2a — Rollen & userstore:
internal/userstore/userstore.go— Neue Rollendomain_admin,superadmin;User.TenantID *int64;Create()mit TenantID;ListByTenant();UpsertLDAPUser()mit tenantID-Parameter; Scan-Helpers aktualisiert
Phase 2b — storage.Save() + email_refs:
internal/storage/storage.go—Save()neue Signatur(ctx, raw, time, tenantID *int64); DB-Migrationen:emails.tenant_id, Tabelleemail_refs; neue MethodenGetTenantForMail(),GetAllIDsByTenant(),StatsByTenant();insertMeta/insertMetaMinimalmit tenantID
Phase 2c — JWT tenant-aware:
internal/auth/auth.go—Session.TenantID *int64; JWT-Claimstenant_id;ValidateToken()extrahiert tenant_id;HasRole()Hierarchie: superadmin > admin > domain_admin > auditor > user
Phase 3 — Tenant-Middleware:
internal/api/server.go—tenantMiddleware(),tenantFromCtx();auth()+authAdmin()Helper;handleListUserstenant-gefiltert;handleStorageStatsviaStatsByTenant();handleGetMail/Attachment/Rawmit Tenant-Isolation
Phase 5 — SMTP Domain-Routing:
internal/smtpd/smtpd.go—DomainToTenantFunc;Daemon.domainToTenant+defaultTenantID;SetDomainToTenant();resolveTenantFromRcpts()config/config.go—SMTPConfig.TenantRouting,SMTPConfig.DefaultTenantIDcmd/archivmail/main.go— Tenant-Routing-Verdrahtung;tenantStvor smtpDaemon initialisiert
Phase 8 — Migrations-Script:
cmd/archivmail/cmd_migrate_tenants.go— Subkommandomigrate-tenants: default-Tenant anlegen, Users/Emails/Audit assignen, email_refs seeden, admins → domain_admin, superadmin erstellen
Alle Save()-Aufrufer angepasst:
internal/imap/importer.go,internal/pop3/importer.go—TenantID *int64Feld +Save(ctx, ..., tenantID)cmd/archivmail/cmd_import.go,cmd/archivmail-import/main.go—Save(ctx, ..., nil)
Phase 4 implementiert + QA-approved (2026-03-18)
Phase 6+7 implementiert (2026-03-18):
- Tenant-Management-Routen (Phase 1 bereits enthalten)
/api/tenant/ldapRouten via PROJ-23- Frontend: Dienste-Tab nur für superadmin sichtbar
- Frontend: Security/Tenants/Modules/LDAP-Global nur für superadmin
- Frontend: domain_admin sieht nur eigene Nutzer/Audit/LDAP-Tab
Phase 4 -- QA-Bericht (2026-03-18, Code-Review)
Gesamtbewertung: APPROVED -- alle 5 Pruefpunkte bestanden
| # | Pruefpunkt | Ergebnis | Details |
|---|---|---|---|
| X1 | Pro-Mandant-Verzeichnis tenant-/ | PASS | tenant_manager.go:69 |
| X2 | ForTenant(nil) nil-safe | PASS | tenant_manager.go:47 |
| X3 | handleSearch nutzt tenant_id aus Session | PASS | server.go:627-633 |
| X4 | Thread-Safety (RWMutex + Channel) | PASS | tenant_manager.go:16,53-62 / tenant_worker.go:25 |
| X5 | Close() auf allen Indexen | PASS | tenant_manager.go:91-110, main.go:121 |
Dateien: internal/index/tenant_manager.go, internal/index/tenant_worker.go, internal/api/server.go, cmd/archivmail/main.go
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 zudomain_adminumgestellt. Ein initialessuperadmin-Konto wird beim ersten Start angelegt.
Datenbankschema-Änderungen
Neue Tabelle: tenants
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
-- 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);
Neue Tabelle: tenant_ldap (PROJ-16 Phase B)
Ermöglicht pro-Mandant-LDAP-Konfiguration durch Domain-Admins ohne config.yml-Zugriff.
Wird in Phase 1 als leere Tabelle angelegt; befüllt durch PROJ-16 Phase B.
CREATE TABLE tenant_ldap (
tenant_id BIGINT PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT false,
url TEXT NOT NULL DEFAULT '',
bind_dn TEXT NOT NULL DEFAULT '',
bind_password BYTEA, -- AES-256-GCM verschlüsselt
base_dn TEXT NOT NULL DEFAULT '',
user_filter TEXT NOT NULL DEFAULT '(sAMAccountName=%s)',
tls BOOLEAN NOT NULL DEFAULT false,
tls_skip_verify BOOLEAN NOT NULL DEFAULT false,
default_role VARCHAR(20) NOT NULL DEFAULT 'user',
group_mappings JSONB -- [{group_dn, role}, ...]
);
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 *int64inUser-Struct Create,List,DeleteerhaltentenantID-Parameter- Neue Methode:
ListByTenant(tenantID int64) - Neue Methode:
CountAdminsByTenant(tenantID int64)
2. internal/storage
Save(raw, date, tenantID)— speicherttenant_idinemails-TabelleSearch,GetMailetc. filtern immer nachtenant_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 ausTo:-Header extrahieren - Domain gegen
tenant_domains-Tabelle auflösen →tenant_idbestimmen - Falls keine Domain passt: konfigurierbare Fallback-Strategie (ablehnen oder default-Mandant)
5. internal/api
Neue Middleware: tenantFromSession
// 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:
{
"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 | ✓ | ✓ | — | — |
| LDAP konfigurieren (eigener Mandant) | ✓ | ✓ | — | — |
| LDAP konfigurieren (alle Mandanten) | ✓ | — | — | — |
| 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, tenant_ldap (leer), Migration |
— |
| Phase 2 | userstore + storage tenant-aware, JWT erweitern, UpsertLDAPUser + tenant_id |
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 + /api/tenant/ldap) |
Phase 3 |
| Phase 7 | Frontend: superadmin-UI, domain_admin-Beschränkungen, LDAP-Konfig-Tab | Phase 6 |
| Phase 8 | Migrations-Script + CLI-Befehl archivmail migrate-tenants |
alle |
PROJ-16 Abhängigkeit:
- PROJ-16 Phase A (Single-Tenant-LDAP via
config.yml) ist unabhängig von PROJ-21 umsetzbar.- PROJ-16 Phase B (Pro-Mandant-LDAP,
tenant_ldap-Tabelle, Domain-Admin-UI) erfordert PROJ-21 Phase 2+6 als Voraussetzung.
Offene Entscheidungen (vor Implementierung klären)
- Login ohne Mandanten-Angabe: Erkennung automatisch per E-Mail-Domain, oder Mandant-Auswahl im Login?
- 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) - Superadmin-Konto: Eigener Nutzer ohne Mandant, oder Mandant "system"?
- 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)