From f78a5474a50473c25bdc5e12728219d784d38beb Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 23 Jun 2026 00:21:11 +0200 Subject: [PATCH] refactor(capturehttp): export Authenticate + DecodeRequest for reuse (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts the Bearer principal-derivation and the request→CaptureInput decode out of the REST handler into exported package funcs, so the MCP capture tool (#55 relay) reuses the exact same auth precedence and wire shape — one implementation, not two. No behaviour change to POST /capture. Co-Authored-By: Claude Opus 4.8 (1M context) --- ingestion/internal/capturehttp/handler.go | 45 ++++++++++++++++------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/ingestion/internal/capturehttp/handler.go b/ingestion/internal/capturehttp/handler.go index 2fd1c73..fd765f4 100644 --- a/ingestion/internal/capturehttp/handler.go +++ b/ingestion/internal/capturehttp/handler.go @@ -11,6 +11,7 @@ import ( "crypto/subtle" "encoding/json" "errors" + "io" "net/http" "strings" @@ -90,19 +91,22 @@ type summaryBody struct { // ServeHTTP authenticates, derives origin, runs the use-case, and maps the // result to an HTTP status. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - principal, viaStatic, ok := h.authenticate(r) + principal, viaStatic, ok := Authenticate(r, h.staticToken, h.staticPrincipal, h.validator) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } - var req request - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + body, err := io.ReadAll(r.Body) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "read body"}) + return + } + in, err := DecodeRequest(body) + if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } - - in := req.toInput() // Principal and origin are server-derived — overwrite anything the // caller may have tried to put in the body. in.Context.Principal = principal @@ -125,26 +129,39 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { writeJSON(w, statusFor(rec), rec) } -// authenticate mirrors the chassis Bearer precedence (static wins, then -// JWT) but returns the resolved principal and whether the static path was -// taken — the chassis middleware hides both, and capture needs them to -// derive the origin. -func (h *Handler) authenticate(r *http.Request) (principal string, viaStatic, ok bool) { +// Authenticate mirrors the chassis Bearer precedence (static token wins, +// then Dex JWT) and returns the resolved principal plus whether the static +// path was taken — the chassis middleware hides both, and capture (REST or +// MCP) needs them to derive the trust-zone origin. ok is false when no +// credential matched. +func Authenticate(r *http.Request, staticToken, staticPrincipal string, validator Validator) (principal string, viaStatic, ok bool) { raw, found := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !found || raw == "" { return "", false, false } - if h.staticToken != "" && subtle.ConstantTimeCompare([]byte(raw), []byte(h.staticToken)) == 1 { - return h.staticPrincipal, true, true + if staticToken != "" && subtle.ConstantTimeCompare([]byte(raw), []byte(staticToken)) == 1 { + return staticPrincipal, true, true } - if h.validator != nil { - if sub, err := h.validator.Validate(r.Context(), raw); err == nil && sub != "" { + if validator != nil { + if sub, err := validator.Validate(r.Context(), raw); err == nil && sub != "" { return sub, false, true } } return "", false, false } +// DecodeRequest parses a capture request body into a CaptureInput. Shared +// by the REST adapter and the MCP capture tool so the wire shape has one +// definition. Principal and Origin are NOT set here — the caller sets them +// from the authenticated identity. +func DecodeRequest(data []byte) (capture.CaptureInput, error) { + var b request + if err := json.Unmarshal(data, &b); err != nil { + return capture.CaptureInput{}, err + } + return b.toInput(), nil +} + func (b request) toInput() capture.CaptureInput { in := capture.CaptureInput{ Context: capture.CaptureContext{