diff --git a/src/frontend/apiserver/middleware/validatehandler.go b/src/frontend/apiserver/middleware/validatehandler.go index 76b0739c..2758af50 100644 --- a/src/frontend/apiserver/middleware/validatehandler.go +++ b/src/frontend/apiserver/middleware/validatehandler.go @@ -44,23 +44,30 @@ func ValidateHandler() gin.HandlerFunc { } } -func isExcludedPath(path string) bool { +// jwtExcludedPaths are the routes that skip JWT validation. An entry is +// matched exactly, unless its last segment is a gin catch-all ("*name", as in +// the route "/swagger/*any"): then any path under the part before the "*" +// matches (bug-1962). +var jwtExcludedPaths = []string{ + "/ping", + "/xce/v1/auth/logon", + "/swagger/*any", + "/xce/v1/auth/logout", +} - allowed := false - // define some excluded paths - excludedPaths := []string{ - "/ping", - "/xce/v1/auth/logon", - "/swagger/*any", - "/xce/v1/auth/logout", - } - for _, excludedPath := range excludedPaths { - if strings.HasPrefix(path, excludedPath[:len(excludedPath)-1]) { // 去掉最后的 * - allowed = true - break +func isExcludedPath(path string) bool { + for _, excludedPath := range jwtExcludedPaths { + if i := strings.LastIndex(excludedPath, "/*"); i >= 0 { + if strings.HasPrefix(path, excludedPath[:i+1]) { + return true + } + continue + } + if path == excludedPath { + return true } } - return allowed + return false } // Token validation middleware diff --git a/src/frontend/apiserver/middleware/validatehandler_test.go b/src/frontend/apiserver/middleware/validatehandler_test.go index eebcd91d..aec2e814 100644 --- a/src/frontend/apiserver/middleware/validatehandler_test.go +++ b/src/frontend/apiserver/middleware/validatehandler_test.go @@ -150,3 +150,58 @@ func TestIsValidToken_PinsSigningAlgorithm(t *testing.T) { }) } } + +// bug-1962: exact excluded entries match only themselves; only the wildcard +// entry (/swagger/*any) matches by prefix. +func TestIsExcludedPath(t *testing.T) { + tests := []struct { + path string + want bool + }{ + {"/ping", true}, + {"/xce/v1/auth/logon", true}, + {"/xce/v1/auth/logout", true}, + {"/swagger/index.html", true}, + {"/swagger/doc.json", true}, + // prefix look-alikes must not skip JWT validation + {"/pinX", false}, + {"/pin", false}, + {"/ping/x", false}, + {"/pingpong", false}, + {"/xce/v1/auth/logonX", false}, + {"/xce/v1/auth/logo", false}, + {"/xce/v1/auth/logout/x", false}, + {"/swaggerX", false}, + {"/xce/v1/jobs", false}, + {"/", false}, + } + for _, tt := range tests { + if got := isExcludedPath(tt.path); got != tt.want { + t.Errorf("isExcludedPath(%q) = %v, want %v", tt.path, got, tt.want) + } + } +} + +// bug-1962: a request to a path that only shares a prefix with an excluded +// entry must still be rejected without a token; the excluded entry itself +// still needs none. +func TestValidateHandler_PrefixLookAlikeNeedsToken(t *testing.T) { + setTestAuthConfig(t) + gin.SetMode(gin.TestMode) + r := gin.New() + r.Use(ValidateHandler()) + r.GET("/pinX", func(ctx *gin.Context) { ctx.String(http.StatusOK, "reached") }) + r.GET("/ping", func(ctx *gin.Context) { ctx.String(http.StatusOK, "pong") }) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/pinX", nil)) + if strings.Contains(w.Body.String(), "reached") { + t.Errorf("/pinX without a token reached the handler (status %d)", w.Code) + } + + w = httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/ping", nil)) + if w.Code != http.StatusOK || w.Body.String() != "pong" { + t.Errorf("/ping without a token: status %d body %q, want 200 pong", w.Code, w.Body.String()) + } +}