Files
archivmail/internal/api/saved_search_handlers.go
T
sysops 92e57431c3 fix(PROJ-55): Tenant-Isolation für Rolle "auditor" + Audit-Log korrigieren
Kritischer Sicherheitsbug: Auditoren mit zugewiesenem Tenant sahen Mails
und Audit-Log-Einträge anderer Tenants (DSGVO-relevant). auditor wird
jetzt analog zu domain_auditor pro Tenant gescoped, sofern tenant_id
gesetzt ist (Abwärtskompatibilität: ohne tenant_id bleibt der bisherige
globale Zugriff erhalten). Betrifft Mail-Suche, Mail-Detailzugriff,
Export, eDiscovery, Threads, OCR sowie das Audit-Log (DB + tamper-evidentes
Flat-File), inkl. Befüllung von tenant_id an allen Audit-Log-Schreibstellen.
2026-06-21 22:26:06 +02:00

134 lines
3.7 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"archivmail/internal/audit"
"archivmail/internal/storage"
)
// handleListSavedSearches returns all saved searches for the current user.
// GET /api/searches/saved
func (s *Server) handleListSavedSearches(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
tenantID := tenantFromCtx(r.Context())
if tenantID == nil {
writeError(w, http.StatusBadRequest, "tenant context required")
return
}
searches, err := s.store.ListSavedSearches(r.Context(), sess.UserID, *tenantID)
if err != nil {
s.logger.Error("saved_searches: list failed", "err", err)
writeError(w, http.StatusInternalServerError, "failed to list saved searches")
return
}
if searches == nil {
searches = []storage.SavedSearch{}
}
writeJSON(w, http.StatusOK, searches)
}
// handleCreateSavedSearch creates a new saved search.
// POST /api/searches/saved
// Body: {"name": "...", "query": {...}}
func (s *Server) handleCreateSavedSearch(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
tenantID := tenantFromCtx(r.Context())
if tenantID == nil {
writeError(w, http.StatusBadRequest, "tenant context required")
return
}
var body struct {
Name string `json:"name"`
Query json.RawMessage `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
name := strings.TrimSpace(body.Name)
if name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if len(name) > 200 {
writeError(w, http.StatusBadRequest, "name too long (max 200 chars)")
return
}
if len(body.Query) == 0 {
writeError(w, http.StatusBadRequest, "query is required")
return
}
// Validate that query is valid JSON object
var tmp map[string]interface{}
if err := json.Unmarshal(body.Query, &tmp); err != nil {
writeError(w, http.StatusBadRequest, "query must be a valid JSON object")
return
}
ss, err := s.store.CreateSavedSearch(r.Context(), sess.UserID, *tenantID, name, []byte(body.Query))
if err != nil {
s.logger.Error("saved_searches: create failed", "err", err)
writeError(w, http.StatusInternalServerError, "failed to create saved search")
return
}
s.audlog.Log(audit.Entry{
EventType: "saved_search_create",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Detail: fmt.Sprintf("saved search id=%d name=%q", ss.ID, name),
Success: true,
})
writeJSON(w, http.StatusCreated, ss)
}
// handleDeleteSavedSearch deletes a saved search by ID.
// DELETE /api/searches/saved/{id}
func (s *Server) handleDeleteSavedSearch(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
tenantID := tenantFromCtx(r.Context())
if tenantID == nil {
writeError(w, http.StatusBadRequest, "tenant context required")
return
}
idStr := r.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
writeError(w, http.StatusBadRequest, "invalid id")
return
}
err = s.store.DeleteSavedSearch(r.Context(), id, sess.UserID, *tenantID)
if err != nil {
if strings.Contains(err.Error(), "not found or not owned") {
writeError(w, http.StatusForbidden, "saved search not found or access denied")
return
}
s.logger.Error("saved_searches: delete failed", "err", err)
writeError(w, http.StatusInternalServerError, "failed to delete saved search")
return
}
s.audlog.Log(audit.Entry{
EventType: "saved_search_delete",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Detail: fmt.Sprintf("saved search id=%d", id),
Success: true,
})
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}