package identity_test import ( "net/http" "net/http/httptest" "strings" "testing" "git.wntrmute.dev/kyle/crossbar/internal/identity" ) // The middleware sits in front of the proxy: it names the route the same way the proxy does // (X-Crossbar-Route header, else first path segment) and refuses callers a route does not list. func TestMiddleware(t *testing.T) { peers := map[string][]string{"locked": {"talos"}, "open": nil} inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) }) h := identity.Middleware(identity.NewChecker(fakeResolver{"100.64.0.5": "talos", "100.64.0.9": "titan"}), func(route string) ([]string, bool) { p, ok := peers[route]; return p, ok }, inner) for _, tc := range []struct { name, path, hdr, addr string want int }{ {"open route, anyone", "/open/v1/models", "", "203.0.113.1:5", 204}, {"locked, right peer", "/locked/v1/models", "", "100.64.0.5:5", 204}, {"locked, wrong peer", "/locked/v1/models", "", "100.64.0.9:5", 403}, {"locked, not a peer", "/locked/v1/models", "", "203.0.113.1:5", 403}, {"locked via header", "/v1/models", "locked", "100.64.0.9:5", 403}, {"header wins over path", "/open/v1/models", "locked", "203.0.113.1:5", 403}, {"unknown route passes through to the proxy's own 404", "/nope/v1/models", "", "203.0.113.1:5", 204}, {"admin path is never gated here", "/_crossbar/hosts", "", "203.0.113.1:5", 204}, } { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tc.path, nil) req.RemoteAddr = tc.addr if tc.hdr != "" { req.Header.Set("X-Crossbar-Route", tc.hdr) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != tc.want { t.Errorf("%s = %d, want %d (%s)", tc.path, rec.Code, tc.want, rec.Body.String()) } if rec.Code == 403 && (!strings.HasPrefix(rec.Header().Get("Content-Type"), "application/json") || !strings.Contains(rec.Body.String(), `"forbidden route"`)) { t.Errorf("403 must be JSON {\"error\":\"forbidden route\"}: %q", rec.Body.String()) } }) } } // HeaderResolver is the test/smoke identity source: it trusts X-Crossbar-Peer. It exists so the // smoke run can exercise the gate without a tailnet; config must call it out as insecure. func TestHeaderResolver(t *testing.T) { inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) }) h := identity.Middleware(identity.NewHeaderChecker(), func(route string) ([]string, bool) { return []string{"talos"}, true }, inner) req := httptest.NewRequest(http.MethodGet, "/r/v1/models", nil) req.Header.Set("X-Crossbar-Peer", "talos") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 204 { t.Errorf("header peer talos: %d", rec.Code) } req.Header.Set("X-Crossbar-Peer", "titan") rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 403 { t.Errorf("header peer titan: %d, want 403", rec.Code) } req.Header.Del("X-Crossbar-Peer") rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 403 { t.Errorf("no header: %d, want 403", rec.Code) } }