fix(PROJ-43): Dry-Run für from_addr/to_addr matcht bare Adressen statt <addr>-Form

dryRunCondition() erwartete faelschlich die Winkelklammer-Form "Name <addr>",
waehrend mail_from/mail_to die Adresse bare speichern - Dry-Run zeigte dadurch
immer 0 Treffer fuer Adress-Regeln, obwohl der Live-Matcher (routeBareAddr)
korrekt matcht. QA-Ergebnisse (Bug-1) in die Feature-Spec uebernommen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-07-04 11:57:21 +02:00
co-authored by Claude Sonnet 5
parent 04e5b0f74a
commit 4c92587b60
4 changed files with 129 additions and 4 deletions
+72
View File
@@ -110,3 +110,75 @@ RoutingRule-Shape: `{id, tenant_id, match_type, pattern, priority, created_at}`.
### Offen / Handoff
- QA gegen Acceptance Criteria (CRUD, Dry-Run, Rollen-Gate) auf Testserver.
- Bereits archivierte Mails werden NICHT rückwirkend umgeroutet (nur neue Ingests).
## QA Test Results
**Getestet:** 2026-07-04 auf Testserver 192.168.1.132 (Binary v0.9.1, deployt 2026-07-03).
Test-Accounts: `qa-superadmin` (superadmin), `qa-da-t1` (domain_admin Tenant 1),
`qa-da-t3` (domain_admin Tenant 3). **Gesamtergebnis: BESTANDEN MIT 1 BUG** (Dry-Run für
`from_addr`/`to_addr` liefert falsche Nullwerte — siehe Bug-1). CRUD, Tenant-Scoping/IDOR und
Wildcard-Matching sind grün.
### Ergebnis je Acceptance Criterion
- [x] **Tabelle `tenant_routing_rules`** — BESTANDEN. Schema mit Spalten (id, tenant_id NOT NULL
FK, match_type, pattern, priority, created_at) vorhanden; CRUD liefert erwartete Shape.
- [x] **SMTP-Daemon + IMAP-Import prüfen Regeln** — Code-verifiziert (nicht per Live-Mail).
`resolveTenantByRules()` in `internal/smtpd/smtpd.go:107` VOR `tenant_domains`-Logik;
`ResolveTenantByRoutingRules()` in `internal/imap/importer.go:255`. Verdrahtung vorhanden.
Hinweis: End-to-End-Zuordnung per echtem Mailversand nicht durchgeführt (kein Live-SMTP-Test).
- [x] **API CRUD (Admin only)** — BESTANDEN. GET/POST/PUT/DELETE unter `/api/admin/routing-rules`
funktionieren; ohne Cookie → `401`. POST als domain_admin ohne `tenant_id` erzwingt eigenen
Tenant (`201 {"id":5}`).
- [x] **Frontend Regel-Verwaltung** — Komponente `RoutingRulesTab.tsx` vorhanden, Datenkontrakt
passt zur API. UI visuell nicht separat durchgeklickt.
- [x] **Priorität: höhere gewinnt** — Code-verifiziert. `ListTenantRoutingRules` sortiert
`ORDER BY priority DESC, id ASC`; `ResolveTenantByRoutingRules` nimmt ersten Match →
höchste Priorität, bei Gleichstand niedrigste id. Logik korrekt.
- [~] **Dry-Run** — TEILWEISE. `from_domain`/`to_domain` korrekt (perlbach24.de → 955 bzw.
2962 Treffer, inkl. Wildcard). `from_addr`/`to_addr` liefern falsche 0-Treffer → **Bug-1**.
### Sicherheit / Tenant-Isolation / IDOR
- **Auth-Bypass:** Ohne Cookie → `401` auf allen Endpunkten. BESTANDEN.
- **IDOR Create:** domain_admin T1 versucht Regel mit `tenant_id:3` → `403
{"error":"tenant_id required and must match your scope"}`. BESTANDEN.
- **IDOR Update:** T1 versucht PUT auf Regel id 2 (gehört Tenant 3) → `403
{"error":"forbidden"}`, Regel unverändert. BESTANDEN.
- **IDOR Delete:** T1 versucht DELETE Regel id 2 (Tenant 3) → `403 {"error":"forbidden"}`,
Regel bleibt bestehen. BESTANDEN.
- **Tenant-Move via Body:** T1 versucht eigene Regel id 5 per PUT `tenant_id:3` zu verschieben
→ `403 {"error":"tenant_id must match your scope"}`, Regel bleibt Tenant 1. BESTANDEN.
- **Listing-Scope:** domain_admin T1 sieht nur Tenant-1-Regeln, T3 nur Tenant-3; superadmin
alle. Kein Cross-Tenant-Leck. BESTANDEN. (Deckt die PROJ-61-Bugklasse ab: Tenant-Scope wird
zusätzlich zum Rollen-Check via `tenantAccessAllowed` geprüft.)
- **SQL-Injection Dry-Run:** Pattern `' OR 1=1 --` → `match_count:0`, keine Fehler, keine
Anomalie im Log. Parametrisierte Query (`$1`), sauber. BESTANDEN.
### Wildcard-Matching
- `domainMatches()` (`tenant_routing_rules.go:217`): `*.base` matcht Sub-Domains UND die
Apex-Domain (dokumentiert im Code-Kommentar Zeile 216). Dry-Run `*.perlbach24.de` = 955 =
`perlbach24.de` bestätigt dieses beabsichtigte Verhalten. BESTANDEN (Spec "matcht
Sub-Domains" wird als "Sub-Domains + Apex" umgesetzt — konsistent Engine↔Dry-Run).
### Offene Bugs
**Bug-1 — Dry-Run für `from_addr`/`to_addr` liefert falsche Nullwerte. Severity: MEDIUM.
Priorität: Hoch (Kernfunktion des AC "Dry-Run" für 2 von 4 Match-Typen unbrauchbar).**
- Repro: `POST /api/admin/routing-rules/dry-run {"match_type":"from_addr","pattern":
"support@perlbach24.de"}` → `match_count:0`, obwohl 955 Mails exakt von dieser Adresse
existieren (bestätigt via `SELECT ... WHERE mail_from ILIKE '%support@perlbach24.de%'`).
- Ursache: `dryRunCondition()` in `internal/storage/tenant_routing_rules.go:317-319` baut für
`from_addr`/`to_addr` die Bedingung `LOWER(mail_from) LIKE '%<' + pattern + '>%'`, erwartet
also die Winkelklammer-Form `Name <addr>`. Die Spalte `mail_from` speichert die Adresse aber
bare (`support@perlbach24.de`, ohne `<>`), daher matcht das Pattern nie. Der Live-Matcher
(`routeBareAddr`, Zeile 205) normalisiert korrekt und würde matchen → Dry-Run und echte
Regel-Anwendung sind inkonsistent. Admin sieht "0 betroffen" und konfiguriert Adress-Regeln
im Blindflug.
- Fix-Richtung (für Backend Developer, nicht von QA umgesetzt): Bedingung so bauen, dass beide
Speicherformen abgedeckt sind, z.B. Match auf bare Adresse (`LOWER(mail_from) LIKE '%'||p||'%'`
bzw. genauer gegen `@`-Grenze) statt harte `<...>`-Umklammerung.
### Testdaten-Hygiene
- Die für den Test angelegte Regel id 5 (Tenant 1) wurde nach dem Test wieder gelöscht
(`DELETE /api/admin/routing-rules/5` → `200 {"ok":true}`). Vorbestehende Regeln id 1-4
(aus früherem QA-Lauf) unangetastet gelassen.
- Passwörter der `qa-*`-Accounts für den Test gesetzt (siehe PROJ-52 QA-Hinweis). Kein
Produktivserver (131) berührt.