Only /mcp was wrapped in BearerMiddleware; /query, /write, /ingest*, /promote, /pending and the rest on the same mux had no auth, and infra#517 found them on the internet (mitigated there by narrowing the ingress). They are still open on the tailnet and LAN NodePort 30330. mountREST puts the REST routes on their own mux, mounted at "/" behind the same bearer middleware and credential as /mcp. Routes registered on the outer mux stay explicitly public (/mcp has its own auth, /capture, the HMAC webhook, OAuth metadata, /metrics); anything else, including a route added later, is authenticated by default. The hyperguild CLI (cmd/hyperguild and internal/brain) now sends BRAIN_MCP_TOKEN, which it never did. brain-ui and brain-ingest-ntfy already send theirs. Refs #149 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.8 KiB
Go
75 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
chassisauth "git.d-ma.be/mathias/mcp-chassis/auth"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/api"
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/pipeline"
|
|
)
|
|
|
|
const routesTok = "routes-test-token"
|
|
|
|
// hyperguild#149: only /mcp was authenticated; every REST route on the same
|
|
// mux was open, and infra#517 found them on the internet. The REST API is now
|
|
// mounted at "/" behind the bearer middleware, so any route not registered
|
|
// as explicitly public is authenticated, including routes added later.
|
|
func testRouter(t *testing.T) http.Handler {
|
|
t.Helper()
|
|
h := api.NewHandler(t.TempDir(), slog.New(slog.NewTextHandler(os.Stderr, nil)), pipeline.Config{})
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /metrics", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
|
|
mountREST(mux, h, func(next http.Handler) http.Handler {
|
|
return chassisauth.BearerMiddleware(routesTok, nil, "brain", "", next)
|
|
})
|
|
return mux
|
|
}
|
|
|
|
func do(t *testing.T, h http.Handler, method, path, authz string) int {
|
|
t.Helper()
|
|
req := httptest.NewRequest(method, path, strings.NewReader(`{"query":"x"}`))
|
|
if authz != "" {
|
|
req.Header.Set("Authorization", authz)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
return rec.Code
|
|
}
|
|
|
|
func TestRESTRoutesRefuseRequestsWithoutAToken(t *testing.T) {
|
|
r := testRouter(t)
|
|
for _, rt := range []struct{ method, path string }{
|
|
{"POST", "/query"}, {"POST", "/write"}, {"POST", "/index"}, {"POST", "/ingest"},
|
|
{"POST", "/ingest-path"}, {"POST", "/ingest-raw"}, {"POST", "/backfill-refs"},
|
|
{"GET", "/pending"}, {"POST", "/promote"}, {"POST", "/backfill-embeddings"}, {"GET", "/pass-rate"},
|
|
} {
|
|
assert.Equal(t, http.StatusUnauthorized, do(t, r, rt.method, rt.path, ""), "%s %s without a token", rt.method, rt.path)
|
|
assert.Equal(t, http.StatusUnauthorized, do(t, r, rt.method, rt.path, "Bearer wrong"), "%s %s with a wrong token", rt.method, rt.path)
|
|
}
|
|
}
|
|
|
|
func TestRESTRoutesServeAValidToken(t *testing.T) {
|
|
r := testRouter(t)
|
|
assert.NotEqual(t, http.StatusUnauthorized, do(t, r, "POST", "/query", "Bearer "+routesTok))
|
|
assert.Equal(t, http.StatusOK, do(t, r, "GET", "/pending", "Bearer "+routesTok))
|
|
}
|
|
|
|
// Default-deny: a path nobody registered is still behind auth, so a route
|
|
// added to the REST mux later cannot ship open by omission.
|
|
func TestUnregisteredPathIsAuthenticatedByDefault(t *testing.T) {
|
|
assert.Equal(t, http.StatusUnauthorized, do(t, testRouter(t), "POST", "/some-future-route", ""))
|
|
}
|
|
|
|
// Explicitly public routes registered on the outer mux keep working.
|
|
func TestMetricsStaysOpenForPrometheus(t *testing.T) {
|
|
assert.Equal(t, http.StatusOK, do(t, testRouter(t), "GET", "/metrics", ""))
|
|
}
|