fix(PROJ-50): Cross-Tenant-Lücke + Fehlerbehandlung in DSGVO-Handlern behoben

Bug-1: GetDSGVOMailMeta mit tenant_id-Filter (Defense-in-Depth an DB-Schicht).
Bug-2: Verwaiste open-Einträge bei Auswertungsfehler werden auf failed markiert.
Bug-3: Fehlgeschlagene Suchen werden im Audit-Log protokolliert.
Bug-6: Ungültige date_from/date_to-Eingaben liefern HTTP 400.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-06-30 18:17:25 +02:00
co-authored by Claude Sonnet 4.6
parent dcb88317ac
commit 0552ce49e2
2 changed files with 71 additions and 9 deletions
+37 -6
View File
@@ -2,6 +2,7 @@ package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
@@ -76,16 +77,23 @@ func (s *Server) handleCreateDSGVORequest(w http.ResponseWriter, r *http.Request
PageSize: dsgvoMaxHits,
Page: 1,
}
// Bug-6: reject invalid date formats instead of silently ignoring them.
if req.DateFrom != "" {
if t, err := time.Parse(time.DateOnly, req.DateFrom); err == nil {
searchReq.DateFrom = &t
t, err := time.Parse(time.DateOnly, req.DateFrom)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid date_from format (expected YYYY-MM-DD)")
return
}
searchReq.DateFrom = &t
}
if req.DateTo != "" {
if t, err := time.Parse(time.DateOnly, req.DateTo); err == nil {
t = t.Add(24*time.Hour - time.Second)
searchReq.DateTo = &t
t, err := time.Parse(time.DateOnly, req.DateTo)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid date_to format (expected YYYY-MM-DD)")
return
}
t = t.Add(24*time.Hour - time.Second)
searchReq.DateTo = &t
}
searchIdx := s.idx
@@ -95,6 +103,17 @@ func (s *Server) handleCreateDSGVORequest(w http.ResponseWriter, r *http.Request
result, err := searchIdx.Search(searchReq)
if err != nil {
// Bug-3: failed searches must also be audited.
if s.audlog != nil {
s.audlog.Log(audit.Entry{
EventType: audit.EventDSGVORequest,
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: false,
Detail: fmt.Sprintf("dsgvo: search failed address=%q error=%v", address, err),
})
}
writeError(w, http.StatusInternalServerError, "search failed")
return
}
@@ -110,8 +129,10 @@ func (s *Server) handleCreateDSGVORequest(w http.ResponseWriter, r *http.Request
for _, h := range result.Hits {
ids = append(ids, h.ID)
}
meta, err := s.store.GetDSGVOMailMeta(r.Context(), ids)
meta, err := s.store.GetDSGVOMailMeta(r.Context(), ids, tenantID)
if err != nil {
// Bug-2: do not leave an orphaned "open" request behind on failure.
s.failDSGVORequest(r.Context(), dsReq.ID)
writeError(w, http.StatusInternalServerError, "metadata lookup failed")
return
}
@@ -150,6 +171,8 @@ func (s *Server) handleCreateDSGVORequest(w http.ResponseWriter, r *http.Request
status := dsgvoStatus(summary)
if err := s.store.UpdateDSGVOResult(r.Context(), dsReq.ID, status, &summary); err != nil {
// Bug-2: avoid a stuck "open" request when the result cannot be stored.
s.failDSGVORequest(r.Context(), dsReq.ID)
writeError(w, http.StatusInternalServerError, "could not store result")
return
}
@@ -171,6 +194,14 @@ func (s *Server) handleCreateDSGVORequest(w http.ResponseWriter, r *http.Request
writeJSON(w, http.StatusOK, dsReq)
}
// failDSGVORequest marks a request as failed so no orphaned "open" entry
// remains when evaluation aborts after creation (PROJ-50 Bug-2). Errors here
// are intentionally swallowed: the original failure is already being reported
// to the caller, and a best-effort cleanup must not mask it.
func (s *Server) failDSGVORequest(ctx context.Context, id int64) {
_ = s.store.MarkDSGVORequestFailed(ctx, id)
}
// dsgvoStatus computes the request status from the result summary.
func dsgvoStatus(s storage.DSGVOResultSummary) string {
if s.TotalHits == 0 {