feat(PROJ-46): E-Mail als primärer Login-Identifier für Tenant-User

Tenant-User (tenant_id IS NOT NULL) melden sich künftig per E-Mail an statt
per Username — behebt Verwechslungen wie im Support-Fall vom 2026-06-13
(Login schlug trotz Passwort-Reset fehl, weil E-Mail statt Username
verwendet wurde). Nicht-Tenant-User (Superadmin/System) können weiterhin
Username ODER E-Mail nutzen.

Neue Store.VerifyLogin() prüft erst per E-Mail (alle User), fällt dann auf
Username zurück (nur tenant_id IS NULL). VerifyPassword() bleibt für den
IMAP-Server-Login-Pfad (PROJ-26) unverändert. Bewusster Breaking Change für
Tenant-User, Datenqualität vorab geprüft (0 Kollisionen).

Security-Nachtrag: bcrypt-Dummy-Compare im "user not found"-Pfad ergänzt,
um Timing-basierte Identifier-Enumeration zu verhindern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-07-03 23:37:20 +02:00
co-authored by Claude Sonnet 5
parent 804cd62201
commit 767373b206
18 changed files with 1710 additions and 14 deletions
+2 -2
View File
@@ -59,10 +59,10 @@
| PROJ-40 | Prometheus Metriken + Health-Check | Deployed | [PROJ-40](PROJ-40-prometheus-metriken.md) | 2026-04-05 |
| PROJ-41 | Dashboard Zeitreihe + Speicherprognose | Deployed | [PROJ-41](PROJ-41-dashboard-zeitreihe.md) | 2026-04-05 |
| PROJ-42 | Gespeicherte Suchanfragen | Deployed | [PROJ-42](PROJ-42-gespeicherte-suchanfragen.md) | 2026-04-05 |
| PROJ-43 | Automatische Archivierungsregeln | Planned | [PROJ-43](PROJ-43-archivierungsregeln.md) | 2026-04-05 |
| PROJ-43 | Automatische Archivierungsregeln | In Review | [PROJ-43](PROJ-43-archivierungsregeln.md) | 2026-04-05 |
| PROJ-44 | OCR-GUI-Integration (Status, Download, Such-Highlight) | Deployed | [PROJ-44](PROJ-44-ocr-gui-integration.md) | 2026-05-08 |
| PROJ-45 | IMAP Per-Folder UID-Tracking + UIDVALIDITY-Check | Deployed | [PROJ-45](PROJ-45-imap-folder-uid-tracking.md) | 2026-05-11 |
| PROJ-46 | E-Mail als primärer Login-Identifier für Tenant-User | Planned | [PROJ-46](PROJ-46-email-login-tenant-user.md) | 2026-06-13 |
| PROJ-46 | E-Mail als primärer Login-Identifier für Tenant-User | In Review | [PROJ-46](PROJ-46-email-login-tenant-user.md) | 2026-07-03 |
| PROJ-47 | Tenant-Voll-Export per CLI | Deployed | [PROJ-47](PROJ-47-tenant-voll-export-cli.md) | 2026-06-13 |
| PROJ-48 | Audit-Log Unveränderbarkeit (Nachbesserung PROJ-11) | Deployed | [PROJ-48](PROJ-48-audit-log-unveraenderbarkeit.md) | 2026-06-13 |
| PROJ-49 | Verschlüsselungspflicht at-rest (Healthcheck & Warnung) | Deployed | [PROJ-49](PROJ-49-verschluesselungspflicht.md) | 2026-06-13 |
+112
View File
@@ -0,0 +1,112 @@
---
id: PROJ-43
title: Automatische Archivierungsregeln (by Domain/Sender)
status: In Review
created: 2026-04-05
---
## Kontext
Das SMTP Domain-Routing (PROJ-21 Phase 5) ist bereits implementiert:
- `tenant_domains`-Tabelle ordnet Domains Mandanten zu
- `resolveTenantFromRcpts()` im SMTP-Daemon weist Mails automatisch zu
- IMAP/POP3-Importer unterstützen ebenfalls TenantID
PROJ-43 **erweitert** diese Basis um flexiblere Muster-Regeln und eine GUI.
## Ziel
Admins können über die Web-Oberfläche Regeln verwalten die über einfache Domain-Zuordnung
hinausgehen — z.B. Wildcard-Domains, Absender-Adressen, Betreff-Muster.
## User Stories
- Als Admin möchte ich alle Mails von @kunde.de automatisch dem Mandanten "Kunde GmbH" zuweisen
- Als Admin möchte ich Mails an archiv@firma.de einem bestimmten Tenant zuordnen
## Namenskonflikt (Nachtrag 2026-07-03)
PROJ-51 (Retention-Kategorien) hat bereits eine Tabelle `archiving_rules` angelegt
(`internal/storage/retention_rules.go`, Spalten `condition_type`, `pattern`,
`priority`, `retention_days`) — andere Bedeutung (Aufbewahrungsfrist statt
Tenant-Zuordnung). Um Kollision zu vermeiden, heißt die neue Tabelle für PROJ-43
**`tenant_routing_rules`**, nicht `archiving_rules` wie ursprünglich benannt.
## Acceptance Criteria
- [ ] Tabelle `tenant_routing_rules (id, tenant_id, match_type [from_domain|to_domain|from_addr|to_addr], pattern, priority, created_at)`
- [ ] SMTP-Daemon und IMAP-Import prüfen Regeln nach jedem eingehenden Mail
- [ ] API: CRUD für tenant_routing_rules (Admin only)
- [ ] Frontend: Regel-Verwaltung im Admin-Bereich
- [ ] Priorität: höhere Priorität gewinnt bei mehreren Treffern
- [ ] Dry-Run: zeigt welche bestehenden Mails von einer Regel betroffen wären
## Betroffene Dateien
- `internal/storage/storage.go` bzw. neues `internal/storage/tenant_routing_rules.go` (DB-Schema + ApplyRules-Methode, analog `retention_rules.go`)
- `internal/smtpd/smtpd.go` (Regel-Check nach Save, ergänzt bestehendes `resolveTenantFromRcpts()` aus PROJ-21 Phase 5)
- `internal/imap/importer.go` (Regel-Check nach Import)
- `internal/api/rules_handlers.go` (neu)
- `src/app/admin/` (Regel-UI)
## Implementierungsnotizen (2026-07-03, Status: In Review)
Kein lokaler `go build` möglich (kein Go-Toolchain im Arbeitsverzeichnis) — Build/Tests
laufen separat auf dem Testserver.
### Neue/geänderte Dateien
- `internal/storage/tenant_routing_rules.go` (NEU) — Tabelle `tenant_routing_rules`,
CRUD, Matching-Engine `ResolveTenantByRoutingRules()`, Dry-Run `DryRunRoutingRule()`.
Stil analog `retention_rules.go`. Match-Typen: `from_domain`, `to_domain`,
`from_addr`, `to_addr`. Wildcard-Domains via `*.kunde.de` (matcht Sub-Domains).
- `internal/storage/storage.go``initTenantRoutingRulesSchema(ctx)` in Init-Kette.
- `internal/smtpd/smtpd.go``resolveTenantByRules()` neu; in `resolveTenant()` als
Stufe 0 VOR der `tenant_domains`-Logik (Regeln sind expliziter → Vorrang).
- `internal/imap/importer.go``storeAndIndex()`: Mail wird jetzt früh geparst,
Regel-Match kann die Account-Default-TenantID vor dem Save überschreiben.
- `internal/api/rules_handlers.go` (NEU) — CRUD + Dry-Run, `authAdmin` (domain_admin+),
Tenant-Scoping + IDOR-Check (`tenantAccessAllowed`) bei jedem `{id}`.
- `internal/api/server.go` — Routen registriert.
### Tabelle
`tenant_routing_rules (id, tenant_id [NOT NULL, FK tenants ON DELETE CASCADE],
match_type, pattern, priority, created_at)`. Höhere `priority` gewinnt, bei
Gleichstand niedrigste `id`.
### API-Endpunkte (alle domain_admin+; superadmin = alle Tenants, domain_admin = eigener)
- `GET /api/admin/routing-rules``{ "rules": RoutingRule[] }`
- `POST /api/admin/routing-rules` Body `{tenant_id?, match_type, pattern, priority}`
`201 {"id": <int>}` (superadmin muss `tenant_id` setzen; domain_admin bekommt
eigenen Tenant erzwungen)
- `PUT /api/admin/routing-rules/{id}` gleicher Body → `200 {"ok": true}`
- `DELETE /api/admin/routing-rules/{id}``200 {"ok": true}`
- `POST /api/admin/routing-rules/dry-run` Body entweder `{rule_id}` ODER
`{match_type, pattern}` (+ optional `limit`, default 20, max 100) →
`200 {match_type, pattern, match_count, sample_limit, sample: [{id, mail_from,
mail_to, subject, received_at}]}`. Dry-Run ist ILIKE-Näherung gegen `emails`,
LIMIT-begrenzt (kein Vollscan-Timeout); domain_admin sieht nur eigene Tenant-Mails.
RoutingRule-Shape: `{id, tenant_id, match_type, pattern, priority, created_at}`.
### Frontend (2026-07-03, Status: In Review — QA offen)
- `src/lib/api/routing_rules.ts` (NEU) — TS-Typen (`RoutingRule`, `RoutingRuleInput`,
`RoutingMatchType`, `RoutingDryRunResult/Input/Sample`) + API-Funktionen
`getRoutingRules`, `createRoutingRule`, `updateRoutingRule`, `deleteRoutingRule`,
`dryRunRoutingRule`. Nutzt zentralen `request()`-Wrapper aus `core.ts`.
- `src/lib/api/index.ts` — Re-exports der neuen Typen + Funktionen ergänzt.
- `src/components/admin/tabs/RoutingRulesTab.tsx` (NEU) — Tab „Routing-Regeln":
Tabelle (Prio, Typ, Muster, Angelegt; bei superadmin zusätzlich Tenant-Spalte),
CRUD via Dialog (match_type-Select, pattern-Input mit typabhängigem Placeholder,
priority-Input, bei superadmin Tenant-Auswahl-Dropdown), Löschen mit Bestätigung.
Dry-Run-Button im Dialog ruft `dryRunRoutingRule({match_type, pattern})` und zeigt
`match_count` + Stichproben-Tabelle. Prioritäts-Erklärung + Hinweis „keine
rückwirkende Umroutung" als Alert. Loading/Error/Empty-States implementiert.
- `src/app/admin/page.tsx` — Tab-Trigger + `<TabsContent value="routing-rules">`
eingebunden; Tab für alle Admin-Seiten-Besucher sichtbar (Seite ist bereits per
`useAuth("domain_admin")` auf domain_admin+ gegated). `isSuperAdmin`-Prop steuert
Tenant-Spalte/-Dropdown; serverseitiges Scoping bleibt maßgeblich.
- Typecheck: `npx tsc --noEmit` → sauber (Exit 0).
### Offen / Handoff
- QA gegen Acceptance Criteria (CRUD, Dry-Run, Rollen-Gate) auf Testserver.
- Bereits archivierte Mails werden NICHT rückwirkend umgeroutet (nur neue Ingests).
+100
View File
@@ -0,0 +1,100 @@
# PROJ-46: E-Mail als primärer Login-Identifier für Tenant-User
## Status: In Review
**Created:** 2026-06-13
**Last Updated:** 2026-07-03
## Implementation Notes (2026-07-03, Backend)
- **`internal/userstore/userstore.go`:** Neue Funktion `VerifyLogin(ctx, identifier, password) (*User, error)`.
Ablauf: (1) Lookup per `email = $1` (matcht alle User). (2) Falls kein Treffer,
Lookup per `username = $1 AND tenant_id IS NULL` — Tenant-User können sich damit
NICHT mehr per Username anmelden. Danach `active`-Check + `bcrypt.CompareHashAndPassword`.
Neuer Helper `scanUserWithHash` (reicht `pgx.ErrNoRows` unverfälscht durch, damit der
Email→Username-Fallback greift). `VerifyPassword` blieb unangetastet (IMAP-Pfad PROJ-26).
- **`internal/auth/auth.go`:** `Manager.Login()` ruft nun `VerifyLogin(context.Background(), ...)`
statt `VerifyPassword(...)`. `extractDomain(identifier)` musste NICHT angepasst werden:
Tenant-User senden jetzt die E-Mail (`user@domain`) als Identifier, woraus die bestehende
`strings.LastIndex(..., "@")`-Logik die Domain korrekt extrahiert — für den per-Tenant-/
Global-LDAP-Fallback ist das sogar zuverlässiger als der frühere reine Username.
- **`internal/userstore/userstore_test.go`:** `TestVerifyLogin` deckt die volle Matrix ab
(Tenant per E-Mail = Erfolg, Tenant per Username = Reject, Non-Tenant per Username = Erfolg,
Non-Tenant per E-Mail = Erfolg, unbekannter Identifier = Reject, falsches Passwort = Reject).
DB-backed (Skip ohne `TEST_DATABASE_URL`, wie die übrigen userstore-Tests).
- **Kein lokaler `go build`/`go test` möglich** (kein Toolchain im Arbeitsverzeichnis) —
Build-/Testverifikation erfolgt separat auf dem Testserver.
- Frontend (`src/app/page.tsx`, `src/app/admin/login/page.tsx`) wird separat vom Frontend-Agent umgesetzt.
## Implementation Notes (2026-07-03, Frontend)
- **`src/app/page.tsx` (Tenant-User-Login):** Label "Benutzername" → "E-Mail-Adresse",
Input `type="text"``type="email"`, `autoComplete="username"``autoComplete="email"`,
Placeholder + `aria-label` entsprechend angepasst. API-Client (`login()`) unverändert —
der eingegebene String geht weiterhin als `username`-Feld ins JSON-Body.
- **`src/app/admin/login/page.tsx` (Admin/Superadmin-Login):** Label + Placeholder →
"Benutzername oder E-Mail-Adresse". Input bleibt bewusst `type="text"` (beide Formate möglich).
- **`npx tsc --noEmit`:** sauber durchgelaufen (Exit 0), keine Typfehler.
- Kein API-Client-/Backend-Code angefasst. QA gegen Acceptance Criteria steht noch aus.
## Problem Statement
Tenant-User melden sich aktuell mit `username` an, nicht mit ihrer E-Mail-Adresse. Das führt in der Praxis zu Verwechslungen: Ein User versucht sich mit seiner E-Mail-Adresse einzuloggen (die einzige Kennung, die er sich merkt), das Login schlägt mit "invalid_password" fehl, und nach 5 Fehlversuchen innerhalb von 15 Minuten greift das Rate-Limit (429 "too many failed login attempts") — selbst nachdem ein Admin das Passwort zurückgesetzt hat.
Konkreter Support-Fall (2026-06-13): patrick@perlbach24.de konnte sich trotz Passwort-Reset durch den Superadmin nicht einloggen, weil er `patrick@perlbach24.de` statt `patrick` (seinem tatsächlichen `username`) als Login-Identifier verwendete.
## Dependencies
- Betrifft: PROJ-1 (Authentifizierung & Rollen), PROJ-21 (Multi-Tenancy)
- Berührt NICHT: PROJ-26 (IMAP-Server-Schnittstelle) — IMAP-Login bleibt unverändert per `username`
## User Stories
- Als **Tenant-User** möchte ich mich mit meiner **E-Mail-Adresse** anmelden, weil das die Kennung ist, die ich kenne und die mir in Einladungs-/Reset-Mails genannt wird.
- Als **Superadmin/System-User** (ohne Tenant-Zugehörigkeit) möchte ich mich weiterhin mit meinem **Benutzernamen ODER meiner E-Mail-Adresse** anmelden können.
- Als **Tenant-Admin** möchte ich, dass sich Tenant-User NICHT mehr per Benutzername einloggen können, um Verwechslungen wie im Support-Fall vom 2026-06-13 zukünftig zu vermeiden.
## Acceptance Criteria
### Login-Logik (Backend)
- [ ] Neue Lookup-Funktion `Store.VerifyLogin(ctx, identifier, password) (*User, error)` in `internal/userstore/userstore.go`
- [ ] Login per E-Mail-Adresse (`email = $1`) funktioniert für alle User (Tenant-User UND Nicht-Tenant-User)
- [ ] Login per `username` funktioniert NUR für User mit `tenant_id IS NULL` (Superadmin/System-User)
- [ ] Tenant-User (`tenant_id IS NOT NULL`), die ihren `username` als Login-Identifier verwenden, erhalten `invalid_credentials` (kein Login)
- [ ] Bestehende `VerifyPassword(username, password)` bleibt unverändert erhalten für den IMAP-Server-Login-Pfad (PROJ-26)
- [ ] `internal/auth/auth.go` `Manager.Login()` ruft `VerifyLogin()` statt `VerifyPassword()`
### Frontend
- [ ] Tenant-User-Login (`src/app/page.tsx`): Label "Benutzername" → "E-Mail-Adresse", Input-Type `email`, `autoComplete="email"`
- [ ] Admin/Superadmin-Login (`src/app/admin/login/page.tsx`): Label → "Benutzername oder E-Mail-Adresse"
- [ ] API-Request-Format bleibt unverändert (`{"username": "<identifier>", "password": "..."}`)
### Rate-Limiting & Audit
- [ ] `login_attempts.username` Spalte auf VARCHAR(255) erweitert (bereits erledigt, siehe Migration unten)
- [ ] Rate-Limiting-Logik (`CountRecentFailures`, `RecordLoginAttempt`) funktioniert unverändert mit E-Mail-Strings als Schlüssel
- [ ] Audit-Log protokolliert bei Fehlversuchen weiterhin den eingegebenen Identifier (E-Mail oder Username)
### Tests
- [ ] Unit-Tests für `VerifyLogin` decken die vollständige Matrix ab:
- Tenant-User per E-Mail → Erfolg
- Tenant-User per Username (≠ E-Mail) → `invalid_credentials`
- Nicht-Tenant-User per Username → Erfolg
- Nicht-Tenant-User per E-Mail → Erfolg
- Unbekannter Identifier → `invalid_credentials`
## Migration (bereits durchgeführt am 2026-06-13)
- Datenqualitäts-Check auf 192.168.1.131: 0 Tenant-User mit fehlender/ungültiger E-Mail, 0 Username↔E-Mail-Kollisionen
- `login_attempts.username` von VARCHAR(100) → VARCHAR(255) erweitert (idempotenter `initSchema`-Eintrag in `internal/userstore/userstore.go` ergänzt)
- Hinweis: `superadmin@localhost` und `auditor@archivmail.local` (beide `tenant_id IS NULL`) sind als E-Mail-Format ungewöhnlich, aber kein Blocker — diese User können weiterhin per `username` einloggen
## Edge Cases
- **Kollision `username` (User A) == `email` (User B):** Mit `email UNIQUE` selten, aber falls vorhanden gewinnt der `email`-Treffer (User B) immer — User A kann sich mit diesem String dann nicht mehr einloggen, auch wenn `tenant_id IS NULL`. Aktuell 0 solcher Fälle (siehe Migration).
- **LDAP-User (PROJ-16/23):** `extractDomain(identifier)` in `internal/auth/auth.go` muss bei E-Mail-Eingabe weiterhin korrekt funktionieren (E-Mail-Format `user@domain` ist kompatibel zum bisherigen Format).
## Non-Goals
- IMAP-Server-Login (PROJ-26) bleibt unverändert per `username`
- Kein einheitlicher Login-Screen für alle Usertypen (bleibt bei zwei separaten Routen: `/` und `/admin/login`)
- Keine Änderung am API-Request-Wire-Format (`username`-Feld bleibt im JSON-Body, nur die Bedeutung ändert sich)
## Technical Requirements
- **Breaking Change (bewusst):** Tenant-User, die sich bisher per `username` einloggten, müssen künftig die E-Mail-Adresse verwenden. REST-API-Clients (PROJ-13), die `username` für Tenant-User senden, müssen auf `email` umgestellt werden.
- **Betroffene Dateien:**
- `internal/userstore/userstore.go` (neue `VerifyLogin`, initSchema-Erweiterung — bereits erledigt)
- `internal/auth/auth.go` (`Login()` ruft `VerifyLogin()`)
- `src/app/page.tsx` (Label/Input-Type)
- `src/app/admin/login/page.tsx` (Label)
- `internal/auth/auth_test.go` (neue Testfälle)