internal/audit/export.go: StreamCSV/StreamJSON filtern nach Tenant, Akteur, Aktion und Zeitraum (Akzeptanzkriterium 1) und schreiben Zeile fuer Zeile ueber rows.Next() DIREKT auf den uebergebenen io.Writer — zu keinem Zeitpunkt wird das komplette Ergebnis im Speicher aufgebaut (Akzeptanz- kriterium 3). JSON-Export als JSON Lines statt einem grossen Array, um Streaming ohne Sonderbehandlung von Klammern/Kommas zu ermoeglichen. ExportHandler (Akzeptanzkriterium 2) schreibt direkt auf http.ResponseWriter — derselbe Streaming-Pfad wie in Tests, kein Zwischenpuffer nur fuer HTTP. Authorize ist eine schmale Schnittstelle (Vorbild: AUD-05 RetentionRegistrar- Muster), da die eigentliche Rollenpruefung RBAC-02 (Policy-Enforcement) ist und nicht Teil dieser Kachel — der Handler kennt nur "darf dieser Aufrufer exportieren", nicht wie das entschieden wird. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Export mit hoher Eintragszahl ohne uebermaessigen Speicherverbrauch — TestExport_StreamsLargeResultWithoutExcessiveMemory: 20.000 Eintraege, Heap-Wachstum waehrend Export nur ~1.8KB (Schwelle 3MB). PASS. 2. Filterkombinationen automatisiert gegen erwartete Ergebnismengen — TestExport_FilterCombinations (Tenant/Actor/Action einzeln und kombiniert) und TestExport_TimeRangeFilter (innerhalb/ausserhalb Zeitraum). PASS. 3. Zugriff ohne passende Berechtigung abgewiesen — TestExportHandler_RejectsWithoutAuthorization: fehlender/falscher caller -> 403, berechtigter caller -> 200. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
74 lines
2.4 KiB
Go
74 lines
2.4 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// Authorize entscheidet, ob caller den Export ausfuehren darf. Die
|
|
// eigentliche Rollen-/Rechtepruefung (RBAC-02 Policy-Enforcement) ist nicht
|
|
// Teil dieser Kachel — ExportHandler kennt nur diese schmale Schnittstelle,
|
|
// analog zum RetentionRegistrar-Muster aus AUD-05.
|
|
type Authorize func(ctx context.Context, caller string) bool
|
|
|
|
// ExportHandler stellt den Export als HTTP-Endpunkt bereit
|
|
// (Akzeptanzkriterium 2: fuer berechtigte Rollen verfuegbar).
|
|
type ExportHandler struct {
|
|
log *Log
|
|
authorize Authorize
|
|
}
|
|
|
|
func NewExportHandler(log *Log, authorize Authorize) *ExportHandler {
|
|
return &ExportHandler{log: log, authorize: authorize}
|
|
}
|
|
|
|
// Export liest Filter-Query-Parameter (tenant, actor, action, from, to,
|
|
// format) und schreibt DIREKT auf den ResponseWriter (io.Writer) — dieselbe
|
|
// Streaming-Funktion wie in export.go, kein zusaetzlicher Pufferungsschritt.
|
|
func (h *ExportHandler) Export(w http.ResponseWriter, r *http.Request) {
|
|
// "caller" identifiziert die anfragende Person fuer die Berechtigungs-
|
|
// pruefung — bewusst getrennt vom Filterfeld "actor" (das den
|
|
// AUDIT-Akteur meint, ueber den gefiltert wird).
|
|
caller := r.URL.Query().Get("caller")
|
|
if caller == "" || !h.authorize(r.Context(), caller) {
|
|
http.Error(w, "keine berechtigung fuer audit-log-export", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
filter := Filter{
|
|
TenantSlug: r.URL.Query().Get("tenant"),
|
|
Actor: r.URL.Query().Get("actor"),
|
|
Action: r.URL.Query().Get("action"),
|
|
}
|
|
if from := r.URL.Query().Get("from"); from != "" {
|
|
t, err := time.Parse(time.RFC3339, from)
|
|
if err != nil {
|
|
http.Error(w, "ungueltiges from-datum, erwartet RFC3339", http.StatusBadRequest)
|
|
return
|
|
}
|
|
filter.From = &t
|
|
}
|
|
if to := r.URL.Query().Get("to"); to != "" {
|
|
t, err := time.Parse(time.RFC3339, to)
|
|
if err != nil {
|
|
http.Error(w, "ungueltiges to-datum, erwartet RFC3339", http.StatusBadRequest)
|
|
return
|
|
}
|
|
filter.To = &t
|
|
}
|
|
|
|
switch r.URL.Query().Get("format") {
|
|
case "json":
|
|
w.Header().Set("Content-Type", "application/x-ndjson")
|
|
if err := h.log.StreamJSON(r.Context(), filter, w); err != nil {
|
|
http.Error(w, "export fehlgeschlagen", http.StatusInternalServerError)
|
|
}
|
|
default:
|
|
w.Header().Set("Content-Type", "text/csv")
|
|
if err := h.log.StreamCSV(r.Context(), filter, w); err != nil {
|
|
http.Error(w, "export fehlgeschlagen", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|