From ed14c5385c9ecda4ec0359351cd7ecfcfdce12b8 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Tue, 30 Jun 2026 09:10:06 -0400 Subject: [PATCH] Fix API endpoint validation for prefix-mounted SCIM routes The SCIM endpoints are served by the elimity-com/scim library mounted as a single prefix handler on the root ServeMux, so they are never registered as individual gorilla/mux routes. Since the routes can't be discovered, supply them to the validator instead: add scim.RegisterValidationRoutes, a FeatureRouteFunc that registers stub routes for the SCIM endpoints (handlers are never invoked, only their path templates and methods are inspected). Wire it into the three Validate call sites (production serve, test helper, svctest). --- cmd/fleet/serve.go | 5 +- ee/server/scim/validation_routes.go | 79 ++++++++++++++++++++++++++++ server/service/svctest/server.go | 4 ++ server/service/testing_utils_test.go | 4 ++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 ee/server/scim/validation_routes.go diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index c015a6a90e..19045aaee3 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -735,7 +735,10 @@ func runServeCmd(cmd *cobra.Command, configManager configpkg.Manager, debug, dev apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore, redisPool, carveStore, []endpointer.HandlerRoutesFunc{android_service.GetRoutes(svc, androidSvc), activityRoutes, acmeRoutes, chartRoutes}, extra...) - if err := apiendpoints.Validate(apiHandler); err != nil { + // SCIM endpoints are served by a prefix-mounted handler (see + // scim.RegisterSCIM) that gorilla/mux can't introspect, so surface + // their routes to the validator explicitly. + if err := apiendpoints.Validate(apiHandler, scim.RegisterValidationRoutes); err != nil { panic(fmt.Sprintf("error initializing API endpoints: %v", err)) } apiHandler = service.WithMDMSSOCallbackRedirect(svc, logger, apiHandler) diff --git a/ee/server/scim/validation_routes.go b/ee/server/scim/validation_routes.go new file mode 100644 index 0000000000..5ea13639d3 --- /dev/null +++ b/ee/server/scim/validation_routes.go @@ -0,0 +1,79 @@ +package scim + +import ( + "net/http" + + kithttp "github.com/go-kit/kit/transport/http" + "github.com/gorilla/mux" +) + +// CAVEAT — keep this in sync with scim.go. The route list below mirrors +// two things that cannot be introspected from the github.com/elimity-com/scim +// library (its Server fields are unexported and routing is a hardcoded switch in +// ServeHTTP): (1) the resource Endpoints registered in RegisterSCIM, and (2) the +// library's discovery endpoints and per-resource method matrix. Adding or +// renaming a SCIM resource type in scim.go, or a library upgrade that changes +// its routing, requires a matching edit here. The coupling is by convention, not +// enforced by the compiler — but it is enforced at runtime: apiendpoints.Validate +// fails whenever a catalog SCIM endpoint isn't covered here, so drift cannot +// ship silently. + +// scimRootPath is the path prefix the SCIM handler is mounted under (see +// RegisterSCIM). The /_version_/ placeholder is expanded to the concrete API +// version when comparing against the api_endpoints catalog. +const scimRootPath = "/api/_version_/fleet/scim" + +// SCIM resource endpoints, matching the Endpoint of each resource type +// registered in RegisterSCIM. Keep these in sync with that list. +const ( + usersEndpoint = "/Users" + groupsEndpoint = "/Groups" +) + +// servedRoute is a (method, path-template) pair served by the SCIM handler. +type servedRoute struct { + method string + tpl string +} + +// servedRoutes returns the routes served by the prefix-mounted SCIM handler. +// The github.com/elimity-com/scim library routes these internally in +// (scim.Server).ServeHTTP, so they never reach gorilla/mux and cannot be +// discovered by walking the router. We reconstruct them from the resource +// endpoints the server is configured with plus the discovery endpoints the +// library hardcodes per RFC 7644. Keep this in sync with RegisterSCIM's +// resource types and the library's ServeHTTP switch. +func servedRoutes() []servedRoute { + var routes []servedRoute + add := func(method, tpl string) { + routes = append(routes, servedRoute{method: method, tpl: tpl}) + } + + // Discovery endpoints (fixed by the SCIM library / RFC 7644). + add(http.MethodGet, scimRootPath+"/Schemas") + add(http.MethodGet, scimRootPath+"/ServiceProviderConfig") + add(http.MethodGet, scimRootPath+"/ResourceTypes") + + // CRUD endpoints, one set per registered resource type. + for _, endpoint := range []string{usersEndpoint, groupsEndpoint} { + base := scimRootPath + endpoint + add(http.MethodGet, base) + add(http.MethodPost, base) + add(http.MethodGet, base+"/{id}") + add(http.MethodPut, base+"/{id}") + add(http.MethodPatch, base+"/{id}") + add(http.MethodDelete, base+"/{id}") + } + return routes +} + +// RegisterValidationRoutes registers stub routes for every endpoint served by +// the prefix-mounted SCIM handler (see RegisterSCIM) onto r. It exists so +// apiendpoints.Validate can confirm the api_endpoints catalog stays in sync +// with what SCIM actually serves; the handlers are never invoked, only their +// path templates and methods are inspected. +func RegisterValidationRoutes(r *mux.Router, _ []kithttp.ServerOption) { + for _, rt := range servedRoutes() { + r.Handle(rt.tpl, http.NotFoundHandler()).Methods(rt.method) + } +} diff --git a/server/service/svctest/server.go b/server/service/svctest/server.go index 15c76db9ba..386f33aeea 100644 --- a/server/service/svctest/server.go +++ b/server/service/svctest/server.go @@ -222,6 +222,10 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl } var carveStore fleet.CarveStore = ds // In tests, we use MySQL as storage for carves. apiHandler := service.MakeHandler(svc, cfg, logger, limitStore, redisPool, carveStore, featureRoutes, extra...) + // SCIM endpoints are served by a prefix-mounted handler (see scim.RegisterSCIM) + // that gorilla/mux can't introspect, so surface their routes to the validator + // explicitly. They're always in the catalog, regardless of opts[0].EnableSCIM. + extraInitFeatureRoutes = append(extraInitFeatureRoutes, scim.RegisterValidationRoutes) if err := apiendpoints.Validate(apiHandler, extraInitFeatureRoutes...); err != nil { t.Fatalf("error initializing API endpoints: %v", err) } diff --git a/server/service/testing_utils_test.go b/server/service/testing_utils_test.go index 4b2f6d9910..fb6f5f35c2 100644 --- a/server/service/testing_utils_test.go +++ b/server/service/testing_utils_test.go @@ -577,6 +577,10 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl } var carveStore fleet.CarveStore = ds // In tests, we use MySQL as storage for carves. apiHandler := MakeHandler(svc, cfg, logger, limitStore, redisPool, carveStore, featureRoutes, extra...) + // SCIM endpoints are served by a prefix-mounted handler (see scim.RegisterSCIM) + // that gorilla/mux can't introspect, so surface their routes to the validator + // explicitly. They're always in the catalog, regardless of opts[0].EnableSCIM. + extraInitFeatureRoutes = append(extraInitFeatureRoutes, scim.RegisterValidationRoutes) if err := apiendpoints.Validate(apiHandler, extraInitFeatureRoutes...); err != nil { t.Fatalf("error initializing API endpoints: %v", err) }