fix(ingestion): authenticate the REST API as a whole, not route by route
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>
This commit is contained in:
@@ -20,6 +20,7 @@ const defaultBrainURL = "http://koala:30330"
|
||||
// shell-friendly client; MCP framing is unnecessary.
|
||||
type brainClient struct {
|
||||
baseURL string
|
||||
token string // BRAIN_MCP_TOKEN; the REST API requires it since hyperguild#149
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ func newBrainClient() *brainClient {
|
||||
}
|
||||
return &brainClient{
|
||||
baseURL: u,
|
||||
token: os.Getenv("BRAIN_MCP_TOKEN"),
|
||||
http: &http.Client{Timeout: 5 * time.Second},
|
||||
}
|
||||
}
|
||||
@@ -62,6 +64,7 @@ func (c *brainClient) Query(ctx context.Context, topic string, limit int) (*Quer
|
||||
return nil, fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.authorize(req)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -105,6 +108,7 @@ func (c *brainClient) Write(ctx context.Context, wing, hall, filename string, co
|
||||
return nil, fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.authorize(req)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -158,3 +162,10 @@ func (c *brainClient) PassRate(ctx context.Context, skill, window string) (*Pass
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// authorize adds the bearer token when one is configured.
|
||||
func (c *brainClient) authorize(req *http.Request) {
|
||||
if c.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// hyperguild#149: the ingestion REST API now requires a bearer token; the CLI
|
||||
// sends BRAIN_MCP_TOKEN, or it gets 401 on every brain call.
|
||||
func TestBrainClientSendsBrainMCPToken(t *testing.T) {
|
||||
var got []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
got = append(got, r.URL.Path+" "+r.Header.Get("Authorization"))
|
||||
if r.URL.Path == "/write" {
|
||||
_, _ = w.Write([]byte(`{"path":"wiki/x/facts/y.md"}`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"results":[]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("BRAIN_URL", srv.URL)
|
||||
t.Setenv("BRAIN_MCP_TOKEN", "cli-test-token")
|
||||
|
||||
c := newBrainClient()
|
||||
_, err := c.Query(context.Background(), "x", 1)
|
||||
require.NoError(t, err)
|
||||
_, err = c.Write(context.Background(), "x", "facts", "y", strings.NewReader("# y"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"/query Bearer cli-test-token", "/write Bearer cli-test-token"}, got)
|
||||
}
|
||||
|
||||
func TestBrainClientSendsNoAuthorizationWhenTokenUnset(t *testing.T) {
|
||||
var auth string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth = r.Header.Get("Authorization")
|
||||
_, _ = w.Write([]byte(`{"results":[]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("BRAIN_URL", srv.URL)
|
||||
t.Setenv("BRAIN_MCP_TOKEN", "")
|
||||
_, err := newBrainClient().Query(context.Background(), "x", 1)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, auth)
|
||||
}
|
||||
Reference in New Issue
Block a user