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
+34 -3
View File
@@ -12,6 +12,7 @@ const (
DSGVOStatusOpen = "open" // angelegt, noch nicht verarbeitet
DSGVOStatusPartial = "partial" // teilweise abgelehnt (Mischung)
DSGVOStatusCompleted = "completed" // abgeschlossen (0 Treffer, alle löschbar/abgelehnt, oder gelöscht)
DSGVOStatusFailed = "failed" // Auswertung nach Anlage fehlgeschlagen (PROJ-50 Bug-2)
)
// DSGVOAffectedMail is a single mail affected by a DSGVO erasure request.
@@ -101,6 +102,21 @@ func (s *Store) UpdateDSGVOResult(ctx context.Context, id int64, status string,
return nil
}
// MarkDSGVORequestFailed flags a request as failed without touching the result
// summary. Used to clean up a freshly created request whose evaluation aborted
// (PROJ-50 Bug-2), so no orphaned "open" entry remains.
func (s *Store) MarkDSGVORequestFailed(ctx context.Context, id int64) error {
if s.db == nil {
return fmt.Errorf("storage: no db")
}
_, err := s.db.Exec(ctx,
`UPDATE dsgvo_requests SET status=$1 WHERE id=$2`, DSGVOStatusFailed, id)
if err != nil {
return fmt.Errorf("storage: mark dsgvo failed: %w", err)
}
return nil
}
// ListDSGVORequests returns all requests in the given tenant scope, newest first.
// A nil tenantID returns requests with NULL tenant_id (global/superadmin scope).
func (s *Store) ListDSGVORequests(ctx context.Context, tenantID *int64) ([]DSGVORequest, error) {
@@ -168,12 +184,27 @@ type DSGVOMailMeta struct {
// GetDSGVOMailMeta batch-loads subject, received_at and retain_until for the
// given mail IDs. Missing IDs are omitted. Used by the DSGVO workflow to avoid
// loading and parsing every raw mail file.
func (s *Store) GetDSGVOMailMeta(ctx context.Context, ids []string) (map[string]DSGVOMailMeta, error) {
//
// tenantID scopes the lookup to a single tenant to prevent cross-tenant leakage
// (PROJ-50 Bug-1): a DSGVO request belongs to exactly one tenant, so only mails
// of that tenant may be evaluated. A nil tenantID is the global/superadmin scope
// and matches mails of every tenant.
func (s *Store) GetDSGVOMailMeta(ctx context.Context, ids []string, tenantID *int64) (map[string]DSGVOMailMeta, error) {
if s.db == nil || len(ids) == 0 {
return map[string]DSGVOMailMeta{}, nil
}
rows, err := s.db.Query(ctx,
`SELECT id, subject, received_at, retain_until FROM emails WHERE id = ANY($1)`, ids)
var (
query string
args []interface{}
)
if tenantID == nil {
query = `SELECT id, subject, received_at, retain_until FROM emails WHERE id = ANY($1)`
args = []interface{}{ids}
} else {
query = `SELECT id, subject, received_at, retain_until FROM emails WHERE id = ANY($1) AND tenant_id = $2`
args = []interface{}{ids, *tenantID}
}
rows, err := s.db.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("storage: dsgvo mail meta: %w", err)
}