Deprecate URLs with "team" and "query" terminology (#40520)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40519

# Details

This PR adds a new system for registering deprecated URLs separately
from the main URLs (i.e. not clogging up `handler.go` with a bunch of
`.WithAltPaths()` or similar. It uses a registry that's shared between
all the different endpointer, which is then iterated over and a new
handler is created for the deprecated endpoint which stores info about
the deprecation (the old and new URLs) in the context. A new middleware
looks for that context info and, if found, logs a deprecation warning
(if the topic is enabled).

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
no need for a changelog as we are not logging the warnings by default

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually

* Verified that going to `/teams` with
`--logging_enable_topics=deprecated-field-names` got me this log:
```
deprecated_path=/api/_version_/fleet/teams deprecation_warning="API `/api/_version_/fleet/teams` is deprecated, use `/api/_version_/fleet/fleets` instead
```
* Going to `/fleets` with that flag enabled resulted in no deprecation
log
* Going to `/teams` _without_ the flag enabled resulted in no
deprecation log
This commit is contained in:
Scott Gress
2026-02-25 22:20:35 -06:00
committed by GitHub
parent 5b4dc33633
commit 647612345c
5 changed files with 469 additions and 55 deletions
+93 -2
View File
@@ -744,6 +744,87 @@ func WriteBrowserSecurityHeaders(w http.ResponseWriter) {
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
}
// handlerKey identifies a registered handler by HTTP method and unversioned path template.
type handlerKey struct {
method string
path string // unversioned path template, e.g. "/api/_version_/fleet/fleets"
}
// HandlerRegistry stores HTTP handlers by method+path during endpoint registration,
// enabling lookup for deprecated path alias registration.
type HandlerRegistry struct {
handlers map[handlerKey]http.Handler
}
// NewHandlerRegistry creates an empty HandlerRegistry.
func NewHandlerRegistry() *HandlerRegistry {
return &HandlerRegistry{handlers: make(map[handlerKey]http.Handler)}
}
// DeprecatedPathAlias maps a primary (canonical) path to one or more deprecated
// paths that should serve the same handler.
type DeprecatedPathAlias struct {
Method string
PrimaryPath string // canonical path (must already be registered)
DeprecatedPaths []string // old paths to alias
}
// deprecatedPathInfoKey is the context key for deprecated URL path info.
type deprecatedPathInfoKey struct{}
// deprecatedPathInfo holds the deprecated and canonical paths for logging.
type deprecatedPathInfo struct {
deprecatedPath string
primaryPath string
}
// LogDeprecatedPathAlias is a kithttp.RequestFunc (ServerBefore function)
// that checks if the request is using a deprecated URL path alias and, if so,
// elevates the log level to Warn and adds deprecation info to the request log.
// It must run after the LoggingContext is created (i.e. after SetRequestsContexts).
func LogDeprecatedPathAlias(ctx context.Context, _ *http.Request) context.Context {
if !platform_logging.TopicEnabled(platform_logging.DeprecatedFieldTopic) {
return ctx
}
info, ok := ctx.Value(deprecatedPathInfoKey{}).(deprecatedPathInfo)
if !ok {
return ctx
}
logging.WithLevel(ctx, slog.LevelWarn)
logging.WithExtras(ctx,
"deprecated_path", info.deprecatedPath,
"deprecation_warning", fmt.Sprintf("API `%s` is deprecated, use `%s` instead", info.deprecatedPath, info.primaryPath),
)
return ctx
}
// RegisterDeprecatedPathAliases registers deprecated URL path aliases that point
// to the same handler as the canonical path, and wraps them in a handler that
// can log deprecation warnings.
func RegisterDeprecatedPathAliases(r *mux.Router, versions []string, registry *HandlerRegistry, aliases []DeprecatedPathAlias) {
allVersions := append(append([]string{}, versions...), "latest")
versionRegex := strings.Join(allVersions, "|")
for _, alias := range aliases {
handler := registry.handlers[handlerKey{alias.Method, alias.PrimaryPath}]
if handler == nil {
panic(fmt.Sprintf("deprecated alias: no handler registered for %s %s", alias.Method, alias.PrimaryPath))
}
for _, path := range alias.DeprecatedPaths {
// Replace the version placeholder in the deprecated path with a regex that matches all versions,
// so that the same handler can be used for all versions of the deprecated path.
pathForHandler := strings.Replace(path, "/_version_/", fmt.Sprintf("/{fleetversion:(?:%s)}/", versionRegex), 1)
info := deprecatedPathInfo{deprecatedPath: path, primaryPath: alias.PrimaryPath}
// Wrap the handler to inject deprecation info into the context for logging.
wrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), deprecatedPathInfoKey{}, info)
handler.ServeHTTP(w, r.WithContext(ctx))
})
nameAndVerb := getNameFromPathAndVerb(alias.Method, path, "")
r.Handle(pathForHandler, wrappedHandler).Name(nameAndVerb).Methods(alias.Method)
}
}
}
type CommonEndpointer[H any] struct {
EP Endpointer[H]
MakeDecoderFn func(iface any, requestBodyLimit int64) kithttp.DecodeRequestFunc
@@ -760,6 +841,12 @@ type CommonEndpointer[H any] struct {
// CustomMiddlewareAfterAuth are middlewares that run after authentication.
CustomMiddlewareAfterAuth []endpoint.Middleware
// HandlerRegistry, if set, records handlers by method+path for deprecated
// path alias lookup. The pointer is shared across shallow copies (created
// by builder methods like WithAltPaths) so all registrations land in the
// same map.
HandlerRegistry *HandlerRegistry
startingAtVersion string
endingAtVersion string
alternativePaths []string
@@ -971,10 +1058,14 @@ func (e *CommonEndpointer[H]) HandlePathHandler(path string, pathHandler func(pa
versionedPath := strings.Replace(path, "/_version_/", fmt.Sprintf("/{fleetversion:(?:%s)}/", strings.Join(versions, "|")), 1)
nameAndVerb := getNameFromPathAndVerb(verb, path, e.startingAtVersion)
handler := pathHandler(versionedPath)
if e.usePathPrefix {
e.Router.PathPrefix(versionedPath).Handler(pathHandler(versionedPath)).Name(nameAndVerb).Methods(verb)
e.Router.PathPrefix(versionedPath).Handler(handler).Name(nameAndVerb).Methods(verb)
} else {
e.Router.Handle(versionedPath, pathHandler(versionedPath)).Name(nameAndVerb).Methods(verb)
e.Router.Handle(versionedPath, handler).Name(nameAndVerb).Methods(verb)
}
if e.HandlerRegistry != nil {
e.HandlerRegistry.handlers[handlerKey{verb, path}] = handler
}
for _, alias := range e.alternativePaths {
nameAndVerb := getNameFromPathAndVerb(verb, alias, e.startingAtVersion)
@@ -3,11 +3,13 @@ package endpointer
import (
"context"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
platform_http "github.com/fleetdm/fleet/v4/server/platform/http"
"github.com/go-kit/kit/endpoint"
kithttp "github.com/go-kit/kit/transport/http"
@@ -123,3 +125,104 @@ func (n nopEP) CallHandlerFunc(f testHandlerFunc, ctx context.Context, request a
func (n nopEP) Service() any {
return nil
}
func TestRegisterDeprecatedPathAliases(t *testing.T) {
// Set up a router and register a primary endpoint via CommonEndpointer.
r := mux.NewRouter()
registry := NewHandlerRegistry()
versions := []string{"v1", "2022-04"}
authMiddleware := func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req any) (any, error) {
if authctx, ok := authz_ctx.FromContext(ctx); ok {
authctx.SetChecked()
}
return next(ctx, req)
}
}
ce := &CommonEndpointer[testHandlerFunc]{
EP: nopEP{},
MakeDecoderFn: func(iface any, requestBodySizeLimit int64) kithttp.DecodeRequestFunc {
return func(ctx context.Context, r *http.Request) (request any, err error) {
return nopRequest{}, nil
}
},
EncodeFn: func(ctx context.Context, w http.ResponseWriter, i any) error {
w.WriteHeader(http.StatusOK)
return nil
},
AuthMiddleware: authMiddleware,
Router: r,
Versions: versions,
HandlerRegistry: registry,
}
// Register the primary endpoint.
ce.GET("/api/_version_/fleet/fleets", func(ctx context.Context, request any) (platform_http.Errorer, error) {
return nopResponse{}, nil
}, nil)
// Register a deprecated alias for it.
RegisterDeprecatedPathAliases(r, versions, registry, []DeprecatedPathAlias{
{
Method: "GET",
PrimaryPath: "/api/_version_/fleet/fleets",
DeprecatedPaths: []string{"/api/_version_/fleet/teams"},
},
})
s := httptest.NewServer(r)
t.Cleanup(s.Close)
// Both the primary and deprecated paths should return 200.
for _, path := range []string{"/api/v1/fleet/fleets", "/api/v1/fleet/teams", "/api/latest/fleet/teams"} {
resp, err := http.Get(s.URL + path)
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, "path %s should return 200", path)
}
}
func TestLogDeprecatedPathAlias(t *testing.T) {
// Without deprecated path info in context, LogDeprecatedPathAlias is a no-op.
lc := &logging.LoggingContext{}
ctx := logging.NewContext(context.Background(), lc)
ctx2 := LogDeprecatedPathAlias(ctx, nil)
require.Equal(t, ctx, ctx2, "should return same context when no deprecated path info")
require.Empty(t, lc.Extras)
// With deprecated path info, it should set warn level and extras.
ctx = context.WithValue(ctx, deprecatedPathInfoKey{}, deprecatedPathInfo{
deprecatedPath: "/api/_version_/fleet/teams",
primaryPath: "/api/_version_/fleet/fleets",
})
LogDeprecatedPathAlias(ctx, nil)
// Extras is a flat []interface{} of key-value pairs.
require.Len(t, lc.Extras, 4) // "deprecated_path", value, "deprecation_warning", value
require.Equal(t, "deprecated_path", lc.Extras[0])
require.Equal(t, "/api/_version_/fleet/teams", lc.Extras[1])
require.Equal(t, "deprecation_warning", lc.Extras[2])
require.Contains(t, lc.Extras[3], "deprecated")
// ForceLevel should be set to Warn.
require.NotNil(t, lc.ForceLevel)
require.Equal(t, slog.LevelWarn, *lc.ForceLevel)
}
func TestRegisterDeprecatedPathAliasesPanicsOnMissing(t *testing.T) {
r := mux.NewRouter()
registry := NewHandlerRegistry()
versions := []string{"v1"}
require.Panics(t, func() {
RegisterDeprecatedPathAliases(r, versions, registry, []DeprecatedPathAlias{
{
Method: "GET",
PrimaryPath: "/api/_version_/fleet/nonexistent",
DeprecatedPaths: []string{"/api/_version_/fleet/old"},
},
})
})
}
+47 -52
View File
@@ -125,6 +125,7 @@ func MakeHandler(
kithttp.ServerBefore(
kithttp.PopulateRequestContext, // populate the request context with common fields
auth.SetRequestsContexts(svc),
endpointer.LogDeprecatedPathAlias, // log deprecation warning for deprecated URL path aliases
setCarveStoreInRequestContext(carveStore),
),
kithttp.ServerErrorHandler(&endpointer.ErrorHandler{Logger: logger.SlogLogger()}),
@@ -282,9 +283,11 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
extra extraHandlerOpts,
) {
apiVersions := []string{"v1", "2022-04"}
registry := endpointer.NewHandlerRegistry()
// user-authenticated endpoints
ue := newUserAuthenticatedEndpointer(svc, opts, r, apiVersions...)
ue.HandlerRegistry = registry
ue.POST("/api/_version_/fleet/trigger", triggerEndpoint, triggerRequest{})
@@ -301,18 +304,18 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.POST("/api/_version_/fleet/users/roles/spec", applyUserRoleSpecsEndpoint, applyUserRoleSpecsRequest{})
ue.POST("/api/_version_/fleet/translate", translatorEndpoint, translatorRequest{})
ue.WithAltPaths("/api/_version_/fleet/spec/teams").WithRequestBodySizeLimit(5*units.MiB).POST("/api/_version_/fleet/spec/fleets", applyTeamSpecsEndpoint, applyTeamSpecsRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{fleet_id:[0-9]+}/secrets").PATCH("/api/_version_/fleet/fleets/{fleet_id:[0-9]+}/secrets", modifyTeamEnrollSecretsEndpoint, modifyTeamEnrollSecretsRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams").POST("/api/_version_/fleet/fleets", createTeamEndpoint, createTeamRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams").GET("/api/_version_/fleet/fleets", listTeamsEndpoint, listTeamsRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{id:[0-9]+}").GET("/api/_version_/fleet/fleets/{id:[0-9]+}", getTeamEndpoint, getTeamRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{id:[0-9]+}").PATCH("/api/_version_/fleet/fleets/{id:[0-9]+}", modifyTeamEndpoint, modifyTeamRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{id:[0-9]+}").DELETE("/api/_version_/fleet/fleets/{id:[0-9]+}", deleteTeamEndpoint, deleteTeamRequest{})
ue.WithRequestBodySizeLimit(2*units.MiB).WithAltPaths("/api/_version_/fleet/teams/{id:[0-9]+}/agent_options").POST("/api/_version_/fleet/fleets/{id:[0-9]+}/agent_options", modifyTeamAgentOptionsEndpoint, modifyTeamAgentOptionsRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{id:[0-9]+}/users").GET("/api/_version_/fleet/fleets/{id:[0-9]+}/users", listTeamUsersEndpoint, listTeamUsersRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{id:[0-9]+}/users").PATCH("/api/_version_/fleet/fleets/{id:[0-9]+}/users", addTeamUsersEndpoint, modifyTeamUsersRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{id:[0-9]+}/users").DELETE("/api/_version_/fleet/fleets/{id:[0-9]+}/users", deleteTeamUsersEndpoint, modifyTeamUsersRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{id:[0-9]+}/secrets").GET("/api/_version_/fleet/fleets/{id:[0-9]+}/secrets", teamEnrollSecretsEndpoint, teamEnrollSecretsRequest{})
ue.WithRequestBodySizeLimit(5*units.MiB).POST("/api/_version_/fleet/spec/fleets", applyTeamSpecsEndpoint, applyTeamSpecsRequest{})
ue.PATCH("/api/_version_/fleet/fleets/{fleet_id:[0-9]+}/secrets", modifyTeamEnrollSecretsEndpoint, modifyTeamEnrollSecretsRequest{})
ue.POST("/api/_version_/fleet/fleets", createTeamEndpoint, createTeamRequest{})
ue.GET("/api/_version_/fleet/fleets", listTeamsEndpoint, listTeamsRequest{})
ue.GET("/api/_version_/fleet/fleets/{id:[0-9]+}", getTeamEndpoint, getTeamRequest{})
ue.PATCH("/api/_version_/fleet/fleets/{id:[0-9]+}", modifyTeamEndpoint, modifyTeamRequest{})
ue.DELETE("/api/_version_/fleet/fleets/{id:[0-9]+}", deleteTeamEndpoint, deleteTeamRequest{})
ue.WithRequestBodySizeLimit(2*units.MiB).POST("/api/_version_/fleet/fleets/{id:[0-9]+}/agent_options", modifyTeamAgentOptionsEndpoint, modifyTeamAgentOptionsRequest{})
ue.GET("/api/_version_/fleet/fleets/{id:[0-9]+}/users", listTeamUsersEndpoint, listTeamUsersRequest{})
ue.PATCH("/api/_version_/fleet/fleets/{id:[0-9]+}/users", addTeamUsersEndpoint, modifyTeamUsersRequest{})
ue.DELETE("/api/_version_/fleet/fleets/{id:[0-9]+}/users", deleteTeamUsersEndpoint, modifyTeamUsersRequest{})
ue.GET("/api/_version_/fleet/fleets/{id:[0-9]+}/secrets", teamEnrollSecretsEndpoint, teamEnrollSecretsRequest{})
ue.GET("/api/_version_/fleet/users", listUsersEndpoint, listUsersRequest{})
ue.POST("/api/_version_/fleet/users/admin", createUserEndpoint, createUserRequest{})
@@ -347,18 +350,12 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.StartingAtVersion("2022-04").PATCH("/api/_version_/fleet/policies/{policy_id}", modifyGlobalPolicyEndpoint, modifyGlobalPolicyRequest{})
ue.POST("/api/_version_/fleet/automations/reset", resetAutomationEndpoint, resetAutomationRequest{})
// Alias /api/_version_/fleet/team/ -> /api/_version_/fleet/teams/
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/policies", "/api/_version_/fleet/teams/{fleet_id}/policies").
POST("/api/_version_/fleet/fleets/{fleet_id}/policies", teamPolicyEndpoint, teamPolicyRequest{})
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/policies", "/api/_version_/fleet/teams/{fleet_id}/policies").
GET("/api/_version_/fleet/fleets/{fleet_id}/policies", listTeamPoliciesEndpoint, listTeamPoliciesRequest{})
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/policies/count", "/api/_version_/fleet/teams/{fleet_id}/policies/count").
GET("/api/_version_/fleet/fleets/{fleet_id}/policies/count", countTeamPoliciesEndpoint, countTeamPoliciesRequest{})
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/policies/{policy_id}", "/api/_version_/fleet/teams/{fleet_id}/policies/{policy_id}").
GET("/api/_version_/fleet/fleets/{fleet_id}/policies/{policy_id}", getTeamPolicyByIDEndpoint, getTeamPolicyByIDRequest{})
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/policies/delete", "/api/_version_/fleet/teams/{fleet_id}/policies/delete").
POST("/api/_version_/fleet/fleets/{fleet_id}/policies/delete", deleteTeamPoliciesEndpoint, deleteTeamPoliciesRequest{})
ue.WithAltPaths("/api/_version_/fleet/teams/{fleet_id}/policies/{policy_id}").PATCH("/api/_version_/fleet/fleets/{fleet_id}/policies/{policy_id}", modifyTeamPolicyEndpoint, modifyTeamPolicyRequest{})
ue.POST("/api/_version_/fleet/fleets/{fleet_id}/policies", teamPolicyEndpoint, teamPolicyRequest{})
ue.GET("/api/_version_/fleet/fleets/{fleet_id}/policies", listTeamPoliciesEndpoint, listTeamPoliciesRequest{})
ue.GET("/api/_version_/fleet/fleets/{fleet_id}/policies/count", countTeamPoliciesEndpoint, countTeamPoliciesRequest{})
ue.GET("/api/_version_/fleet/fleets/{fleet_id}/policies/{policy_id}", getTeamPolicyByIDEndpoint, getTeamPolicyByIDRequest{})
ue.POST("/api/_version_/fleet/fleets/{fleet_id}/policies/delete", deleteTeamPoliciesEndpoint, deleteTeamPoliciesRequest{})
ue.PATCH("/api/_version_/fleet/fleets/{fleet_id}/policies/{policy_id}", modifyTeamPolicyEndpoint, modifyTeamPolicyRequest{})
ue.WithRequestBodySizeLimit(fleet.MaxSpecSize).POST("/api/_version_/fleet/spec/policies", applyPolicySpecsEndpoint, applyPolicySpecsRequest{})
ue.POST("/api/_version_/fleet/certificates", createCertificateTemplateEndpoint, createCertificateTemplateRequest{})
@@ -368,17 +365,17 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.POST("/api/_version_/fleet/spec/certificates", applyCertificateTemplateSpecsEndpoint, applyCertificateTemplateSpecsRequest{})
ue.DELETE("/api/_version_/fleet/spec/certificates", deleteCertificateTemplateSpecsEndpoint, deleteCertificateTemplateSpecsRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries/{id:[0-9]+}").GET("/api/_version_/fleet/reports/{id:[0-9]+}", getQueryEndpoint, getQueryRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries").GET("/api/_version_/fleet/reports", listQueriesEndpoint, listQueriesRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries/{id:[0-9]+}/report").GET("/api/_version_/fleet/reports/{id:[0-9]+}/report", getQueryReportEndpoint, getQueryReportRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries").POST("/api/_version_/fleet/reports", createQueryEndpoint, createQueryRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries/{id:[0-9]+}").PATCH("/api/_version_/fleet/reports/{id:[0-9]+}", modifyQueryEndpoint, modifyQueryRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries/{name}").DELETE("/api/_version_/fleet/reports/{name}", deleteQueryEndpoint, deleteQueryRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries/id/{id:[0-9]+}").DELETE("/api/_version_/fleet/reports/id/{id:[0-9]+}", deleteQueryByIDEndpoint, deleteQueryByIDRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries/delete").POST("/api/_version_/fleet/reports/delete", deleteQueriesEndpoint, deleteQueriesRequest{})
ue.WithAltPaths("/api/_version_/fleet/spec/queries").WithRequestBodySizeLimit(fleet.MaxSpecSize).POST("/api/_version_/fleet/spec/reports", applyQuerySpecsEndpoint, applyQuerySpecsRequest{})
ue.WithAltPaths("/api/_version_/fleet/spec/queries").GET("/api/_version_/fleet/spec/reports", getQuerySpecsEndpoint, getQuerySpecsRequest{})
ue.WithAltPaths("/api/_version_/fleet/spec/queries/{name}").GET("/api/_version_/fleet/spec/reports/{name}", getQuerySpecEndpoint, getQuerySpecRequest{})
ue.GET("/api/_version_/fleet/reports/{id:[0-9]+}", getQueryEndpoint, getQueryRequest{})
ue.GET("/api/_version_/fleet/reports", listQueriesEndpoint, listQueriesRequest{})
ue.GET("/api/_version_/fleet/reports/{id:[0-9]+}/report", getQueryReportEndpoint, getQueryReportRequest{})
ue.POST("/api/_version_/fleet/reports", createQueryEndpoint, createQueryRequest{})
ue.PATCH("/api/_version_/fleet/reports/{id:[0-9]+}", modifyQueryEndpoint, modifyQueryRequest{})
ue.DELETE("/api/_version_/fleet/reports/{name}", deleteQueryEndpoint, deleteQueryRequest{})
ue.DELETE("/api/_version_/fleet/reports/id/{id:[0-9]+}", deleteQueryByIDEndpoint, deleteQueryByIDRequest{})
ue.POST("/api/_version_/fleet/reports/delete", deleteQueriesEndpoint, deleteQueriesRequest{})
ue.WithRequestBodySizeLimit(fleet.MaxSpecSize).POST("/api/_version_/fleet/spec/reports", applyQuerySpecsEndpoint, applyQuerySpecsRequest{})
ue.GET("/api/_version_/fleet/spec/reports", getQuerySpecsEndpoint, getQuerySpecsRequest{})
ue.GET("/api/_version_/fleet/spec/reports/{name}", getQuerySpecEndpoint, getQuerySpecRequest{})
ue.GET("/api/_version_/fleet/packs/{id:[0-9]+}", getPackEndpoint, getPackRequest{})
ue.POST("/api/_version_/fleet/packs", createPackEndpoint, createPackRequest{})
@@ -475,7 +472,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.GET("/api/_version_/fleet/hosts/report", hostsReportEndpoint, hostsReportRequest{})
ue.GET("/api/_version_/fleet/os_versions", osVersionsEndpoint, osVersionsRequest{})
ue.GET("/api/_version_/fleet/os_versions/{id:[0-9]+}", getOSVersionEndpoint, getOSVersionRequest{})
ue.WithAltPaths("/api/_version_/fleet/hosts/{id:[0-9]+}/queries/{report_id:[0-9]+}").GET("/api/_version_/fleet/hosts/{id:[0-9]+}/reports/{report_id:[0-9]+}", getHostQueryReportEndpoint, getHostQueryReportRequest{})
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/reports/{report_id:[0-9]+}", getHostQueryReportEndpoint, getHostQueryReportRequest{})
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/health", getHostHealthEndpoint, getHostHealthRequest{})
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/labels", addLabelsToHostEndpoint, addLabelsToHostRequest{})
ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/labels", removeLabelsFromHostEndpoint, removeLabelsFromHostRequest{})
@@ -498,16 +495,16 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.GET("/api/_version_/fleet/spec/labels/{name}", getLabelSpecEndpoint, getGenericSpecRequest{})
// This endpoint runs live queries synchronously (with a configured timeout).
ue.WithAltPaths("/api/_version_/fleet/queries/{id:[0-9]+}/run").POST("/api/_version_/fleet/reports/{id:[0-9]+}/run", runOneLiveQueryEndpoint, runOneLiveQueryRequest{})
ue.POST("/api/_version_/fleet/reports/{id:[0-9]+}/run", runOneLiveQueryEndpoint, runOneLiveQueryRequest{})
// Old endpoint, removed from docs. This GET endpoint runs live queries synchronously (with a configured timeout).
ue.WithAltPaths("/api/_version_/fleet/queries/run").GET("/api/_version_/fleet/reports/run", runLiveQueryEndpoint, runLiveQueryRequest{})
ue.GET("/api/_version_/fleet/reports/run", runLiveQueryEndpoint, runLiveQueryRequest{})
// The following two POST APIs are the asynchronous way to run live queries.
// The live queries are created with these two endpoints and their results can be queried via
// websockets via the `GET /api/_version_/fleet/results/` endpoint.
ue.WithAltPaths("/api/_version_/fleet/queries/run").POST("/api/_version_/fleet/reports/run", createDistributedQueryCampaignEndpoint, createDistributedQueryCampaignRequest{})
ue.WithAltPaths("/api/_version_/fleet/queries/run_by_identifiers").POST("/api/_version_/fleet/reports/run_by_identifiers", createDistributedQueryCampaignByIdentifierEndpoint, createDistributedQueryCampaignByIdentifierRequest{})
ue.POST("/api/_version_/fleet/reports/run", createDistributedQueryCampaignEndpoint, createDistributedQueryCampaignRequest{})
ue.POST("/api/_version_/fleet/reports/run_by_identifiers", createDistributedQueryCampaignByIdentifierEndpoint, createDistributedQueryCampaignByIdentifierRequest{})
// This endpoint is deprecated and maintained for backwards compatibility. This and above endpoint are functionally equivalent
ue.WithAltPaths("/api/_version_/fleet/queries/run_by_names").POST("/api/_version_/fleet/reports/run_by_names", createDistributedQueryCampaignByIdentifierEndpoint, createDistributedQueryCampaignByIdentifierRequest{})
ue.POST("/api/_version_/fleet/reports/run_by_names", createDistributedQueryCampaignByIdentifierEndpoint, createDistributedQueryCampaignByIdentifierRequest{})
ue.GET("/api/_version_/fleet/packs/{id:[0-9]+}/scheduled", getScheduledQueriesInPackEndpoint, getScheduledQueriesInPackRequest{})
ue.EndingAtVersion("v1").POST("/api/_version_/fleet/schedule", scheduleQueryEndpoint, scheduleQueryRequest{})
@@ -527,15 +524,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.EndingAtVersion("v1").DELETE("/api/_version_/fleet/global/schedule/{id:[0-9]+}", deleteGlobalScheduleEndpoint, deleteGlobalScheduleRequest{})
ue.StartingAtVersion("2022-04").DELETE("/api/_version_/fleet/schedule/{id:[0-9]+}", deleteGlobalScheduleEndpoint, deleteGlobalScheduleRequest{})
// Alias /api/_version_/fleet/team/ -> /api/_version_/fleet/teams/
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/schedule", "/api/_version_/fleet/teams/{fleet_id}/schedule").
GET("/api/_version_/fleet/fleets/{fleet_id}/schedule", getTeamScheduleEndpoint, getTeamScheduleRequest{})
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/schedule", "/api/_version_/fleet/teams/{fleet_id}/schedule").
POST("/api/_version_/fleet/fleets/{fleet_id}/schedule", teamScheduleQueryEndpoint, teamScheduleQueryRequest{})
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/schedule/{report_id}", "/api/_version_/fleet/teams/{fleet_id}/schedule/{report_id}").
PATCH("/api/_version_/fleet/fleets/{fleet_id}/schedule/{report_id}", modifyTeamScheduleEndpoint, modifyTeamScheduleRequest{})
ue.WithAltPaths("/api/_version_/fleet/team/{fleet_id}/schedule/{report_id}", "/api/_version_/fleet/teams/{fleet_id}/schedule/{report_id}").
DELETE("/api/_version_/fleet/fleets/{fleet_id}/schedule/{report_id}", deleteTeamScheduleEndpoint, deleteTeamScheduleRequest{})
ue.GET("/api/_version_/fleet/fleets/{fleet_id}/schedule", getTeamScheduleEndpoint, getTeamScheduleRequest{})
ue.POST("/api/_version_/fleet/fleets/{fleet_id}/schedule", teamScheduleQueryEndpoint, teamScheduleQueryRequest{})
ue.PATCH("/api/_version_/fleet/fleets/{fleet_id}/schedule/{report_id}", modifyTeamScheduleEndpoint, modifyTeamScheduleRequest{})
ue.DELETE("/api/_version_/fleet/fleets/{fleet_id}/schedule/{report_id}", deleteTeamScheduleEndpoint, deleteTeamScheduleRequest{})
ue.GET("/api/_version_/fleet/carves", listCarvesEndpoint, listCarvesRequest{})
ue.GET("/api/_version_/fleet/carves/{id:[0-9]+}", getCarveEndpoint, getCarveRequest{})
@@ -829,7 +821,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.DELETE("/api/_version_/fleet/abm_tokens/{id:[0-9]+}", deleteABMTokenEndpoint, deleteABMTokenRequest{})
ue.GET("/api/_version_/fleet/abm_tokens", listABMTokensEndpoint, nil)
ue.GET("/api/_version_/fleet/abm_tokens/count", countABMTokensEndpoint, nil)
ue.WithAltPaths("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/teams").PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/fleets", updateABMTokenTeamsEndpoint, updateABMTokenTeamsRequest{})
ue.PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/fleets", updateABMTokenTeamsEndpoint, updateABMTokenTeamsRequest{})
ue.PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/renew", renewABMTokenEndpoint, renewABMTokenRequest{})
ue.GET("/api/_version_/fleet/mdm/apple/request_csr", getMDMAppleCSREndpoint, getMDMAppleCSRRequest{})
@@ -839,7 +831,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// VPP Tokens
ue.GET("/api/_version_/fleet/vpp_tokens", getVPPTokens, getVPPTokensRequest{})
ue.POST("/api/_version_/fleet/vpp_tokens", uploadVPPTokenEndpoint, uploadVPPTokenRequest{})
ue.WithAltPaths("/api/_version_/fleet/vpp_tokens/{id}/teams").PATCH("/api/_version_/fleet/vpp_tokens/{id}/fleets", patchVPPTokensTeams, patchVPPTokensTeamsRequest{})
ue.PATCH("/api/_version_/fleet/vpp_tokens/{id}/fleets", patchVPPTokensTeams, patchVPPTokensTeamsRequest{})
ue.PATCH("/api/_version_/fleet/vpp_tokens/{id}/renew", patchVPPTokenRenewEndpoint, patchVPPTokenRenewRequest{})
ue.DELETE("/api/_version_/fleet/vpp_tokens/{id}", deleteVPPToken, deleteVPPTokenRequest{})
@@ -1103,6 +1095,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
POST("/api/_version_/fleet/mdm/sso", initiateMDMSSOEndpoint, initiateMDMSSORequest{})
ne.WithCustomMiddleware(mdmSsoLimiter).
POST("/api/_version_/fleet/mdm/sso/callback", callbackMDMSSOEndpoint, callbackMDMSSORequest{})
// Register all deprecated URL path aliases from the declarative table.
endpointer.RegisterDeprecatedPathAliases(r, apiVersions, registry, deprecatedPathAliases)
}
// WithSetup is an http middleware that checks if setup procedures have been completed.
+223
View File
@@ -0,0 +1,223 @@
package service
import (
eu "github.com/fleetdm/fleet/v4/server/platform/endpointer"
)
// deprecatedPathAliases defines deprecated URL path aliases that map old
// (deprecated) paths to their canonical (primary) paths. Each entry causes
// the deprecated path(s) to serve the same handler as the primary path.
//
// These are organized by category:
// - teams → fleets: team CRUD, secrets, agent_options, users, spec
// - team/teams → fleets: policies, schedule (both singular and plural deprecated)
// - queries → reports: query CRUD, spec, report data
// - host queries → reports
// - live queries → reports: run, run_by_identifiers, run_by_names
// - ABM/VPP token teams → fleets
var deprecatedPathAliases = []eu.DeprecatedPathAlias{
// ---- teams → fleets ----
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/spec/fleets",
DeprecatedPaths: []string{"/api/_version_/fleet/spec/teams"},
},
{
Method: "PATCH", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id:[0-9]+}/secrets",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{fleet_id:[0-9]+}/secrets"},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/fleets",
DeprecatedPaths: []string{"/api/_version_/fleet/teams"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/fleets",
DeprecatedPaths: []string{"/api/_version_/fleet/teams"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/fleets/{id:[0-9]+}",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{id:[0-9]+}"},
},
{
Method: "PATCH", PrimaryPath: "/api/_version_/fleet/fleets/{id:[0-9]+}",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{id:[0-9]+}"},
},
{
Method: "DELETE", PrimaryPath: "/api/_version_/fleet/fleets/{id:[0-9]+}",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{id:[0-9]+}"},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/fleets/{id:[0-9]+}/agent_options",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{id:[0-9]+}/agent_options"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/fleets/{id:[0-9]+}/users",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{id:[0-9]+}/users"},
},
{
Method: "PATCH", PrimaryPath: "/api/_version_/fleet/fleets/{id:[0-9]+}/users",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{id:[0-9]+}/users"},
},
{
Method: "DELETE", PrimaryPath: "/api/_version_/fleet/fleets/{id:[0-9]+}/users",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{id:[0-9]+}/users"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/fleets/{id:[0-9]+}/secrets",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{id:[0-9]+}/secrets"},
},
// ---- team/teams → fleets (policies) ----
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/policies",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/policies",
"/api/_version_/fleet/teams/{fleet_id}/policies",
},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/policies",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/policies",
"/api/_version_/fleet/teams/{fleet_id}/policies",
},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/policies/count",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/policies/count",
"/api/_version_/fleet/teams/{fleet_id}/policies/count",
},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/policies/{policy_id}",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/policies/{policy_id}",
"/api/_version_/fleet/teams/{fleet_id}/policies/{policy_id}",
},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/policies/delete",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/policies/delete",
"/api/_version_/fleet/teams/{fleet_id}/policies/delete",
},
},
{
Method: "PATCH", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/policies/{policy_id}",
DeprecatedPaths: []string{"/api/_version_/fleet/teams/{fleet_id}/policies/{policy_id}"},
},
// ---- queries → reports ----
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/reports/{id:[0-9]+}",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/{id:[0-9]+}"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/reports",
DeprecatedPaths: []string{"/api/_version_/fleet/queries"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/reports/{id:[0-9]+}/report",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/{id:[0-9]+}/report"},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/reports",
DeprecatedPaths: []string{"/api/_version_/fleet/queries"},
},
{
Method: "PATCH", PrimaryPath: "/api/_version_/fleet/reports/{id:[0-9]+}",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/{id:[0-9]+}"},
},
{
Method: "DELETE", PrimaryPath: "/api/_version_/fleet/reports/{name}",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/{name}"},
},
{
Method: "DELETE", PrimaryPath: "/api/_version_/fleet/reports/id/{id:[0-9]+}",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/id/{id:[0-9]+}"},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/reports/delete",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/delete"},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/spec/reports",
DeprecatedPaths: []string{"/api/_version_/fleet/spec/queries"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/spec/reports",
DeprecatedPaths: []string{"/api/_version_/fleet/spec/queries"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/spec/reports/{name}",
DeprecatedPaths: []string{"/api/_version_/fleet/spec/queries/{name}"},
},
// ---- host queries → reports ----
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/hosts/{id:[0-9]+}/reports/{report_id:[0-9]+}",
DeprecatedPaths: []string{"/api/_version_/fleet/hosts/{id:[0-9]+}/queries/{report_id:[0-9]+}"},
},
// ---- live queries → reports ----
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/reports/{id:[0-9]+}/run",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/{id:[0-9]+}/run"},
},
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/reports/run",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/run"},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/reports/run",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/run"},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/reports/run_by_identifiers",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/run_by_identifiers"},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/reports/run_by_names",
DeprecatedPaths: []string{"/api/_version_/fleet/queries/run_by_names"},
},
// ---- team/teams → fleets (schedule) ----
{
Method: "GET", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/schedule",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/schedule",
"/api/_version_/fleet/teams/{fleet_id}/schedule",
},
},
{
Method: "POST", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/schedule",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/schedule",
"/api/_version_/fleet/teams/{fleet_id}/schedule",
},
},
{
Method: "PATCH", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/schedule/{report_id}",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/schedule/{report_id}",
"/api/_version_/fleet/teams/{fleet_id}/schedule/{report_id}",
},
},
{
Method: "DELETE", PrimaryPath: "/api/_version_/fleet/fleets/{fleet_id}/schedule/{report_id}",
DeprecatedPaths: []string{
"/api/_version_/fleet/team/{fleet_id}/schedule/{report_id}",
"/api/_version_/fleet/teams/{fleet_id}/schedule/{report_id}",
},
},
// ---- ABM/VPP token teams → fleets ----
{
Method: "PATCH", PrimaryPath: "/api/_version_/fleet/abm_tokens/{id:[0-9]+}/fleets",
DeprecatedPaths: []string{"/api/_version_/fleet/abm_tokens/{id:[0-9]+}/teams"},
},
{
Method: "PATCH", PrimaryPath: "/api/_version_/fleet/vpp_tokens/{id}/fleets",
DeprecatedPaths: []string{"/api/_version_/fleet/vpp_tokens/{id}/teams"},
},
}
+3 -1
View File
@@ -95,12 +95,14 @@ func (s *integrationLoggerTestSuite) TestLogger() {
assert.Equal(t, "/api/latest/fleet/config", attrs["uri"])
assert.Equal(t, "admin1@example.com", attrs["user"])
case 2:
assert.Equal(t, slog.LevelDebug, rec.Level)
assert.Equal(t, slog.LevelWarn, rec.Level) // Warn because /queries is a deprecated path
assert.Equal(t, "POST", attrs["method"])
assert.Equal(t, "/api/latest/fleet/queries", attrs["uri"])
assert.Equal(t, "admin1@example.com", attrs["user"])
assert.Equal(t, "somequery", attrs["name"])
assert.Equal(t, "select 1 from osquery;", attrs["sql"])
assert.Equal(t, "/api/_version_/fleet/queries", attrs["deprecated_path"])
assert.Contains(t, attrs["deprecation_warning"], "deprecated")
default:
t.Fail()
}