fields.go: neuer Typ FacetField mit vier geschlossenen Konstanten (FacetFieldSender/Mailbox/AttachmentType/Tag). IsValid() entscheidet über ein erschöpfendes switch/case statt eine []string-Liste zu durchsuchen — genau der aus known-issues-archivmail.md #12 und known-issues-archivdms.md #10 bekannte Fehler (dynamische Tabellen-/ Feldnamen nur durch eine fragile Whitelist-Funktion abgesichert) wird damit strukturell vermieden: ein vergessener Listeneintrag kann nichts mehr durchlassen, weil es keine durchsuchte Liste mehr gibt. ParseFacetField ist die einzige vorgesehene Konstruktionsstelle für FacetField aus einer externen Zeichenkette. facets.go: FacetFilter.Field ist jetzt FacetField statt string, buildFilteredMust prüft f.Field.IsValid() statt Listenmitgliedschaft (isFacetField entfernt, es gibt keine Liste mehr, die die Entscheidung trifft). Alle Pflichtprüfungen mit echten Nachweisen: unbekannte/erfundene Facettenfelder werden abgelehnt, alle vier realen Facettenfelder funktionieren weiterhin, ein FacetField-Wert per direkter Typkonvertierung (nicht über ParseFacetField) wird trotzdem zuverlässig abgelehnt (Akzeptanzkriterium 2: Whitelist ist nicht die einzige Absicherung), automatisiertes Code-Review bestätigt kein fmt.Sprintf in facets.go/fields.go. Entscheidung dokumentiert: Mail-eigene Implementierung, keine geteilte Utility mit dem DMS-Board (Prüfprotokoll). Keine Regression, insbesondere mail/internal/savedsearch (Konsument von FacetFilter) unverändert grün — go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
4.8 KiB
SRC-11 — Feld-Whitelist-Query-Builder für Suchindex-Zugriff: Prüfprotokoll
Datum: 2026-09-01
Host: 192.168.1.131 (Build/Test/Lint), rsync + ssh
Paket: mail/internal/search (fields.go, facets.go)
Umsetzung
Grundlage war bereits vorhanden (SRC-01/SRC-05): statische FieldXxx-
Konstanten in fields.go, Suchanfragen ausschließlich über Manticores
strukturierte HTTP-JSON-API (kein SQL-String-Zusammenbau). Was fehlte,
war Akzeptanzkriterium 2: die Facetten-Whitelist war eine []string
(FacetFields), gegen die isFacetField per Schleife prüfte — eine
klassische "Whitelist-Funktion", genau das Muster, das
known-issues-archivmail.md #12 und known-issues-archivdms.md #10
als unzureichend benennen (ein vergessener/fehlerhafter Eintrag in der
Liste lässt unbemerkt alles durch).
Neu: FacetField ist ein eigener, geschlossener Typ (fields.go).
FacetField.IsValid() entscheidet über ein erschöpfendes switch/case
auf den vier Konstanten (FacetFieldSender, FacetFieldMailbox,
FacetFieldAttachmentType, FacetFieldTag) — keine Liste mehr, die
durchsucht wird und die man vergessen könnte zu pflegen.
ParseFacetField ist die einzige vorgesehene Stelle, um aus einer
externen Zeichenkette (z. B. künftig ein HTTP-Query-Parameter) ein
FacetField zu machen. FacetFilter.Field ist jetzt FacetField statt
string. buildFilteredMust (einzige Stelle, die Filter-Feldnamen in
eine Suchanfrage einbaut) prüft f.Field.IsValid() statt
Listenmitgliedschaft.
isFacetField (die alte Listenfunktion) ist entfernt — es gibt keine
Liste mehr, die die Zulässigkeitsentscheidung trifft, nur noch das
switch/case in IsValid().
Pflichtprüfung 1: Versuch, ein nicht in der Whitelist enthaltenes Feld anzufragen, wird abgewiesen statt stillschweigend ignoriert
TestBuildFilteredMust_RejectsUnknownField
(search/src11_test.go): zwei Fälle — ein reales Suchfeld, das aber
KEIN Facettenfeld ist (tenant_slug), und ein frei erfundenes Feld
(inkl. eines absichtlich SQL-injection-artigen Strings, um zu zeigen,
dass er nicht einmal in die Fehlermeldung unverarbeitet "verschwindet",
sondern sauber als Fehler zurückkommt) — beide werden mit Fehler
abgelehnt, kein stillschweigendes Ignorieren.
TestBuildFilteredMust_AcceptsAllWhitelistedFields stellt sicher, dass
die Prüfung nicht zu streng ist (alle vier realen Facettenfelder
funktionieren).
Ergebnis: BESTANDEN.
Pflichtprüfung 2: Code-Review bestätigt: kein dynamischer Spalten-/Tabellenname wird per String-Zusammenbau erzeugt
TestNoDynamicFieldNameConstruction (search/src11_test.go):
automatisiertes Code-Review — facets.go und fields.go enthalten in
keiner Codezeile (Kommentarzeilen ausgenommen, dort nur erklärender
Text über den zu vermeidenden Fehler) ein fmt.Sprintf. Ergänzt um
TestFacetField_ClosedSetEvenViaDirectTypeConversion
(Akzeptanzkriterium 2 wörtlich: die Whitelist ist NICHT die einzige
Absicherung — selbst ein FacetField-Wert, der nicht über
ParseFacetField entstanden ist, sondern durch direkte
Typkonvertierung, wird von IsValid() zuverlässig abgelehnt) und
TestParseFacetField_OnlyAcceptsKnownStrings.
Ergebnis: BESTANDEN.
Akzeptanzkriterien
- Spalten-/Feldnamen für dynamische Query-Teile stammen
ausschließlich aus statischen Konstanten bzw. einem geschlossenen
Enum/Switch-Typ:
FacetField+ die vierFacetFieldXxx-Konstanten, durch Pflichtprüfung 2 belegt. - Whitelist ist nicht die einzige Absicherung:
IsValid()ist ein erschöpfendesswitch/case, keine Listen-Iteration mehr — durch Pflichtprüfung 1+2 belegt. - Entscheidung dokumentiert: Mail-eigene Implementierung, keine geteilte Utility mit dem DMS-Board: siehe unten.
Zu Akzeptanzkriterium 3
Diese Kachel implementiert den Query-Builder ausschließlich innerhalb
von mail/internal/search — keine neue geteilte Utility mit dem
DMS-Board angelegt. Konsistent mit der bereits im Ticket-Prompt
genannten, vorab getroffenen Entscheidung
(nexarch-state.json → bewusst_nicht_zentralisiert), Suche/OCR
zwischen Mail und DMS nicht zu zentralisieren.
Build/Vet/Lint/Test — Gesamtmodul
go build ./... → OK
go vet ./... → OK
golangci-lint run ./... → 0 issues
go test ./... -p 1 (TEST_TENANT_DSN, TEST_MANTICORE_URL gesetzt) → alle Pakete ok
Keine Regression — insbesondere mail/internal/savedsearch (Konsument
von search.FacetFilter) unverändert grün: die Typänderung von
Field string zu Field FacetField ist für bestehende Aufrufer, die
den untypisierten String-Konstanten FieldSender usw. übergeben,
verhalten sich unverändert (Go erlaubt die implizite Umwandlung
untypisierter Konstanten).
Ergebnis
SRC-11 erfüllt alle Akzeptanzkriterien mit echten, ausgeführten Nachweisen. Freigeschaltet: QA-04 (zusammen mit ARC-06).