105 lines
4.0 KiB
Go
105 lines
4.0 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/svcauth"
|
|
)
|
|
|
|
// ExportTokenHeader ist der Header, in dem der Aufrufer ab QA-15 F-09 das
|
|
// Admin-Token uebergibt — vorher stand es als "caller"-Query-Parameter in
|
|
// der URL (Zugriffsprotokolle, Browserverlauf, Proxy-Logs). Derselbe Header
|
|
// wie die anderen internen Admin-Endpunkte (QA-15 F-01..F-03).
|
|
const ExportTokenHeader = svcauth.Header
|
|
|
|
// legacyTokenQueryParams sind die Query-Parameter, unter denen fruehere
|
|
// Versionen das Admin-Token akzeptierten. Taucht einer davon in der Anfrage
|
|
// auf, wird sie abgelehnt UND protokolliert (Akzeptanzkriterium 3) — auch
|
|
// wenn zusaetzlich ein gueltiger Header mitgeschickt wurde, damit ein
|
|
// bestehender, geteilter (und damit potenziell schon kompromittierter)
|
|
// Bookmark/Skript-Aufruf mit URL-Token sichtbar auffaellt und nicht still
|
|
// weiterfunktioniert.
|
|
var legacyTokenQueryParams = []string{"caller", "token"}
|
|
|
|
// 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) {
|
|
// QA-15 F-09: ein per URL-Query mitgeschicktes Token wird explizit
|
|
// abgewiesen UND protokolliert (Akzeptanzkriterium 3), statt es (auch
|
|
// wenn zusaetzlich ein gueltiger Header vorhanden ist) still zu
|
|
// akzeptieren — Query-Parameter landen in Zugriffsprotokollen,
|
|
// Browserverlauf und Proxy-Logs.
|
|
for _, p := range legacyTokenQueryParams {
|
|
if r.URL.Query().Get(p) != "" {
|
|
log.Printf("audit: export-aufruf mit token im query-parameter %q abgewiesen (veraltetes verfahren, siehe QA-15 F-09)", p)
|
|
http.Error(w, "token im query nicht mehr erlaubt, bitte Header "+ExportTokenHeader+" verwenden", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Das Token identifiziert die anfragende Person/den Dienst fuer die
|
|
// Berechtigungspruefung — bewusst getrennt vom Filterfeld "actor" (das
|
|
// den AUDIT-Akteur meint, ueber den gefiltert wird).
|
|
caller := r.Header.Get(ExportTokenHeader)
|
|
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)
|
|
}
|
|
}
|
|
}
|