mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #42368 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] 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. For the overall story - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
1457 lines
92 KiB
Go
1457 lines
92 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/ee/server/service/scep"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
carvestorectx "github.com/fleetdm/fleet/v4/server/contexts/carvestore"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/publicip"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/redis"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
mdmcrypto "github.com/fleetdm/fleet/v4/server/mdm/crypto"
|
|
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/cryptoutil"
|
|
httpmdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/http/mdm"
|
|
nanomdm_service "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service/certauth"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service/multi"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service/nanomdm"
|
|
scep_depot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
|
|
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
|
|
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
|
|
"github.com/fleetdm/fleet/v4/server/platform/middleware/ratelimit"
|
|
"github.com/fleetdm/fleet/v4/server/service/contract"
|
|
"github.com/fleetdm/fleet/v4/server/service/middleware/auth"
|
|
"github.com/fleetdm/fleet/v4/server/service/middleware/log"
|
|
"github.com/fleetdm/fleet/v4/server/service/middleware/mdmconfigured"
|
|
"github.com/fleetdm/fleet/v4/server/service/middleware/otel"
|
|
|
|
"github.com/docker/go-units"
|
|
kithttp "github.com/go-kit/kit/transport/http"
|
|
"github.com/gorilla/mux"
|
|
"github.com/klauspost/compress/gzhttp"
|
|
nanomdm_log "github.com/micromdm/nanolib/log"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"github.com/throttled/throttled/v2"
|
|
"go.elastic.co/apm/module/apmgorilla/v2"
|
|
otmiddleware "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
|
|
)
|
|
|
|
func checkLicenseExpiration(svc fleet.Service) func(context.Context, http.ResponseWriter) context.Context {
|
|
return func(ctx context.Context, w http.ResponseWriter) context.Context {
|
|
license, err := svc.License(ctx)
|
|
if err != nil || license == nil {
|
|
return ctx
|
|
}
|
|
if license.IsPremium() && license.IsExpired() {
|
|
w.Header().Set(fleet.HeaderLicenseKey, fleet.HeaderLicenseValueExpired)
|
|
}
|
|
return ctx
|
|
}
|
|
}
|
|
|
|
type extraHandlerOpts struct {
|
|
loginRateLimit *throttled.Rate
|
|
mdmSsoRateLimit *throttled.Rate
|
|
httpSigVerifier mux.MiddlewareFunc
|
|
}
|
|
|
|
// ExtraHandlerOption allows adding extra configuration to the HTTP handler.
|
|
type ExtraHandlerOption func(*extraHandlerOpts)
|
|
|
|
// WithLoginRateLimit configures the rate limit for the login endpoints.
|
|
func WithLoginRateLimit(r throttled.Rate) ExtraHandlerOption {
|
|
return func(o *extraHandlerOpts) {
|
|
o.loginRateLimit = &r
|
|
}
|
|
}
|
|
|
|
// WithMdmSsoRateLimit configures the rate limit for the MDM SSO endpoints (falls back to login rate limit otherwise).
|
|
func WithMdmSsoRateLimit(r throttled.Rate) ExtraHandlerOption {
|
|
return func(o *extraHandlerOpts) {
|
|
o.mdmSsoRateLimit = &r
|
|
}
|
|
}
|
|
|
|
func WithHTTPSigVerifier(m mux.MiddlewareFunc) ExtraHandlerOption {
|
|
return func(o *extraHandlerOpts) {
|
|
o.httpSigVerifier = m
|
|
}
|
|
}
|
|
|
|
func setCarveStoreInRequestContext(carveStore fleet.CarveStore) kithttp.RequestFunc {
|
|
return func(ctx context.Context, r *http.Request) context.Context {
|
|
ctx = carvestorectx.NewContext(ctx, carveStore)
|
|
return ctx
|
|
}
|
|
}
|
|
|
|
// MakeHandler creates an HTTP handler for the Fleet server endpoints.
|
|
func MakeHandler(
|
|
svc fleet.Service,
|
|
config config.FleetConfig,
|
|
logger *slog.Logger,
|
|
limitStore throttled.GCRAStore,
|
|
redisPool fleet.RedisPool,
|
|
carveStore fleet.CarveStore,
|
|
featureRoutes []endpointer.HandlerRoutesFunc,
|
|
extra ...ExtraHandlerOption,
|
|
) http.Handler {
|
|
var eopts extraHandlerOpts
|
|
for _, fn := range extra {
|
|
fn(&eopts)
|
|
}
|
|
|
|
// Create the client IP extraction strategy based on config.
|
|
ipStrategy, err := endpointer.NewClientIPStrategy(config.Server.TrustedProxies)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("invalid server.trusted_proxies configuration: %v", err))
|
|
}
|
|
|
|
fleetAPIOptions := []kithttp.ServerOption{
|
|
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}),
|
|
kithttp.ServerErrorEncoder(fleetErrorEncoder),
|
|
kithttp.ServerAfter(
|
|
kithttp.SetContentType("application/json; charset=utf-8"),
|
|
log.LogRequestEnd(logger),
|
|
checkLicenseExpiration(svc),
|
|
),
|
|
}
|
|
|
|
r := mux.NewRouter()
|
|
if config.Logging.TracingEnabled {
|
|
if config.OTELEnabled() {
|
|
r.Use(otmiddleware.Middleware(
|
|
"service",
|
|
otmiddleware.WithSpanNameFormatter(func(route string, r *http.Request) string {
|
|
// Use the guideline for span names: {method} {target}
|
|
// See https://opentelemetry.io/docs/specs/semconv/http/http-spans/
|
|
return r.Method + " " + route
|
|
})))
|
|
} else {
|
|
apmgorilla.Instrument(r)
|
|
}
|
|
}
|
|
|
|
if config.Server.GzipResponses {
|
|
r.Use(func(h http.Handler) http.Handler {
|
|
return gzhttp.GzipHandler(h)
|
|
})
|
|
}
|
|
|
|
// Add middleware to extract the client IP and set it in the request context.
|
|
r.Use(func(handler http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ip := ipStrategy.ClientIP(r.Header, r.RemoteAddr)
|
|
if ip != "" {
|
|
r.RemoteAddr = ip
|
|
}
|
|
handler.ServeHTTP(w, r.WithContext(publicip.NewContext(r.Context(), ip)))
|
|
})
|
|
})
|
|
|
|
if eopts.httpSigVerifier != nil {
|
|
r.Use(eopts.httpSigVerifier)
|
|
}
|
|
|
|
attachFleetAPIRoutes(r, svc, config, logger, limitStore, redisPool, fleetAPIOptions, eopts)
|
|
for _, featureRoute := range featureRoutes {
|
|
featureRoute(r, fleetAPIOptions)
|
|
}
|
|
addMetrics(r)
|
|
|
|
return r
|
|
}
|
|
|
|
// PrometheusMetricsHandler wraps the provided handler with prometheus metrics
|
|
// middleware and returns the resulting handler that should be mounted for that
|
|
// route.
|
|
func PrometheusMetricsHandler(name string, handler http.Handler) http.Handler {
|
|
reg := prometheus.DefaultRegisterer
|
|
registerOrExisting := func(coll prometheus.Collector) prometheus.Collector {
|
|
if err := reg.Register(coll); err != nil {
|
|
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
|
|
return are.ExistingCollector
|
|
}
|
|
panic(err)
|
|
}
|
|
return coll
|
|
}
|
|
|
|
// this configuration is to keep prometheus metrics as close as possible to
|
|
// what the v0.9.3 (that we used to use) provided via the now-deprecated
|
|
// prometheus.InstrumentHandler.
|
|
|
|
reqCnt := registerOrExisting(prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Subsystem: "http",
|
|
Name: "requests_total",
|
|
Help: "Total number of HTTP requests made.",
|
|
ConstLabels: prometheus.Labels{"handler": name},
|
|
},
|
|
[]string{"method", "code"},
|
|
)).(*prometheus.CounterVec)
|
|
|
|
reqDur := registerOrExisting(prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Subsystem: "http",
|
|
Name: "request_duration_seconds",
|
|
Help: "The HTTP request latencies in seconds.",
|
|
ConstLabels: prometheus.Labels{"handler": name},
|
|
// Use default buckets, as they are suited for durations.
|
|
},
|
|
nil,
|
|
)).(*prometheus.HistogramVec)
|
|
|
|
// 1KB, 100KB, 1MB, 100MB, 1GB
|
|
sizeBuckets := []float64{1024, 100 * 1024, 1024 * 1024, 100 * 1024 * 1024, 1024 * 1024 * 1024}
|
|
|
|
resSz := registerOrExisting(prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Subsystem: "http",
|
|
Name: "response_size_bytes",
|
|
Help: "The HTTP response sizes in bytes.",
|
|
ConstLabels: prometheus.Labels{"handler": name},
|
|
Buckets: sizeBuckets,
|
|
},
|
|
nil,
|
|
)).(*prometheus.HistogramVec)
|
|
|
|
reqSz := registerOrExisting(prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Subsystem: "http",
|
|
Name: "request_size_bytes",
|
|
Help: "The HTTP request sizes in bytes.",
|
|
ConstLabels: prometheus.Labels{"handler": name},
|
|
Buckets: sizeBuckets,
|
|
},
|
|
nil,
|
|
)).(*prometheus.HistogramVec)
|
|
|
|
return promhttp.InstrumentHandlerDuration(reqDur,
|
|
promhttp.InstrumentHandlerCounter(reqCnt,
|
|
promhttp.InstrumentHandlerResponseSize(resSz,
|
|
promhttp.InstrumentHandlerRequestSize(reqSz, handler))))
|
|
}
|
|
|
|
// addMetrics decorates each handler with prometheus instrumentation
|
|
func addMetrics(r *mux.Router) {
|
|
walkFn := func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
|
route.Handler(PrometheusMetricsHandler(route.GetName(), route.GetHandler()))
|
|
return nil
|
|
}
|
|
r.Walk(walkFn) //nolint:errcheck
|
|
}
|
|
|
|
// These are defined as const so that they can be used in tests.
|
|
const (
|
|
forgotPasswordRateLimitMaxBurst = 9 // Max burst used for rate limiting on the the forgot_password endpoint.
|
|
|
|
// Fleet Desktop API endpoints rate limiting:
|
|
//
|
|
// Allow up to 1_000 consecutive failing requests per minute.
|
|
// If the threshold of 1_000 consecutive failures is reached for an IP,
|
|
// ban requests from such IP for a duration of 1 minute.
|
|
//
|
|
|
|
deviceIPAllowedConsecutiveFailingRequestsCount = 1_000
|
|
deviceIPAllowedConsecutiveFailingRequestsTimeWindow = 1 * time.Minute
|
|
deviceIPBanTime = 1 * time.Minute
|
|
)
|
|
|
|
func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetConfig,
|
|
logger *slog.Logger, limitStore throttled.GCRAStore, redisPool fleet.RedisPool, opts []kithttp.ServerOption,
|
|
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{})
|
|
|
|
ue.GET("/api/_version_/fleet/me", meEndpoint, getMeRequest{})
|
|
ue.GET("/api/_version_/fleet/sessions/{id:[0-9]+}", getInfoAboutSessionEndpoint, getInfoAboutSessionRequest{})
|
|
ue.DELETE("/api/_version_/fleet/sessions/{id:[0-9]+}", deleteSessionEndpoint, deleteSessionRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/config/certificate", getCertificateEndpoint, nil)
|
|
ue.GET("/api/_version_/fleet/config", getAppConfigEndpoint, nil)
|
|
ue.PATCH("/api/_version_/fleet/config", modifyAppConfigEndpoint, modifyAppConfigRequest{})
|
|
ue.POST("/api/_version_/fleet/spec/enroll_secret", applyEnrollSecretSpecEndpoint, applyEnrollSecretSpecRequest{})
|
|
ue.GET("/api/_version_/fleet/spec/enroll_secret", getEnrollSecretSpecEndpoint, nil)
|
|
ue.GET("/api/_version_/fleet/version", versionEndpoint, nil)
|
|
|
|
ue.POST("/api/_version_/fleet/users/roles/spec", applyUserRoleSpecsEndpoint, applyUserRoleSpecsRequest{})
|
|
ue.POST("/api/_version_/fleet/translate", translatorEndpoint, translatorRequest{})
|
|
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{})
|
|
ue.GET("/api/_version_/fleet/users/{id:[0-9]+}", getUserEndpoint, getUserRequest{})
|
|
ue.PATCH("/api/_version_/fleet/users/{id:[0-9]+}", modifyUserEndpoint, modifyUserRequest{})
|
|
ue.DELETE("/api/_version_/fleet/users/{id:[0-9]+}", deleteUserEndpoint, deleteUserRequest{})
|
|
ue.POST("/api/_version_/fleet/users/{id:[0-9]+}/require_password_reset", requirePasswordResetEndpoint, requirePasswordResetRequest{})
|
|
ue.GET("/api/_version_/fleet/users/{id:[0-9]+}/sessions", getInfoAboutSessionsForUserEndpoint, getInfoAboutSessionsForUserRequest{})
|
|
ue.DELETE("/api/_version_/fleet/users/{id:[0-9]+}/sessions", deleteSessionsForUserEndpoint, deleteSessionsForUserRequest{})
|
|
ue.POST("/api/_version_/fleet/change_password", changePasswordEndpoint, changePasswordRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/email/change/{token}", changeEmailEndpoint, changeEmailRequest{})
|
|
// TODO: searchTargetsEndpoint will be removed in Fleet 5.0
|
|
ue.POST("/api/_version_/fleet/targets", searchTargetsEndpoint, searchTargetsRequest{})
|
|
ue.POST("/api/_version_/fleet/targets/count", countTargetsEndpoint, countTargetsRequest{})
|
|
|
|
ue.POST("/api/_version_/fleet/invites", createInviteEndpoint, createInviteRequest{})
|
|
ue.GET("/api/_version_/fleet/invites", listInvitesEndpoint, listInvitesRequest{})
|
|
ue.DELETE("/api/_version_/fleet/invites/{id:[0-9]+}", deleteInviteEndpoint, deleteInviteRequest{})
|
|
ue.PATCH("/api/_version_/fleet/invites/{id:[0-9]+}", updateInviteEndpoint, updateInviteRequest{})
|
|
|
|
ue.EndingAtVersion("v1").POST("/api/_version_/fleet/global/policies", globalPolicyEndpoint, fleet.GlobalPolicyRequest{})
|
|
ue.StartingAtVersion("2022-04").POST("/api/_version_/fleet/policies", globalPolicyEndpoint, fleet.GlobalPolicyRequest{})
|
|
ue.EndingAtVersion("v1").GET("/api/_version_/fleet/global/policies", listGlobalPoliciesEndpoint, fleet.ListGlobalPoliciesRequest{})
|
|
ue.StartingAtVersion("2022-04").GET("/api/_version_/fleet/policies", listGlobalPoliciesEndpoint, fleet.ListGlobalPoliciesRequest{})
|
|
ue.GET("/api/_version_/fleet/policies/count", countGlobalPoliciesEndpoint, fleet.CountGlobalPoliciesRequest{})
|
|
ue.EndingAtVersion("v1").GET("/api/_version_/fleet/global/policies/{policy_id}", getPolicyByIDEndpoint, fleet.GetPolicyByIDRequest{})
|
|
ue.StartingAtVersion("2022-04").GET("/api/_version_/fleet/policies/{policy_id}", getPolicyByIDEndpoint, fleet.GetPolicyByIDRequest{})
|
|
ue.EndingAtVersion("v1").POST("/api/_version_/fleet/global/policies/delete", deleteGlobalPoliciesEndpoint, fleet.DeleteGlobalPoliciesRequest{})
|
|
ue.StartingAtVersion("2022-04").POST("/api/_version_/fleet/policies/delete", deleteGlobalPoliciesEndpoint, fleet.DeleteGlobalPoliciesRequest{})
|
|
ue.EndingAtVersion("v1").PATCH("/api/_version_/fleet/global/policies/{policy_id}", modifyGlobalPolicyEndpoint, fleet.ModifyGlobalPolicyRequest{})
|
|
ue.StartingAtVersion("2022-04").PATCH("/api/_version_/fleet/policies/{policy_id}", modifyGlobalPolicyEndpoint, fleet.ModifyGlobalPolicyRequest{})
|
|
ue.POST("/api/_version_/fleet/automations/reset", resetAutomationEndpoint, fleet.ResetAutomationRequest{})
|
|
|
|
ue.POST("/api/_version_/fleet/fleets/{fleet_id}/policies", teamPolicyEndpoint, fleet.TeamPolicyRequest{})
|
|
ue.GET("/api/_version_/fleet/fleets/{fleet_id}/policies", listTeamPoliciesEndpoint, fleet.ListTeamPoliciesRequest{})
|
|
ue.GET("/api/_version_/fleet/fleets/{fleet_id}/policies/count", countTeamPoliciesEndpoint, fleet.CountTeamPoliciesRequest{})
|
|
ue.GET("/api/_version_/fleet/fleets/{fleet_id}/policies/{policy_id}", getTeamPolicyByIDEndpoint, fleet.GetTeamPolicyByIDRequest{})
|
|
ue.POST("/api/_version_/fleet/fleets/{fleet_id}/policies/delete", deleteTeamPoliciesEndpoint, fleet.DeleteTeamPoliciesRequest{})
|
|
ue.PATCH("/api/_version_/fleet/fleets/{fleet_id}/policies/{policy_id}", modifyTeamPolicyEndpoint, fleet.ModifyTeamPolicyRequest{})
|
|
ue.WithRequestBodySizeLimit(fleet.MaxSpecSize).POST("/api/_version_/fleet/spec/policies", applyPolicySpecsEndpoint, fleet.ApplyPolicySpecsRequest{})
|
|
|
|
ue.POST("/api/_version_/fleet/certificates", createCertificateTemplateEndpoint, createCertificateTemplateRequest{})
|
|
ue.GET("/api/_version_/fleet/certificates", listCertificateTemplatesEndpoint, listCertificateTemplatesRequest{})
|
|
ue.GET("/api/_version_/fleet/certificates/{id:[0-9]+}", getCertificateTemplateEndpoint, getCertificateTemplateRequest{})
|
|
ue.DELETE("/api/_version_/fleet/certificates/{id:[0-9]+}", deleteCertificateTemplateEndpoint, deleteCertificateTemplateRequest{})
|
|
ue.POST("/api/_version_/fleet/spec/certificates", applyCertificateTemplateSpecsEndpoint, applyCertificateTemplateSpecsRequest{})
|
|
ue.DELETE("/api/_version_/fleet/spec/certificates", deleteCertificateTemplateSpecsEndpoint, deleteCertificateTemplateSpecsRequest{})
|
|
|
|
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{})
|
|
ue.PATCH("/api/_version_/fleet/packs/{id:[0-9]+}", modifyPackEndpoint, modifyPackRequest{})
|
|
ue.GET("/api/_version_/fleet/packs", listPacksEndpoint, listPacksRequest{})
|
|
ue.DELETE("/api/_version_/fleet/packs/{name}", deletePackEndpoint, deletePackRequest{})
|
|
ue.DELETE("/api/_version_/fleet/packs/id/{id:[0-9]+}", deletePackByIDEndpoint, deletePackByIDRequest{})
|
|
ue.WithRequestBodySizeLimit(fleet.MaxSpecSize).POST("/api/_version_/fleet/spec/packs", applyPackSpecsEndpoint, applyPackSpecsRequest{})
|
|
ue.GET("/api/_version_/fleet/spec/packs", getPackSpecsEndpoint, nil)
|
|
ue.GET("/api/_version_/fleet/spec/packs/{name}", getPackSpecEndpoint, getGenericSpecRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/software/versions", listSoftwareVersionsEndpoint, listSoftwareRequest{})
|
|
ue.GET("/api/_version_/fleet/software/versions/{id:[0-9]+}", getSoftwareEndpoint, getSoftwareRequest{})
|
|
|
|
// DEPRECATED: use /api/_version_/fleet/software/versions instead
|
|
ue.GET("/api/_version_/fleet/software", listSoftwareEndpoint, listSoftwareRequest{})
|
|
// DEPRECATED: use /api/_version_/fleet/software/versions{id:[0-9]+} instead
|
|
ue.GET("/api/_version_/fleet/software/{id:[0-9]+}", getSoftwareEndpoint, getSoftwareRequest{})
|
|
// DEPRECATED: software version counts are now included directly in the software version list
|
|
ue.GET("/api/_version_/fleet/software/count", countSoftwareEndpoint, countSoftwareRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/software/titles", listSoftwareTitlesEndpoint, listSoftwareTitlesRequest{})
|
|
ue.GET("/api/_version_/fleet/software/titles/{id:[0-9]+}", getSoftwareTitleEndpoint, getSoftwareTitleRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/software/{software_title_id:[0-9]+}/install", installSoftwareTitleEndpoint,
|
|
installSoftwareRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/software/{software_title_id:[0-9]+}/uninstall", uninstallSoftwareTitleEndpoint,
|
|
uninstallSoftwareRequest{})
|
|
|
|
// Software installers
|
|
ue.GET("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/package", getSoftwareInstallerEndpoint, getSoftwareInstallerRequest{})
|
|
ue.POST("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/package/token", getSoftwareInstallerTokenEndpoint,
|
|
getSoftwareInstallerRequest{})
|
|
// Software package endpoints are already limited to max installer size in serve.go
|
|
ue.SkipRequestBodySizeLimit().POST("/api/_version_/fleet/software/package", uploadSoftwareInstallerEndpoint, uploadSoftwareInstallerRequest{})
|
|
ue.PATCH("/api/_version_/fleet/software/titles/{id:[0-9]+}/name", updateSoftwareNameEndpoint, updateSoftwareNameRequest{})
|
|
// Software package endpoints are already limited to max installer size in serve.go
|
|
ue.SkipRequestBodySizeLimit().PATCH("/api/_version_/fleet/software/titles/{id:[0-9]+}/package", updateSoftwareInstallerEndpoint, updateSoftwareInstallerRequest{})
|
|
ue.DELETE("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/available_for_install", deleteSoftwareInstallerEndpoint, deleteSoftwareInstallerRequest{})
|
|
ue.GET("/api/_version_/fleet/software/install/{install_uuid}/results", getSoftwareInstallResultsEndpoint,
|
|
getSoftwareInstallResultsRequest{})
|
|
// POST /api/_version_/fleet/software/batch is asynchronous, meaning it will start the process of software download+upload in the background
|
|
// and will return a request UUID to be used in GET /api/_version_/fleet/software/batch/{request_uuid} to query for the status of the operation.
|
|
ue.WithRequestBodySizeLimit(fleet.MaxSoftwareBatchSize).POST("/api/_version_/fleet/software/batch", batchSetSoftwareInstallersEndpoint, batchSetSoftwareInstallersRequest{})
|
|
ue.GET("/api/_version_/fleet/software/batch/{request_uuid}", batchSetSoftwareInstallersResultEndpoint, batchSetSoftwareInstallersResultRequest{})
|
|
|
|
// software title custom icons
|
|
ue.GET("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/icon", getSoftwareTitleIconsEndpoint, getSoftwareTitleIconsRequest{})
|
|
ue.PUT("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/icon", putSoftwareTitleIconEndpoint, putSoftwareTitleIconRequest{})
|
|
ue.DELETE("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/icon", deleteSoftwareTitleIconEndpoint, deleteSoftwareTitleIconRequest{})
|
|
|
|
// App store software
|
|
ue.GET("/api/_version_/fleet/software/app_store_apps", getAppStoreAppsEndpoint, getAppStoreAppsRequest{})
|
|
ue.POST("/api/_version_/fleet/software/app_store_apps", addAppStoreAppEndpoint, addAppStoreAppRequest{})
|
|
ue.PATCH("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/app_store_app", updateAppStoreAppEndpoint, updateAppStoreAppRequest{})
|
|
|
|
// Setup Experience
|
|
//
|
|
// Setup experience software endpoints:
|
|
ue.PUT("/api/_version_/fleet/setup_experience/software", putSetupExperienceSoftware, putSetupExperienceSoftwareRequest{})
|
|
ue.GET("/api/_version_/fleet/setup_experience/software", getSetupExperienceSoftware, getSetupExperienceSoftwareRequest{})
|
|
|
|
// Setup experience script endpoints:
|
|
ue.GET("/api/_version_/fleet/setup_experience/script", getSetupExperienceScriptEndpoint, getSetupExperienceScriptRequest{})
|
|
ue.WithRequestBodySizeLimit(fleet.MaxScriptSize).POST("/api/_version_/fleet/setup_experience/script", setSetupExperienceScriptEndpoint, setSetupExperienceScriptRequest{})
|
|
ue.DELETE("/api/_version_/fleet/setup_experience/script", deleteSetupExperienceScriptEndpoint, deleteSetupExperienceScriptRequest{})
|
|
|
|
// Fleet-maintained apps
|
|
ue.WithRequestBodySizeLimit(fleet.MaxMultiScriptQuerySize).POST("/api/_version_/fleet/software/fleet_maintained_apps", addFleetMaintainedAppEndpoint, addFleetMaintainedAppRequest{})
|
|
ue.GET("/api/_version_/fleet/software/fleet_maintained_apps", listFleetMaintainedAppsEndpoint, listFleetMaintainedAppsRequest{})
|
|
ue.GET("/api/_version_/fleet/software/fleet_maintained_apps/{app_id}", getFleetMaintainedApp, getFleetMaintainedAppRequest{})
|
|
|
|
// Vulnerabilities
|
|
ue.GET("/api/_version_/fleet/vulnerabilities", listVulnerabilitiesEndpoint, listVulnerabilitiesRequest{})
|
|
ue.GET("/api/_version_/fleet/vulnerabilities/{cve}", getVulnerabilityEndpoint, getVulnerabilityRequest{})
|
|
|
|
// Hosts
|
|
ue.GET("/api/_version_/fleet/host_summary", getHostSummaryEndpoint, getHostSummaryRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts", listHostsEndpoint, listHostsRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/delete", deleteHostsEndpoint, deleteHostsRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}", getHostEndpoint, getHostRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts/count", countHostsEndpoint, countHostsRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/search", searchHostsEndpoint, searchHostsRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts/identifier/{identifier}", hostByIdentifierEndpoint, hostByIdentifierRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/identifier/{identifier}/query", runLiveQueryOnHostEndpoint, runLiveQueryOnHostRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/query", runLiveQueryOnHostByIDEndpoint, runLiveQueryOnHostByIDRequest{})
|
|
ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}", deleteHostEndpoint, deleteHostRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/transfer", addHostsToTeamEndpoint, addHostsToTeamRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/transfer/filter", addHostsToTeamByFilterEndpoint, addHostsToTeamByFilterRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/refetch", refetchHostEndpoint, refetchHostRequest{})
|
|
// Deprecated: Device mappings are included in the host details endpoint: /api/_version_/fleet/hosts/{id}
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/device_mapping", listHostDeviceMappingEndpoint, listHostDeviceMappingRequest{})
|
|
ue.PUT("/api/_version_/fleet/hosts/{id:[0-9]+}/device_mapping", putHostDeviceMappingEndpoint, putHostDeviceMappingRequest{})
|
|
ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/device_mapping/idp", deleteHostIDPEndpoint, deleteHostIDPRequest{})
|
|
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.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/reports/{report_id:[0-9]+}", getHostQueryReportEndpoint, getHostQueryReportRequest{})
|
|
ue.WithAltPaths("/api/_version_/fleet/hosts/{id:[0-9]+}/queries").GET("/api/_version_/fleet/hosts/{id:[0-9]+}/reports", listHostReportsEndpoint, listHostReportsRequest{})
|
|
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{})
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/software", getHostSoftwareEndpoint, getHostSoftwareRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/certificates", listHostCertificatesEndpoint, listHostCertificatesRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/certificates/{template_id:[0-9]+}/resend", resendHostCertificateTemplateEndpoint, resendHostCertificateTemplateRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/recovery_lock_password", getHostRecoveryLockPasswordEndpoint, getHostRecoveryLockPasswordRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/hosts/summary/mdm", getHostMDMSummary, getHostMDMSummaryRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/mdm", getHostMDM, getHostMDMRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/dep_assignment", getHostDEPAssignmentEndpoint, getHostDEPAssignmentRequest{})
|
|
|
|
ue.POST("/api/_version_/fleet/labels", createLabelEndpoint, createLabelRequest{})
|
|
ue.PATCH("/api/_version_/fleet/labels/{id:[0-9]+}", modifyLabelEndpoint, modifyLabelRequest{})
|
|
ue.GET("/api/_version_/fleet/labels/{id:[0-9]+}", getLabelEndpoint, getLabelRequest{})
|
|
ue.GET("/api/_version_/fleet/labels", listLabelsEndpoint, listLabelsRequest{})
|
|
ue.GET("/api/_version_/fleet/labels/summary", getLabelsSummaryEndpoint, getLabelsSummaryRequest{})
|
|
ue.GET("/api/_version_/fleet/labels/{id:[0-9]+}/hosts", listHostsInLabelEndpoint, listHostsInLabelRequest{})
|
|
ue.DELETE("/api/_version_/fleet/labels/{name}", deleteLabelEndpoint, deleteLabelRequest{})
|
|
ue.DELETE("/api/_version_/fleet/labels/id/{id:[0-9]+}", deleteLabelByIDEndpoint, deleteLabelByIDRequest{})
|
|
ue.WithRequestBodySizeLimit(fleet.MaxSpecSize).POST("/api/_version_/fleet/spec/labels", applyLabelSpecsEndpoint, applyLabelSpecsRequest{})
|
|
ue.GET("/api/_version_/fleet/spec/labels", getLabelSpecsEndpoint, getLabelSpecsRequest{})
|
|
ue.GET("/api/_version_/fleet/spec/labels/{name}", getLabelSpecEndpoint, getGenericSpecRequest{})
|
|
|
|
// This endpoint runs live queries synchronously (with a configured timeout).
|
|
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.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.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.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{})
|
|
ue.StartingAtVersion("2022-04").POST("/api/_version_/fleet/packs/schedule", scheduleQueryEndpoint, scheduleQueryRequest{})
|
|
ue.GET("/api/_version_/fleet/schedule/{id:[0-9]+}", getScheduledQueryEndpoint, getScheduledQueryRequest{})
|
|
ue.EndingAtVersion("v1").PATCH("/api/_version_/fleet/schedule/{id:[0-9]+}", modifyScheduledQueryEndpoint, modifyScheduledQueryRequest{})
|
|
ue.StartingAtVersion("2022-04").PATCH("/api/_version_/fleet/packs/schedule/{id:[0-9]+}", modifyScheduledQueryEndpoint, modifyScheduledQueryRequest{})
|
|
ue.EndingAtVersion("v1").DELETE("/api/_version_/fleet/schedule/{id:[0-9]+}", deleteScheduledQueryEndpoint, deleteScheduledQueryRequest{})
|
|
ue.StartingAtVersion("2022-04").DELETE("/api/_version_/fleet/packs/schedule/{id:[0-9]+}", deleteScheduledQueryEndpoint, deleteScheduledQueryRequest{})
|
|
|
|
ue.EndingAtVersion("v1").GET("/api/_version_/fleet/global/schedule", getGlobalScheduleEndpoint, getGlobalScheduleRequest{})
|
|
ue.StartingAtVersion("2022-04").GET("/api/_version_/fleet/schedule", getGlobalScheduleEndpoint, getGlobalScheduleRequest{})
|
|
ue.EndingAtVersion("v1").POST("/api/_version_/fleet/global/schedule", globalScheduleQueryEndpoint, globalScheduleQueryRequest{})
|
|
ue.StartingAtVersion("2022-04").POST("/api/_version_/fleet/schedule", globalScheduleQueryEndpoint, globalScheduleQueryRequest{})
|
|
ue.EndingAtVersion("v1").PATCH("/api/_version_/fleet/global/schedule/{id:[0-9]+}", modifyGlobalScheduleEndpoint, modifyGlobalScheduleRequest{})
|
|
ue.StartingAtVersion("2022-04").PATCH("/api/_version_/fleet/schedule/{id:[0-9]+}", modifyGlobalScheduleEndpoint, modifyGlobalScheduleRequest{})
|
|
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{})
|
|
|
|
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{})
|
|
ue.GET("/api/_version_/fleet/carves/{id:[0-9]+}/block/{block_id}", getCarveBlockEndpoint, getCarveBlockRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/macadmins", getMacadminsDataEndpoint, getMacadminsDataRequest{})
|
|
ue.GET("/api/_version_/fleet/macadmins", getAggregatedMacadminsDataEndpoint, getAggregatedMacadminsDataRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/status/result_store", statusResultStoreEndpoint, nil)
|
|
ue.GET("/api/_version_/fleet/status/live_query", statusLiveQueryEndpoint, nil)
|
|
|
|
ue.WithRequestBodySizeLimit(fleet.MaxScriptSize).POST("/api/_version_/fleet/scripts/run", runScriptEndpoint, runScriptRequest{})
|
|
ue.WithRequestBodySizeLimit(fleet.MaxScriptSize).POST("/api/_version_/fleet/scripts/run/sync", runScriptSyncEndpoint, runScriptSyncRequest{})
|
|
ue.POST("/api/_version_/fleet/scripts/run/batch", batchScriptRunEndpoint, batchScriptRunRequest{})
|
|
ue.GET("/api/_version_/fleet/scripts/results/{execution_id}", getScriptResultEndpoint, getScriptResultRequest{})
|
|
ue.WithRequestBodySizeLimit(fleet.MaxScriptSize).POST("/api/_version_/fleet/scripts", createScriptEndpoint, createScriptRequest{})
|
|
ue.GET("/api/_version_/fleet/scripts", listScriptsEndpoint, listScriptsRequest{})
|
|
ue.GET("/api/_version_/fleet/scripts/{script_id:[0-9]+}", getScriptEndpoint, getScriptRequest{})
|
|
ue.WithRequestBodySizeLimit(fleet.MaxScriptSize).PATCH("/api/_version_/fleet/scripts/{script_id:[0-9]+}", updateScriptEndpoint, updateScriptRequest{})
|
|
ue.DELETE("/api/_version_/fleet/scripts/{script_id:[0-9]+}", deleteScriptEndpoint, deleteScriptRequest{})
|
|
ue.WithRequestBodySizeLimit(fleet.MaxBatchScriptSize).POST("/api/_version_/fleet/scripts/batch", batchSetScriptsEndpoint, batchSetScriptsRequest{})
|
|
ue.POST("/api/_version_/fleet/scripts/batch/{batch_execution_id:[a-zA-Z0-9-]+}/cancel", batchScriptCancelEndpoint, batchScriptCancelRequest{})
|
|
// Deprecated, will remove in favor of batchScriptExecutionStatusEndpoint when batch script details page is ready.
|
|
ue.GET("/api/_version_/fleet/scripts/batch/summary/{batch_execution_id:[a-zA-Z0-9-]+}", batchScriptExecutionSummaryEndpoint, batchScriptExecutionSummaryRequest{})
|
|
ue.WithAltPaths("/api/_version_/fleet/scripts/batch/{batch_execution_id:[a-zA-Z0-9-]+}/host-results"). // .../host-results is DEPRECATED but we need to maintain for backwards compatibility because customers may already be using it
|
|
GET("/api/_version_/fleet/scripts/batch/{batch_execution_id:[a-zA-Z0-9-]+}/host_results", batchScriptExecutionHostResultsEndpoint, batchScriptExecutionHostResultsRequest{})
|
|
ue.GET("/api/_version_/fleet/scripts/batch/{batch_execution_id:[a-zA-Z0-9-]+}", batchScriptExecutionStatusEndpoint, batchScriptExecutionStatusRequest{})
|
|
ue.GET("/api/_version_/fleet/scripts/batch", batchScriptExecutionListEndpoint, batchScriptExecutionListRequest{})
|
|
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/scripts", getHostScriptDetailsEndpoint, getHostScriptDetailsRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/activities/upcoming", listHostUpcomingActivitiesEndpoint, listHostUpcomingActivitiesRequest{})
|
|
ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/activities/upcoming/{activity_id}", cancelHostUpcomingActivityEndpoint, cancelHostUpcomingActivityRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/lock", lockHostEndpoint, lockHostRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/unlock", unlockHostEndpoint, unlockHostRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/wipe", wipeHostEndpoint, wipeHostRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/clear_passcode", clearPasscodeEndpoint, clearPasscodeRequest{})
|
|
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/recovery_lock_password/rotate", rotateRecoveryLockPasswordEndpoint, rotateRecoveryLockPasswordRequest{})
|
|
|
|
// Generative AI
|
|
ue.POST("/api/_version_/fleet/autofill/policy", autofillPoliciesEndpoint, fleet.AutofillPoliciesRequest{})
|
|
|
|
// Secret variables
|
|
ue.PUT("/api/_version_/fleet/spec/secret_variables", createSecretVariablesEndpoint, createSecretVariablesRequest{})
|
|
ue.POST("/api/_version_/fleet/custom_variables", createSecretVariableEndpoint, createSecretVariableRequest{})
|
|
ue.GET("/api/_version_/fleet/custom_variables", listSecretVariablesEndpoint, listSecretVariablesRequest{})
|
|
ue.DELETE("/api/_version_/fleet/custom_variables/{id:[0-9]+}", deleteSecretVariableEndpoint, deleteSecretVariableRequest{})
|
|
|
|
// Scim details
|
|
ue.GET("/api/_version_/fleet/scim/details", getScimDetailsEndpoint, nil)
|
|
|
|
// Microsoft Compliance Partner
|
|
ue.POST("/api/_version_/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateEndpoint, conditionalAccessMicrosoftCreateRequest{})
|
|
ue.POST("/api/_version_/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmEndpoint, conditionalAccessMicrosoftConfirmRequest{})
|
|
ue.DELETE("/api/_version_/fleet/conditional-access/microsoft", conditionalAccessMicrosoftDeleteEndpoint, conditionalAccessMicrosoftDeleteRequest{})
|
|
|
|
// Okta Conditional Access
|
|
ue.GET("/api/_version_/fleet/conditional_access/idp/signing_cert", conditionalAccessGetIdPSigningCertEndpoint, conditionalAccessGetIdPSigningCertRequest{})
|
|
ue.GET("/api/_version_/fleet/conditional_access/idp/apple/profile", conditionalAccessGetIdPAppleProfileEndpoint, nil)
|
|
|
|
// Deprecated: PATCH /mdm/apple/setup is now deprecated, replaced by the
|
|
// PATCH /setup_experience endpoint.
|
|
ue.PATCH("/api/_version_/fleet/mdm/apple/setup", updateMDMAppleSetupEndpoint, updateMDMAppleSetupRequest{})
|
|
ue.PATCH("/api/_version_/fleet/setup_experience", updateMDMAppleSetupEndpoint, updateMDMAppleSetupRequest{})
|
|
|
|
// Only Fleet MDM specific endpoints should be within the root /mdm/ path.
|
|
// NOTE: remember to update
|
|
// `service.mdmConfigurationRequiredEndpoints` when you add an
|
|
// endpoint that's behind the mdmConfiguredMiddleware, this applies
|
|
// both to this set of endpoints and to any public/token-authenticated
|
|
// endpoints using `neMDM` below in this file.
|
|
mdmConfiguredMiddleware := mdmconfigured.NewMDMConfigMiddleware(svc)
|
|
mdmAppleMW := ue.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
|
|
|
|
// Deprecated: POST /mdm/apple/enqueue is now deprecated, replaced by the
|
|
// platform-agnostic POST /mdm/commands/run. It is still supported
|
|
// indefinitely for backwards compatibility.
|
|
mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/enqueue", enqueueMDMAppleCommandEndpoint, enqueueMDMAppleCommandRequest{})
|
|
// Deprecated: POST /mdm/apple/commandresults is now deprecated, replaced by the
|
|
// platform-agnostic POST /mdm/commands/commandresults. It is still supported
|
|
// indefinitely for backwards compatibility.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/commandresults", getMDMAppleCommandResultsEndpoint, getMDMAppleCommandResultsRequest{})
|
|
// Deprecated: POST /mdm/apple/commands is now deprecated, replaced by the
|
|
// platform-agnostic POST /mdm/commands/commands. It is still supported
|
|
// indefinitely for backwards compatibility.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/commands", listMDMAppleCommandsEndpoint, listMDMAppleCommandsRequest{})
|
|
// Deprecated: those /mdm/apple/profiles/... endpoints are now deprecated,
|
|
// replaced by the platform-agnostic /mdm/profiles/... It is still supported
|
|
// indefinitely for backwards compatibility.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", getMDMAppleConfigProfileEndpoint, getMDMAppleConfigProfileRequest{})
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", deleteMDMAppleConfigProfileEndpoint, deleteMDMAppleConfigProfileRequest{})
|
|
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxProfileSize).POST("/api/_version_/fleet/mdm/apple/profiles", newMDMAppleConfigProfileEndpoint, newMDMAppleConfigProfileRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesEndpoint, listMDMAppleConfigProfilesRequest{})
|
|
|
|
// Deprecated: GET /mdm/apple/filevault/summary is now deprecated, replaced by the
|
|
// platform-agnostic GET /mdm/disk_encryption/summary. It is still supported indefinitely
|
|
// for backwards compatibility.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/filevault/summary", getMdmAppleFileVaultSummaryEndpoint, getMDMAppleFileVaultSummaryRequest{})
|
|
|
|
// Deprecated: GET /mdm/apple/profiles/summary is now deprecated, replaced by the
|
|
// platform-agnostic GET /mdm/profiles/summary. It is still supported indefinitely
|
|
// for backwards compatibility.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles/summary", getMDMAppleProfilesSummaryEndpoint, getMDMAppleProfilesSummaryRequest{})
|
|
|
|
// Deprecated: POST /mdm/apple/enrollment_profile is now deprecated, replaced by the
|
|
// POST /enrollment_profiles/automatic endpoint.
|
|
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxProfileSize).POST("/api/_version_/fleet/mdm/apple/enrollment_profile", createMDMAppleSetupAssistantEndpoint, createMDMAppleSetupAssistantRequest{})
|
|
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxProfileSize).POST("/api/_version_/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantEndpoint, createMDMAppleSetupAssistantRequest{})
|
|
|
|
// Deprecated: GET /mdm/apple/enrollment_profile is now deprecated, replaced by the
|
|
// GET /enrollment_profiles/automatic endpoint.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/enrollment_profile", getMDMAppleSetupAssistantEndpoint, getMDMAppleSetupAssistantRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/enrollment_profiles/automatic", getMDMAppleSetupAssistantEndpoint, getMDMAppleSetupAssistantRequest{})
|
|
|
|
// Deprecated: DELETE /mdm/apple/enrollment_profile is now deprecated, replaced by the
|
|
// DELETE /enrollment_profiles/automatic endpoint.
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/enrollment_profile", deleteMDMAppleSetupAssistantEndpoint, deleteMDMAppleSetupAssistantRequest{})
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/enrollment_profiles/automatic", deleteMDMAppleSetupAssistantEndpoint, deleteMDMAppleSetupAssistantRequest{})
|
|
|
|
// TODO: are those undocumented endpoints still needed? I think they were only used
|
|
// by 'fleetctl apple-mdm' sub-commands.
|
|
// Generous limit for these unknown old unused endpoints-
|
|
mdmAppleMW.WithRequestBodySizeLimit(512*units.MiB).POST("/api/_version_/fleet/mdm/apple/installers", uploadAppleInstallerEndpoint, uploadAppleInstallerRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", getAppleInstallerEndpoint, getAppleInstallerDetailsRequest{})
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/installers/{installer_id:[0-9]+}", deleteAppleInstallerEndpoint, deleteAppleInstallerDetailsRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/installers", listMDMAppleInstallersEndpoint, listMDMAppleInstallersRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{})
|
|
|
|
// Deprecated: GET /mdm/manual_enrollment_profile is now deprecated, replaced by the
|
|
// GET /enrollment_profiles/manual endpoint.
|
|
// Ref: https://github.com/fleetdm/fleet/issues/16252
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/manual_enrollment_profile", getManualEnrollmentProfileEndpoint, getManualEnrollmentProfileRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/enrollment_profiles/manual", getManualEnrollmentProfileEndpoint, getManualEnrollmentProfileRequest{})
|
|
|
|
// bootstrap-package routes
|
|
|
|
// Deprecated: POST /mdm/bootstrap is now deprecated, replaced by the
|
|
// POST /bootstrap endpoint.
|
|
// Bootstrap endpoints are already max size limited to installer size in serve.go
|
|
mdmAppleMW.SkipRequestBodySizeLimit().POST("/api/_version_/fleet/mdm/bootstrap", uploadBootstrapPackageEndpoint, uploadBootstrapPackageRequest{})
|
|
mdmAppleMW.SkipRequestBodySizeLimit().POST("/api/_version_/fleet/bootstrap", uploadBootstrapPackageEndpoint, uploadBootstrapPackageRequest{})
|
|
|
|
// Deprecated: GET /mdm/bootstrap/:team_id/metadata is now deprecated, replaced by the
|
|
// GET /bootstrap/:team_id/metadata endpoint.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/bootstrap/{fleet_id:[0-9]+}/metadata", bootstrapPackageMetadataEndpoint, bootstrapPackageMetadataRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/bootstrap/{fleet_id:[0-9]+}/metadata", bootstrapPackageMetadataEndpoint, bootstrapPackageMetadataRequest{})
|
|
|
|
// Deprecated: DELETE /mdm/bootstrap/:team_id is now deprecated, replaced by the
|
|
// DELETE /bootstrap/:team_id endpoint.
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/bootstrap/{fleet_id:[0-9]+}", deleteBootstrapPackageEndpoint, deleteBootstrapPackageRequest{})
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/bootstrap/{fleet_id:[0-9]+}", deleteBootstrapPackageEndpoint, deleteBootstrapPackageRequest{})
|
|
|
|
// Deprecated: GET /mdm/bootstrap/summary is now deprecated, replaced by the
|
|
// GET /bootstrap/summary endpoint.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/bootstrap/summary", getMDMAppleBootstrapPackageSummaryEndpoint, getMDMAppleBootstrapPackageSummaryRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/bootstrap/summary", getMDMAppleBootstrapPackageSummaryEndpoint, getMDMAppleBootstrapPackageSummaryRequest{})
|
|
|
|
// Deprecated: POST /mdm/apple/bootstrap is now deprecated, replaced by the platform agnostic /mdm/bootstrap
|
|
// Bootstrap endpoints are already max size limited to installer size in serve.go
|
|
mdmAppleMW.SkipRequestBodySizeLimit().POST("/api/_version_/fleet/mdm/apple/bootstrap", uploadBootstrapPackageEndpoint, uploadBootstrapPackageRequest{})
|
|
// Deprecated: GET /mdm/apple/bootstrap/:team_id/metadata is now deprecated, replaced by the platform agnostic /mdm/bootstrap/:team_id/metadata
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/bootstrap/{fleet_id:[0-9]+}/metadata", bootstrapPackageMetadataEndpoint, bootstrapPackageMetadataRequest{})
|
|
// Deprecated: DELETE /mdm/apple/bootstrap/:team_id is now deprecated, replaced by the platform agnostic /mdm/bootstrap/:team_id
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/bootstrap/{fleet_id:[0-9]+}", deleteBootstrapPackageEndpoint, deleteBootstrapPackageRequest{})
|
|
// Deprecated: GET /mdm/apple/bootstrap/summary is now deprecated, replaced by the platform agnostic /mdm/bootstrap/summary
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/bootstrap/summary", getMDMAppleBootstrapPackageSummaryEndpoint, getMDMAppleBootstrapPackageSummaryRequest{})
|
|
|
|
// host-specific mdm routes
|
|
|
|
// Deprecated: POST /mdm/hosts/:id/lock is now deprecated, replaced by
|
|
// POST /hosts/:id/lock.
|
|
mdmAppleMW.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/lock", deviceLockEndpoint, deviceLockRequest{})
|
|
mdmAppleMW.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/wipe", deviceWipeEndpoint, deviceWipeRequest{})
|
|
|
|
// Deprecated: GET /mdm/hosts/:id/profiles is now deprecated, replaced by
|
|
// GET /hosts/:id/configuration_profiles.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/profiles", getHostProfilesEndpoint, getHostProfilesRequest{})
|
|
// TODO: Confirm if response should be updated to include Windows profiles and use mdmAnyMW
|
|
mdmAppleMW.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/configuration_profiles", getHostProfilesEndpoint, getHostProfilesRequest{})
|
|
|
|
// Deprecated: GET /mdm/apple is now deprecated, replaced by the
|
|
// GET /apns endpoint.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil)
|
|
mdmAppleMW.GET("/api/_version_/fleet/apns", getAppleMDMEndpoint, nil)
|
|
|
|
// EULA routes
|
|
|
|
// Deprecated: POST /mdm/setup/eula is now deprecated, replaced by the
|
|
// POST /setup_experience/eula endpoint.
|
|
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxEULASize).POST("/api/_version_/fleet/mdm/setup/eula", createMDMEULAEndpoint, createMDMEULARequest{})
|
|
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxEULASize).POST("/api/_version_/fleet/setup_experience/eula", createMDMEULAEndpoint, createMDMEULARequest{})
|
|
|
|
// Deprecated: GET /mdm/setup/eula/metadata is now deprecated, replaced by the
|
|
// GET /setup_experience/eula/metadata endpoint.
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/setup/eula/metadata", getMDMEULAMetadataEndpoint, getMDMEULAMetadataRequest{})
|
|
mdmAppleMW.GET("/api/_version_/fleet/setup_experience/eula/metadata", getMDMEULAMetadataEndpoint, getMDMEULAMetadataRequest{})
|
|
|
|
// Deprecated: DELETE /mdm/setup/eula/:token is now deprecated, replaced by the
|
|
// DELETE /setup_experience/eula/:token endpoint.
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/setup/eula/{token}", deleteMDMEULAEndpoint, deleteMDMEULARequest{})
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/setup_experience/eula/{token}", deleteMDMEULAEndpoint, deleteMDMEULARequest{})
|
|
|
|
// Deprecated: POST /mdm/apple/setup/eula is now deprecated, replaced by the platform agnostic /mdm/setup/eula
|
|
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxEULASize).POST("/api/_version_/fleet/mdm/apple/setup/eula", createMDMEULAEndpoint, createMDMEULARequest{})
|
|
// Deprecated: GET /mdm/apple/setup/eula/metadata is now deprecated, replaced by the platform agnostic /mdm/setup/eula/metadata
|
|
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/setup/eula/metadata", getMDMEULAMetadataEndpoint, getMDMEULAMetadataRequest{})
|
|
// Deprecated: DELETE /mdm/apple/setup/eula/:token is now deprecated, replaced by the platform agnostic /mdm/setup/eula/:token
|
|
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/setup/eula/{token}", deleteMDMEULAEndpoint, deleteMDMEULARequest{})
|
|
|
|
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxProfileSize).POST("/api/_version_/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileEndpoint, preassignMDMAppleProfileRequest{})
|
|
mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentEndpoint, matchMDMApplePreassignmentRequest{})
|
|
|
|
mdmAnyMW := ue.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAnyMDM())
|
|
|
|
// Deprecated: POST /mdm/commands/run is now deprecated, replaced by the
|
|
// POST /commands/run endpoint.
|
|
mdmAnyMW.WithRequestBodySizeLimit(fleet.MaxMDMCommandSize).POST("/api/_version_/fleet/mdm/commands/run", runMDMCommandEndpoint, runMDMCommandRequest{})
|
|
mdmAnyMW.WithRequestBodySizeLimit(fleet.MaxMDMCommandSize).POST("/api/_version_/fleet/commands/run", runMDMCommandEndpoint, runMDMCommandRequest{})
|
|
|
|
// Deprecated: GET /mdm/commandresults is now deprecated, replaced by the
|
|
// GET /commands/results endpoint.
|
|
mdmAnyMW.GET("/api/_version_/fleet/mdm/commandresults", getMDMCommandResultsEndpoint, getMDMCommandResultsRequest{})
|
|
mdmAnyMW.GET("/api/_version_/fleet/commands/results", getMDMCommandResultsEndpoint, getMDMCommandResultsRequest{})
|
|
|
|
// Deprecated: GET /mdm/commands is now deprecated, replaced by the
|
|
// GET /commands endpoint.
|
|
mdmAnyMW.GET("/api/_version_/fleet/mdm/commands", listMDMCommandsEndpoint, listMDMCommandsRequest{})
|
|
mdmAnyMW.GET("/api/_version_/fleet/commands", listMDMCommandsEndpoint, listMDMCommandsRequest{})
|
|
|
|
// Deprecated: PATCH /mdm/hosts/:id/unenroll is now deprecated, replaced by
|
|
// DELETE /hosts/:id/mdm.
|
|
mdmAnyMW.PATCH("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/unenroll", mdmUnenrollEndpoint, mdmUnenrollRequest{})
|
|
mdmAnyMW.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/mdm", mdmUnenrollEndpoint, mdmUnenrollRequest{})
|
|
|
|
// Deprecated: GET /mdm/disk_encryption/summary is now deprecated, replaced by the
|
|
// GET /disk_encryption endpoint.
|
|
ue.GET("/api/_version_/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{})
|
|
ue.GET("/api/_version_/fleet/disk_encryption", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{})
|
|
|
|
// Deprecated: GET /mdm/hosts/:id/encryption_key is now deprecated, replaced by
|
|
// GET /hosts/:id/encryption_key.
|
|
ue.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
|
|
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
|
|
|
|
// Deprecated: GET /mdm/profiles/summary is now deprecated, replaced by the
|
|
// GET /configuration_profiles/summary endpoint.
|
|
ue.GET("/api/_version_/fleet/mdm/profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{})
|
|
ue.GET("/api/_version_/fleet/configuration_profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{})
|
|
|
|
// Deprecated: GET /mdm/profiles/:profile_uuid is now deprecated, replaced by
|
|
// GET /configuration_profiles/:profile_uuid.
|
|
mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles/{profile_uuid}", getMDMConfigProfileEndpoint, getMDMConfigProfileRequest{})
|
|
mdmAnyMW.GET("/api/_version_/fleet/configuration_profiles/{profile_uuid}", getMDMConfigProfileEndpoint, getMDMConfigProfileRequest{})
|
|
|
|
// Deprecated: DELETE /mdm/profiles/:profile_uuid is now deprecated, replaced by
|
|
// DELETE /configuration_profiles/:profile_uuid.
|
|
ue.DELETE("/api/_version_/fleet/mdm/profiles/{profile_uuid}", deleteMDMConfigProfileEndpoint, deleteMDMConfigProfileRequest{})
|
|
ue.DELETE("/api/_version_/fleet/configuration_profiles/{profile_uuid}", deleteMDMConfigProfileEndpoint, deleteMDMConfigProfileRequest{})
|
|
|
|
// Deprecated: GET /mdm/profiles is now deprecated, replaced by the
|
|
// GET /configuration_profiles endpoint.
|
|
mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles", listMDMConfigProfilesEndpoint, listMDMConfigProfilesRequest{})
|
|
mdmAnyMW.GET("/api/_version_/fleet/configuration_profiles", listMDMConfigProfilesEndpoint, listMDMConfigProfilesRequest{})
|
|
|
|
// Deprecated: POST /mdm/profiles is now deprecated, replaced by the
|
|
// POST /configuration_profiles endpoint.
|
|
mdmAnyMW.WithRequestBodySizeLimit(fleet.MaxProfileSize).POST("/api/_version_/fleet/mdm/profiles", newMDMConfigProfileEndpoint, newMDMConfigProfileRequest{})
|
|
mdmAnyMW.WithRequestBodySizeLimit(fleet.MaxProfileSize).POST("/api/_version_/fleet/configuration_profiles", newMDMConfigProfileEndpoint, newMDMConfigProfileRequest{})
|
|
// Batch needs to allow being called without any MDM enabled, to support deleting profiles, but will fail later if trying to add
|
|
ue.WithRequestBodySizeLimit(fleet.MaxBatchProfileSize).POST("/api/_version_/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesEndpoint, batchModifyMDMConfigProfilesRequest{})
|
|
|
|
// Deprecated: POST /hosts/{host_id:[0-9]+}/configuration_profiles/resend/{profile_uuid} is now deprecated, replaced by the
|
|
// POST /hosts/{host_id:[0-9]+}/configuration_profiles/{profile_uuid}/resend endpoint.
|
|
mdmAnyMW.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/configuration_profiles/resend/{profile_uuid}", resendHostMDMProfileEndpoint, resendHostMDMProfileRequest{})
|
|
mdmAnyMW.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/configuration_profiles/{profile_uuid}/resend", resendHostMDMProfileEndpoint, resendHostMDMProfileRequest{})
|
|
mdmAnyMW.POST("/api/_version_/fleet/configuration_profiles/resend/batch", batchResendMDMProfileToHostsEndpoint, batchResendMDMProfileToHostsRequest{})
|
|
mdmAnyMW.GET("/api/_version_/fleet/configuration_profiles/{profile_uuid}/status", getMDMConfigProfileStatusEndpoint, getMDMConfigProfileStatusRequest{})
|
|
|
|
// Deprecated: PATCH /mdm/apple/settings is deprecated, replaced by POST /disk_encryption.
|
|
// It was only used to set disk encryption.
|
|
mdmAnyMW.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{})
|
|
ue.POST("/api/_version_/fleet/disk_encryption", updateDiskEncryptionEndpoint, updateDiskEncryptionRequest{})
|
|
|
|
// the following set of mdm endpoints must always be accessible (even
|
|
// if MDM is not configured) as it bootstraps the setup of MDM
|
|
// (generates CSR request for APNs, plus the SCEP and ABM keypairs).
|
|
// Deprecated: this endpoint shouldn't be used anymore in favor of the
|
|
// new flow described in https://github.com/fleetdm/fleet/issues/10383
|
|
ue.POST("/api/_version_/fleet/mdm/apple/request_csr", requestMDMAppleCSREndpoint, requestMDMAppleCSRRequest{})
|
|
// Deprecated: this endpoint shouldn't be used anymore in favor of the
|
|
// new flow described in https://github.com/fleetdm/fleet/issues/10383
|
|
ue.POST("/api/_version_/fleet/mdm/apple/dep/key_pair", newMDMAppleDEPKeyPairEndpoint, nil)
|
|
ue.GET("/api/_version_/fleet/mdm/apple/abm_public_key", generateABMKeyPairEndpoint, nil)
|
|
ue.POST("/api/_version_/fleet/abm_tokens", uploadABMTokenEndpoint, uploadABMTokenRequest{})
|
|
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.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{})
|
|
ue.POST("/api/_version_/fleet/mdm/apple/apns_certificate", uploadMDMAppleAPNSCertEndpoint, uploadMDMAppleAPNSCertRequest{})
|
|
ue.DELETE("/api/_version_/fleet/mdm/apple/apns_certificate", deleteMDMAppleAPNSCertEndpoint, deleteMDMAppleAPNSCertRequest{})
|
|
|
|
// VPP Tokens
|
|
ue.GET("/api/_version_/fleet/vpp_tokens", getVPPTokens, getVPPTokensRequest{})
|
|
ue.POST("/api/_version_/fleet/vpp_tokens", uploadVPPTokenEndpoint, uploadVPPTokenRequest{})
|
|
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{})
|
|
|
|
// Batch VPP Associations
|
|
ue.POST("/api/_version_/fleet/software/app_store_apps/batch", batchAssociateAppStoreAppsEndpoint, batchAssociateAppStoreAppsRequest{})
|
|
|
|
// Deprecated: GET /mdm/apple_bm is now deprecated, replaced by the
|
|
// GET /abm endpoint.
|
|
ue.GET("/api/_version_/fleet/mdm/apple_bm", getAppleBMEndpoint, nil)
|
|
// Deprecated: GET /abm is now deprecated, replaced by the GET /abm_tokens endpoint.
|
|
ue.GET("/api/_version_/fleet/abm", getAppleBMEndpoint, nil)
|
|
|
|
// Deprecated: POST /mdm/apple/profiles/batch is now deprecated, replaced by the
|
|
// platform-agnostic POST /mdm/profiles/batch. It is still supported
|
|
// indefinitely for backwards compatibility.
|
|
//
|
|
// batch-apply is accessible even though MDM is not enabled, it needs
|
|
// to support the case where `fleetctl get config`'s output is used as
|
|
// input to `fleetctl apply`
|
|
ue.WithRequestBodySizeLimit(fleet.MaxBatchProfileSize).POST("/api/_version_/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesEndpoint, batchSetMDMAppleProfilesRequest{})
|
|
|
|
// batch-apply is accessible even though MDM is not enabled, it needs
|
|
// to support the case where `fleetctl get config`'s output is used as
|
|
// input to `fleetctl apply`
|
|
ue.WithRequestBodySizeLimit(fleet.MaxBatchProfileSize).POST("/api/_version_/fleet/mdm/profiles/batch", batchSetMDMProfilesEndpoint, batchSetMDMProfilesRequest{})
|
|
|
|
// Certificate Authority endpoints
|
|
ue.POST("/api/_version_/fleet/certificate_authorities", createCertificateAuthorityEndpoint, createCertificateAuthorityRequest{})
|
|
ue.GET("/api/_version_/fleet/certificate_authorities", listCertificateAuthoritiesEndpoint, listCertificateAuthoritiesRequest{})
|
|
ue.GET("/api/_version_/fleet/certificate_authorities/{id:[0-9]+}", getCertificateAuthorityEndpoint, getCertificateAuthorityRequest{})
|
|
ue.DELETE("/api/_version_/fleet/certificate_authorities/{id:[0-9]+}", deleteCertificateAuthorityEndpoint, deleteCertificateAuthorityRequest{})
|
|
ue.PATCH("/api/_version_/fleet/certificate_authorities/{id:[0-9]+}", updateCertificateAuthorityEndpoint, updateCertificateAuthorityRequest{})
|
|
ue.POST("/api/_version_/fleet/certificate_authorities/{id:[0-9]+}/request_certificate", requestCertificateEndpoint, requestCertificateRequest{})
|
|
ue.POST("/api/_version_/fleet/spec/certificate_authorities", batchApplyCertificateAuthoritiesEndpoint, batchApplyCertificateAuthoritiesRequest{})
|
|
ue.GET("/api/_version_/fleet/spec/certificate_authorities", getCertificateAuthoritiesSpecEndpoint, getCertificateAuthoritiesSpecRequest{})
|
|
|
|
mdmAndroidMW := ue.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAndroidMDM())
|
|
mdmAndroidMW.POST("/api/_version_/fleet/software/web_apps", createAndroidWebAppEndpoint, createAndroidWebAppRequest{})
|
|
|
|
ipBanner := redis.NewIPBanner(redisPool, "ipbanner::",
|
|
deviceIPAllowedConsecutiveFailingRequestsCount,
|
|
deviceIPAllowedConsecutiveFailingRequestsTimeWindow,
|
|
deviceIPBanTime,
|
|
)
|
|
errorLimiter := ratelimit.NewErrorMiddleware(ipBanner).Limit(logger)
|
|
|
|
// Device-authenticated endpoints.
|
|
de := newDeviceAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}", getDeviceHostEndpoint, getDeviceHostRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/desktop", getFleetDesktopEndpoint, getFleetDesktopRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).HEAD("/api/_version_/fleet/device/{token}/ping", devicePingEndpoint, deviceAuthPingRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).POST("/api/_version_/fleet/device/{token}/refetch", refetchDeviceHostEndpoint, refetchDeviceHostRequest{})
|
|
// Deprecated: Device mapping data is now included in host details endpoint
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/device_mapping", listDeviceHostDeviceMappingEndpoint, listDeviceHostDeviceMappingRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/macadmins", getDeviceMacadminsDataEndpoint, getDeviceMacadminsDataRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/policies", listDevicePoliciesEndpoint, listDevicePoliciesRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/transparency", transparencyURL, transparencyURLRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).WithRequestBodySizeLimit(fleet.MaxFleetdErrorReportSize).POST("/api/_version_/fleet/device/{token}/debug/errors", fleetdError, fleetdErrorRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/software", getDeviceSoftwareEndpoint, getDeviceSoftwareRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).POST("/api/_version_/fleet/device/{token}/software/install/{software_title_id}", submitSelfServiceSoftwareInstall, fleetSelfServiceSoftwareInstallRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).POST("/api/_version_/fleet/device/{token}/software/uninstall/{software_title_id}", submitDeviceSoftwareUninstall, fleetDeviceSoftwareUninstallRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/software/install/{install_uuid}/results", getDeviceSoftwareInstallResultsEndpoint, getDeviceSoftwareInstallResultsRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/software/uninstall/{execution_id}/results", getDeviceSoftwareUninstallResultsEndpoint, getDeviceSoftwareUninstallResultsRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/certificates", listDeviceCertificatesEndpoint, listDeviceCertificatesRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).POST("/api/_version_/fleet/device/{token}/setup_experience/status", getDeviceSetupExperienceStatusEndpoint, getDeviceSetupExperienceStatusRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/software/titles/{software_title_id}/icon", getDeviceSoftwareIconEndpoint, getDeviceSoftwareIconRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).POST("/api/_version_/fleet/device/{token}/mdm/linux/trigger_escrow", triggerLinuxDiskEncryptionEscrowEndpoint, triggerLinuxDiskEncryptionEscrowRequest{})
|
|
de.WithCustomMiddleware(errorLimiter).POST("/api/_version_/fleet/device/{token}/bypass_conditional_access", bypassConditionalAccessEndpoint, bypassConditionalAccessRequest{})
|
|
// Device authenticated, Apple MDM endpoints.
|
|
demdm := de.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
|
|
demdm.AppendCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/mdm/apple/manual_enrollment_profile", getDeviceMDMManualEnrollProfileEndpoint, getDeviceMDMManualEnrollProfileRequest{})
|
|
demdm.AppendCustomMiddleware(errorLimiter).GET("/api/_version_/fleet/device/{token}/software/commands/{command_uuid}/results", getDeviceMDMCommandResultsEndpoint, getDeviceMDMCommandResultsRequest{})
|
|
demdm.AppendCustomMiddleware(errorLimiter).POST("/api/_version_/fleet/device/{token}/configuration_profiles/{profile_uuid}/resend", resendDeviceConfigurationProfileEndpoint, resendDeviceConfigurationProfileRequest{})
|
|
demdm.AppendCustomMiddleware(errorLimiter).POST("/api/_version_/fleet/device/{token}/migrate_mdm", migrateMDMDeviceEndpoint, deviceMigrateMDMRequest{})
|
|
|
|
// host-authenticated endpoints
|
|
he := newHostAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
|
|
|
|
// Note that the /osquery/ endpoints are *not* versioned, i.e. there is no
|
|
// `_version_` placeholder in the path. This is deliberate, see
|
|
// https://github.com/fleetdm/fleet/pull/4731#discussion_r838931732 For now
|
|
// we add an alias to `/api/v1/osquery` so that it is backwards compatible,
|
|
// but even that `v1` is *not* part of the standard versioning, it will still
|
|
// work even after we remove support for the `v1` version for the rest of the
|
|
// API. This allows us to deprecate osquery endpoints separately.
|
|
he.WithAltPaths("/api/v1/osquery/config").
|
|
POST("/api/osquery/config", getClientConfigEndpoint, getClientConfigRequest{})
|
|
he.WithAltPaths("/api/v1/osquery/distributed/read").
|
|
POST("/api/osquery/distributed/read", getDistributedQueriesEndpoint, getDistributedQueriesRequest{})
|
|
distWriteLimit := config.Osquery.MaxDistributedWriteBodySize
|
|
if distWriteLimit == 0 {
|
|
distWriteLimit = fleet.DefaultMaxOsqueryDistributedWriteSize
|
|
}
|
|
he.WithRequestBodySizeLimit(distWriteLimit).WithAltPaths("/api/v1/osquery/distributed/write").
|
|
POST("/api/osquery/distributed/write", submitDistributedQueryResultsEndpoint, submitDistributedQueryResultsRequestShim{})
|
|
he.WithAltPaths("/api/v1/osquery/carve/begin").
|
|
POST("/api/osquery/carve/begin", carveBeginEndpoint, carveBeginRequest{})
|
|
logWriteLimit := config.Osquery.MaxLogWriteBodySize
|
|
if logWriteLimit == 0 {
|
|
logWriteLimit = fleet.DefaultMaxOsqueryLogWriteSize
|
|
}
|
|
he.WithRequestBodySizeLimit(logWriteLimit).WithAltPaths("/api/v1/osquery/log").
|
|
POST("/api/osquery/log", submitLogsEndpoint, submitLogsRequest{})
|
|
he.WithAltPaths("/api/v1/osquery/yara/{name}").
|
|
POST("/api/osquery/yara/{name}", getYaraEndpoint, getYaraRequest{})
|
|
|
|
// android authenticated end-points
|
|
// Authentication is implemented using the orbit_node_key from the 'Authentication' header.
|
|
// The 'orbit_node_key' is used because it's the only thing we have available when the device gets enrolled
|
|
// after the MDM setup is complete.
|
|
androidEndpoints := androidAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
|
|
androidEndpoints.GET("/api/fleetd/certificates/{id:[0-9]+}", getDeviceCertificateTemplateEndpoint, getDeviceCertificateTemplateRequest{})
|
|
androidEndpoints.PUT("/api/fleetd/certificates/{id:[0-9]+}/status", updateCertificateStatusEndpoint, updateCertificateStatusRequest{})
|
|
|
|
// orbit authenticated endpoints
|
|
oe := newOrbitAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
|
|
oe.POST("/api/fleet/orbit/device_token", setOrUpdateDeviceTokenEndpoint, fleet.SetOrUpdateDeviceTokenRequest{})
|
|
oe.POST("/api/fleet/orbit/config", getOrbitConfigEndpoint, fleet.OrbitGetConfigRequest{})
|
|
// using POST to get a script execution request since all authenticated orbit
|
|
// endpoints are POST due to passing the device token in the JSON body.
|
|
oe.POST("/api/fleet/orbit/scripts/request", getOrbitScriptEndpoint, fleet.OrbitGetScriptRequest{})
|
|
oe.POST("/api/fleet/orbit/scripts/result", postOrbitScriptResultEndpoint, fleet.OrbitPostScriptResultRequest{})
|
|
oe.PUT("/api/fleet/orbit/device_mapping", putOrbitDeviceMappingEndpoint, fleet.OrbitPutDeviceMappingRequest{})
|
|
oe.WithRequestBodySizeLimit(fleet.MaxMultiScriptQuerySize).POST("/api/fleet/orbit/software_install/result", postOrbitSoftwareInstallResultEndpoint, fleet.OrbitPostSoftwareInstallResultRequest{})
|
|
oe.POST("/api/fleet/orbit/software_install/package", orbitDownloadSoftwareInstallerEndpoint, fleet.OrbitDownloadSoftwareInstallerRequest{})
|
|
oe.POST("/api/fleet/orbit/software_install/details", getOrbitSoftwareInstallDetails, fleet.OrbitGetSoftwareInstallRequest{})
|
|
oe.POST("/api/fleet/orbit/setup_experience/init", orbitSetupExperienceInitEndpoint, fleet.OrbitSetupExperienceInitRequest{})
|
|
|
|
// POST /api/fleet/orbit/setup_experience/status is used by macOS and Linux hosts.
|
|
// For macOS hosts we verify Apple MDM is enabled and configured.
|
|
oeAppleMDM := oe.WithCustomMiddlewareAfterAuth(mdmConfiguredMiddleware.VerifyAppleMDMOnMacOSHosts())
|
|
oeAppleMDM.POST("/api/fleet/orbit/setup_experience/status", getOrbitSetupExperienceStatusEndpoint, fleet.GetOrbitSetupExperienceStatusRequest{})
|
|
|
|
oeWindowsMDM := oe.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM())
|
|
oeWindowsMDM.POST("/api/fleet/orbit/disk_encryption_key", postOrbitDiskEncryptionKeyEndpoint, fleet.OrbitPostDiskEncryptionKeyRequest{})
|
|
|
|
oe.POST("/api/fleet/orbit/luks_data", postOrbitLUKSEndpoint, fleet.OrbitPostLUKSRequest{})
|
|
|
|
// unauthenticated endpoints - most of those are either login-related,
|
|
// invite-related or host-enrolling. So they typically do some kind of
|
|
// one-time authentication by verifying that a valid secret token is provided
|
|
// with the request.
|
|
ne := newNoAuthEndpointer(svc, opts, r, apiVersions...)
|
|
ne.WithAltPaths("/api/v1/osquery/enroll").
|
|
POST("/api/osquery/enroll", enrollAgentEndpoint, contract.EnrollOsqueryAgentRequest{})
|
|
|
|
// These endpoint are token authenticated.
|
|
// NOTE: remember to update
|
|
// `service.mdmConfigurationRequiredEndpoints` when you add an
|
|
// endpoint that's behind the mdmConfiguredMiddleware, this applies
|
|
// both to this set of endpoints and to any user authenticated
|
|
// endpoints using `mdmAppleMW.*` above in this file.
|
|
neAppleMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
|
|
neAppleMDM.GET(apple_mdm.EnrollPath, mdmAppleEnrollEndpoint, mdmAppleEnrollRequest{})
|
|
neAppleMDM.POST(apple_mdm.EnrollPath, mdmAppleEnrollEndpoint, mdmAppleEnrollRequest{})
|
|
|
|
neAppleMDM.GET(apple_mdm.InstallerPath, mdmAppleGetInstallerEndpoint, mdmAppleGetInstallerRequest{})
|
|
neAppleMDM.HEAD(apple_mdm.InstallerPath, mdmAppleHeadInstallerEndpoint, mdmAppleHeadInstallerRequest{})
|
|
neAppleMDM.POST("/api/_version_/fleet/ota_enrollment", mdmAppleOTAEndpoint, mdmAppleOTARequest{})
|
|
|
|
// Deprecated: GET /mdm/bootstrap is now deprecated, replaced by the
|
|
// GET /bootstrap endpoint.
|
|
neAppleMDM.GET("/api/_version_/fleet/mdm/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{})
|
|
neAppleMDM.GET("/api/_version_/fleet/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{})
|
|
|
|
// Deprecated: GET /mdm/apple/bootstrap is now deprecated, replaced by the platform agnostic /mdm/bootstrap
|
|
neAppleMDM.GET("/api/_version_/fleet/mdm/apple/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{})
|
|
|
|
// Deprecated: GET /mdm/setup/eula/:token is now deprecated, replaced by the
|
|
// GET /setup_experience/eula/:token endpoint.
|
|
neAppleMDM.GET("/api/_version_/fleet/mdm/setup/eula/{token}", getMDMEULAEndpoint, getMDMEULARequest{})
|
|
neAppleMDM.GET("/api/_version_/fleet/setup_experience/eula/{token}", getMDMEULAEndpoint, getMDMEULARequest{})
|
|
|
|
// Deprecated: GET /mdm/apple/setup/eula/:token is now deprecated, replaced by the platform agnostic /mdm/setup/eula/:token
|
|
neAppleMDM.GET("/api/_version_/fleet/mdm/apple/setup/eula/{token}", getMDMEULAEndpoint, getMDMEULARequest{})
|
|
|
|
// Get OTA profile
|
|
neAppleMDM.GET("/api/_version_/fleet/enrollment_profiles/ota", getOTAProfileEndpoint, getOTAProfileRequest{})
|
|
|
|
// This is the account-driven enrollment endpoint for BYoD Apple devices, also known as User Enrollment.
|
|
neAppleMDM.POST(apple_mdm.AccountDrivenEnrollPath, mdmAppleAccountEnrollEndpoint, mdmAppleAccountEnrollRequest{})
|
|
// This is for OAUTH2 token based auth
|
|
// ne.POST(apple_mdm.EnrollPath+"/token", mdmAppleAccountEnrollTokenEndpoint, mdmAppleAccountEnrollTokenRequest{})
|
|
|
|
// These endpoint are used by Microsoft devices during MDM device enrollment phase
|
|
neWindowsMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM())
|
|
|
|
// Microsoft MS-MDE2 Endpoints
|
|
// This endpoint is unauthenticated and is used by Microsoft devices to discover the MDM server endpoints
|
|
neWindowsMDM.WithRequestBodySizeLimit(fleet.MaxMicrosoftMDMSize).POST(microsoft_mdm.MDE2DiscoveryPath, mdmMicrosoftDiscoveryEndpoint, SoapRequestContainer{})
|
|
|
|
// This endpoint is unauthenticated and is used by Microsoft devices to retrieve the opaque STS auth token
|
|
neWindowsMDM.WithRequestBodySizeLimit(fleet.MaxMicrosoftMDMSize).GET(microsoft_mdm.MDE2AuthPath, mdmMicrosoftAuthEndpoint, SoapRequestContainer{})
|
|
|
|
// This endpoint is authenticated using the BinarySecurityToken header field
|
|
neWindowsMDM.WithRequestBodySizeLimit(fleet.MaxMicrosoftMDMSize).POST(microsoft_mdm.MDE2PolicyPath, mdmMicrosoftPolicyEndpoint, SoapRequestContainer{})
|
|
|
|
// This endpoint is authenticated using the BinarySecurityToken header field
|
|
neWindowsMDM.WithRequestBodySizeLimit(fleet.MaxMicrosoftMDMSize).POST(microsoft_mdm.MDE2EnrollPath, mdmMicrosoftEnrollEndpoint, SoapRequestContainer{})
|
|
|
|
// This endpoint is unauthenticated for now
|
|
// It should be authenticated through TLS headers once proper implementation is in place
|
|
neWindowsMDM.WithRequestBodySizeLimit(fleet.MaxMicrosoftMDMSize).POST(microsoft_mdm.MDE2ManagementPath, mdmMicrosoftManagementEndpoint, SyncMLReqMsgContainer{})
|
|
|
|
// This endpoint is unauthenticated and is used by to retrieve the MDM enrollment Terms of Use
|
|
neWindowsMDM.WithRequestBodySizeLimit(fleet.MaxMicrosoftMDMSize).GET(microsoft_mdm.MDE2TOSPath, mdmMicrosoftTOSEndpoint, MDMWebContainer{})
|
|
|
|
// These endpoints are unauthenticated and made from orbit, and add the orbit capabilities header.
|
|
neOrbit := newOrbitNoAuthEndpointer(svc, opts, r, apiVersions...)
|
|
neOrbit.POST("/api/fleet/orbit/enroll", enrollOrbitEndpoint, fleet.EnrollOrbitRequest{})
|
|
|
|
ne.GET("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/in_house_app", getInHouseAppPackageEndpoint, getInHouseAppPackageRequest{})
|
|
ne.GET("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/in_house_app/manifest", getInHouseAppManifestEndpoint, getInHouseAppManifestRequest{})
|
|
|
|
// For some reason osquery does not provide a node key with the block data.
|
|
// Instead the carve session ID should be verified in the service method.
|
|
// Since []byte slices is encoded as base64 in JSON, increase the limit to 1.5x
|
|
ne.SkipRequestBodySizeLimit().WithAltPaths("/api/v1/osquery/carve/block").
|
|
POST("/api/osquery/carve/block", carveBlockEndpoint, carveBlockRequest{})
|
|
|
|
ne.GET("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/package/token/{token}", downloadSoftwareInstallerEndpoint,
|
|
downloadSoftwareInstallerRequest{})
|
|
|
|
ne.POST("/api/_version_/fleet/perform_required_password_reset", performRequiredPasswordResetEndpoint, performRequiredPasswordResetRequest{})
|
|
ne.POST("/api/_version_/fleet/users", createUserFromInviteEndpoint, createUserRequest{})
|
|
ne.GET("/api/_version_/fleet/invites/{token}", verifyInviteEndpoint, verifyInviteRequest{})
|
|
ne.POST("/api/_version_/fleet/reset_password", resetPasswordEndpoint, resetPasswordRequest{})
|
|
ne.POST("/api/_version_/fleet/logout", logoutEndpoint, nil)
|
|
ne.POST("/api/v1/fleet/sso", initiateSSOEndpoint, initiateSSORequest{})
|
|
ne.POST("/api/v1/fleet/sso/callback", makeCallbackSSOEndpoint(config.Server.URLPrefix), callbackSSORequest{})
|
|
ne.GET("/api/v1/fleet/sso", settingsSSOEndpoint, nil)
|
|
|
|
// the websocket distributed query results endpoint is a bit different - the
|
|
// provided path is a prefix, not an exact match, and it is not a go-kit
|
|
// endpoint but a raw http.Handler. It uses the NoAuthEndpointer because
|
|
// authentication is done when the websocket session is established, inside
|
|
// the handler.
|
|
ne.UsePathPrefix().PathHandler("GET", "/api/_version_/fleet/results/",
|
|
makeStreamDistributedQueryCampaignResultsHandler(config.Server, svc, logger))
|
|
|
|
quota := throttled.RateQuota{MaxRate: throttled.PerHour(10), MaxBurst: forgotPasswordRateLimitMaxBurst}
|
|
limiter := ratelimit.NewMiddleware(limitStore)
|
|
ne.
|
|
WithCustomMiddleware(limiter.Limit("forgot_password", quota)).
|
|
POST("/api/_version_/fleet/forgot_password", forgotPasswordEndpoint, forgotPasswordRequest{})
|
|
|
|
// By default, MDM SSO shares the login rate limit bucket; if MDM SSO limit is overridden, MDM SSO gets its
|
|
// own rate limit bucket.
|
|
loginRateLimit := throttled.PerMin(10)
|
|
if extra.loginRateLimit != nil {
|
|
loginRateLimit = *extra.loginRateLimit
|
|
}
|
|
loginLimiter := limiter.Limit("login", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})
|
|
mdmSsoLimiter := loginLimiter
|
|
if extra.mdmSsoRateLimit != nil {
|
|
mdmSsoLimiter = limiter.Limit("mdm_sso", throttled.RateQuota{MaxRate: *extra.mdmSsoRateLimit, MaxBurst: 9})
|
|
}
|
|
|
|
ne.WithCustomMiddleware(loginLimiter).
|
|
POST("/api/_version_/fleet/login", loginEndpoint, contract.LoginRequest{})
|
|
ne.WithCustomMiddleware(limiter.Limit("mfa", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})).
|
|
POST("/api/_version_/fleet/sessions", sessionCreateEndpoint, sessionCreateRequest{})
|
|
|
|
ne.HEAD("/api/fleet/device/ping", devicePingEndpoint, devicePingRequest{})
|
|
|
|
ne.HEAD("/api/fleet/orbit/ping", orbitPingEndpoint, fleet.OrbitPingRequest{})
|
|
|
|
// This is a callback endpoint for calendar integration -- it is called to notify an event change in a user calendar
|
|
ne.POST("/api/_version_/fleet/calendar/webhook/{event_uuid}", calendarWebhookEndpoint, calendarWebhookRequest{})
|
|
|
|
neAppleMDM.WithCustomMiddleware(mdmSsoLimiter).
|
|
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.
|
|
// If setup hasn't been completed it serves the API with a setup middleware.
|
|
// If the server is already configured, the default API handler is exposed.
|
|
func WithSetup(svc fleet.Service, logger *slog.Logger, applyStarterLibrary func(ctx context.Context, serverURL, token string) error, next http.Handler) http.HandlerFunc {
|
|
rxOsquery := regexp.MustCompile(`^/api/[^/]+/osquery`)
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
configRouter := http.NewServeMux()
|
|
srv := kithttp.NewServer(
|
|
makeSetupEndpoint(svc, logger, applyStarterLibrary),
|
|
decodeSetupRequest,
|
|
encodeResponse,
|
|
)
|
|
// NOTE: support setup on both /v1/ and version-less, in the future /v1/
|
|
// will be dropped.
|
|
configRouter.Handle("/api/v1/setup", srv)
|
|
configRouter.Handle("/api/setup", srv)
|
|
|
|
// whitelist osqueryd endpoints
|
|
if rxOsquery.MatchString(r.URL.Path) {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
requireSetup, err := svc.SetupRequired(ctx)
|
|
if err != nil {
|
|
logger.ErrorContext(ctx, "fetching setup info from db", "err", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if requireSetup {
|
|
configRouter.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// RedirectLoginToSetup detects if the setup endpoint should be used. If setup is required it redirect all
|
|
// frontend urls to /setup, otherwise the frontend router is used.
|
|
func RedirectLoginToSetup(svc fleet.Service, logger *slog.Logger, next http.Handler, urlPrefix string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/setup" {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
newURL := r.URL
|
|
newURL.Path = urlPrefix + "/setup"
|
|
http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
|
|
})
|
|
|
|
ctx := r.Context()
|
|
setupRequired, err := svc.SetupRequired(ctx)
|
|
if err != nil {
|
|
logger.ErrorContext(ctx, "fetching setupinfo from db", "err", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if setupRequired {
|
|
redirect.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
RedirectSetupToLogin(svc, logger, next, urlPrefix).ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// RedirectSetupToLogin forces the /setup path to be redirected to login. This middleware is used after
|
|
// the app has been setup.
|
|
func RedirectSetupToLogin(svc fleet.Service, logger *slog.Logger, next http.Handler, urlPrefix string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/setup" {
|
|
newURL := r.URL
|
|
newURL.Path = urlPrefix + "/login"
|
|
http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// RegisterAppleMDMProtocolServices registers the HTTP handlers that serve
|
|
// the MDM services to Apple devices.
|
|
func RegisterAppleMDMProtocolServices(
|
|
mux *http.ServeMux,
|
|
scepConfig config.MDMConfig,
|
|
mdmStorage fleet.MDMAppleStore,
|
|
scepStorage scep_depot.Depot,
|
|
logger *slog.Logger,
|
|
checkinAndCommandService nanomdm_service.CheckinAndCommandService,
|
|
ddmService nanomdm_service.DeclarativeManagement,
|
|
profileService nanomdm_service.ProfileService,
|
|
serverURLPrefix string,
|
|
fleetConfig config.FleetConfig,
|
|
) error {
|
|
if err := registerSCEP(mux, scepConfig, scepStorage, mdmStorage, logger, fleetConfig); err != nil {
|
|
return fmt.Errorf("scep: %w", err)
|
|
}
|
|
if err := registerMDM(mux, mdmStorage, checkinAndCommandService, ddmService, profileService, logger, fleetConfig); err != nil {
|
|
return fmt.Errorf("mdm: %w", err)
|
|
}
|
|
if err := registerMDMServiceDiscovery(mux, logger, serverURLPrefix, fleetConfig); err != nil {
|
|
return fmt.Errorf("service discovery: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func registerMDMServiceDiscovery(
|
|
mux *http.ServeMux,
|
|
logger *slog.Logger,
|
|
serverURLPrefix string,
|
|
fleetConfig config.FleetConfig,
|
|
) error {
|
|
serviceDiscoveryLogger := logger.With("component", "mdm-apple-service-discovery")
|
|
fullMDMEnrollmentURL := fmt.Sprintf("%s%s", serverURLPrefix, apple_mdm.AccountDrivenEnrollPath)
|
|
serviceDiscoveryHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
serviceDiscoveryLogger.InfoContext(ctx, "serving MDM service discovery response", "url", fullMDMEnrollmentURL)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, err := fmt.Fprintf(w, `{"Servers":[{"Version": "mdm-byod", "BaseURL": "%s"}]}`, fullMDMEnrollmentURL)
|
|
if err != nil {
|
|
serviceDiscoveryLogger.ErrorContext(ctx, "error writing service discovery response", "err", err)
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
}
|
|
})
|
|
mux.Handle(apple_mdm.ServiceDiscoveryPath, otel.WrapHandler(serviceDiscoveryHandler, apple_mdm.ServiceDiscoveryPath, fleetConfig))
|
|
return nil
|
|
}
|
|
|
|
// registerSCEP registers the HTTP handler for SCEP service needed for enrollment to MDM.
|
|
// Returns the SCEP CA certificate that can be used by verifiers.
|
|
func registerSCEP(
|
|
mux *http.ServeMux,
|
|
scepConfig config.MDMConfig,
|
|
scepStorage scep_depot.Depot,
|
|
mdmStorage fleet.MDMAppleStore,
|
|
logger *slog.Logger,
|
|
fleetConfig config.FleetConfig,
|
|
) error {
|
|
var signer scepserver.CSRSignerContext = scepserver.SignCSRAdapter(scep_depot.NewSigner(
|
|
scepStorage,
|
|
scep_depot.WithValidityDays(scepConfig.AppleSCEPSignerValidityDays),
|
|
// This value was allowed to be configured via --mdm_apple_scep_signer_allow_renewal_days but there was no real use case for
|
|
// customizing it and it was confusing for customers, so it has been removed and replaced with the default of 14. For discussion,
|
|
// see https://github.com/fleetdm/fleet/issues/38611 and https://github.com/fleetdm/fleet/issues/37880#issuecomment-3805983198
|
|
// Fleet has a 180-day renewal cron that is completely unrelated to this or its value
|
|
scep_depot.WithAllowRenewalDays(14),
|
|
))
|
|
assets, err := mdmStorage.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{fleet.MDMAssetSCEPChallenge}, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("retrieving SCEP challenge: %w", err)
|
|
}
|
|
|
|
scepChallenge := string(assets[fleet.MDMAssetSCEPChallenge].Value)
|
|
signer = scepserver.StaticChallengeMiddleware(scepChallenge, signer)
|
|
scepService := NewSCEPService(
|
|
mdmStorage,
|
|
signer,
|
|
logger.With("component", "mdm-apple-scep"),
|
|
)
|
|
|
|
scepSlogLogger := logger.With("component", "http-mdm-apple-scep")
|
|
e := scepserver.MakeServerEndpoints(scepService)
|
|
e.GetEndpoint = scepserver.EndpointLoggingMiddleware(scepSlogLogger)(e.GetEndpoint)
|
|
e.PostEndpoint = scepserver.EndpointLoggingMiddleware(scepSlogLogger)(e.PostEndpoint)
|
|
scepHandler := scepserver.MakeHTTPHandler(e, scepService, scepSlogLogger)
|
|
mux.Handle(apple_mdm.SCEPPath, otel.WrapHandler(scepHandler, apple_mdm.SCEPPath, fleetConfig))
|
|
return nil
|
|
}
|
|
|
|
func RegisterSCEPProxy(
|
|
rootMux *http.ServeMux,
|
|
ds fleet.Datastore,
|
|
logger *slog.Logger,
|
|
timeout *time.Duration,
|
|
fleetConfig *config.FleetConfig,
|
|
) error {
|
|
if fleetConfig == nil {
|
|
return errors.New("fleet config is nil")
|
|
}
|
|
scepService := scep.NewSCEPProxyService(
|
|
ds,
|
|
logger.With("component", "scep-proxy-service"),
|
|
timeout,
|
|
)
|
|
scepProxySlogLogger := logger.With("component", "http-scep-proxy")
|
|
e := scepserver.MakeServerEndpointsWithIdentifier(scepService)
|
|
e.GetEndpoint = scepserver.EndpointLoggingMiddleware(scepProxySlogLogger)(e.GetEndpoint)
|
|
e.PostEndpoint = scepserver.EndpointLoggingMiddleware(scepProxySlogLogger)(e.PostEndpoint)
|
|
scepHandler := scepserver.MakeHTTPHandlerWithIdentifier(e, apple_mdm.SCEPProxyPath, scepProxySlogLogger)
|
|
// Not using OTEL dynamic wrapper so as not to expose {identifier} in the span name
|
|
scepHandler = otel.WrapHandler(scepHandler, apple_mdm.SCEPProxyPath, *fleetConfig)
|
|
rootMux.Handle(apple_mdm.SCEPProxyPath, scepHandler)
|
|
return nil
|
|
}
|
|
|
|
// NanoMDMLogger is a logger adapter for nanomdm.
|
|
type NanoMDMLogger struct {
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewNanoMDMLogger(logger *slog.Logger) *NanoMDMLogger {
|
|
return &NanoMDMLogger{
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func (l *NanoMDMLogger) Info(keyvals ...interface{}) {
|
|
l.logger.InfoContext(context.TODO(), "", keyvals...)
|
|
}
|
|
|
|
func (l *NanoMDMLogger) Debug(keyvals ...interface{}) {
|
|
l.logger.DebugContext(context.TODO(), "", keyvals...)
|
|
}
|
|
|
|
func (l *NanoMDMLogger) With(keyvals ...interface{}) nanomdm_log.Logger {
|
|
return &NanoMDMLogger{
|
|
logger: l.logger.With(keyvals...),
|
|
}
|
|
}
|
|
|
|
// registerMDM registers the HTTP handlers that serve core MDM services (like checking in for MDM commands).
|
|
func registerMDM(
|
|
mux *http.ServeMux,
|
|
mdmStorage fleet.MDMAppleStore,
|
|
checkinAndCommandService nanomdm_service.CheckinAndCommandService,
|
|
ddmService nanomdm_service.DeclarativeManagement,
|
|
profileService nanomdm_service.ProfileService,
|
|
logger *slog.Logger,
|
|
fleetConfig config.FleetConfig,
|
|
) error {
|
|
certVerifier := mdmcrypto.NewSCEPVerifier(mdmStorage)
|
|
mdmLogger := NewNanoMDMLogger(logger.With("component", "http-mdm-apple-mdm"))
|
|
|
|
// As usual, handlers are applied from bottom to top:
|
|
// 1. Extract and verify MDM signature.
|
|
// 2. Verify signer certificate with CA.
|
|
// 3. Verify new or enrolled certificate (certauth.CertAuth which wraps the MDM service).
|
|
// 4. Pass a copy of the request to Fleet middleware that ingests new hosts from pending MDM
|
|
// enrollments and updates the Fleet hosts table accordingly with the UDID and serial number of
|
|
// the device.
|
|
// 5. Run actual MDM service operation (checkin handler or command and results handler).
|
|
coreMDMService := nanomdm.New(mdmStorage, nanomdm.WithLogger(mdmLogger), nanomdm.WithDeclarativeManagement(ddmService),
|
|
nanomdm.WithProfileService(profileService), nanomdm.WithUserAuthenticate(checkinAndCommandService))
|
|
// NOTE: it is critical that the coreMDMService runs first, as the first
|
|
// service in the multi-service feature is run to completion _before_ running
|
|
// the other ones in parallel. This way, subsequent services have access to
|
|
// the result of the core service, e.g. the device is enrolled, etc.
|
|
var mdmService nanomdm_service.CheckinAndCommandService = multi.New(mdmLogger, coreMDMService, checkinAndCommandService)
|
|
|
|
mdmService = certauth.New(mdmService, mdmStorage, certauth.WithLogger(mdmLogger.With("handler", "cert-auth")))
|
|
var mdmHandler http.Handler = httpmdm.CheckinAndCommandHandler(mdmService, mdmLogger.With("handler", "checkin-command"))
|
|
verifyDisable, exists := os.LookupEnv("FLEET_MDM_APPLE_SCEP_VERIFY_DISABLE")
|
|
if exists && (strings.EqualFold(verifyDisable, "true") || verifyDisable == "1") {
|
|
logger.InfoContext(context.TODO(),
|
|
"disabling verification of macOS SCEP certificates as FLEET_MDM_APPLE_SCEP_VERIFY_DISABLE is set to true")
|
|
} else {
|
|
mdmHandler = httpmdm.CertVerifyMiddleware(mdmHandler, certVerifier, mdmLogger.With("handler", "cert-verify"))
|
|
}
|
|
mdmHandler = httpmdm.CertExtractMdmSignatureMiddleware(mdmHandler, httpmdm.MdmSignatureVerifierFunc(cryptoutil.VerifyMdmSignature),
|
|
httpmdm.SigLogWithLogger(mdmLogger.With("handler", "cert-extract")))
|
|
mux.Handle(apple_mdm.MDMPath, otel.WrapHandler(mdmHandler, apple_mdm.MDMPath, fleetConfig))
|
|
return nil
|
|
}
|
|
|
|
func WithMDMEnrollmentMiddleware(svc fleet.Service, logger *slog.Logger, next http.Handler) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/mdm/sso" && r.URL.Path != "/account_driven_enroll/sso" {
|
|
// TODO: redirects for non-SSO config web url?
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// if x-apple-aspen-deviceinfo custom header is present, we need to check for minimum os version
|
|
di := r.Header.Get("x-apple-aspen-deviceinfo")
|
|
if di != "" {
|
|
parsed, err := apple_mdm.ParseDeviceinfo(di, false) // FIXME: use verify=true when we have better parsing for various Apple certs (https://github.com/fleetdm/fleet/issues/20879)
|
|
if err != nil {
|
|
// just log the error and continue to next
|
|
logger.ErrorContext(r.Context(), "parsing x-apple-aspen-deviceinfo", "err", err)
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// TODO: skip os version check if deviceinfo query param is present? or find another way
|
|
// to avoid polling the DB and Apple endpoint twice for each enrollment.
|
|
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(r.Context(), parsed)
|
|
if err != nil {
|
|
// just log the error and continue to next
|
|
logger.ErrorContext(r.Context(), "checking minimum os version for mdm", "err", err)
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
if sur != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusForbidden)
|
|
if err := json.NewEncoder(w).Encode(sur); err != nil {
|
|
logger.ErrorContext(r.Context(), "failed to encode software update required", "err", err)
|
|
http.Redirect(w, r, r.URL.String()+"?error=true", http.StatusSeeOther)
|
|
}
|
|
return
|
|
}
|
|
|
|
// TODO: Do non-Apple devices ever use this route? If so, we probably need to change the
|
|
// approach below so we don't endlessly redirect non-Apple clients to the same URL.
|
|
|
|
// if we get here, the minimum os version is satisfied, so we continue with SSO flow
|
|
q := r.URL.Query()
|
|
v, ok := q["deviceinfo"]
|
|
if !ok || len(v) == 0 {
|
|
// If the deviceinfo query param is empty, we add the deviceinfo to the URL and
|
|
// redirect.
|
|
//
|
|
// Note: We'll apply this redirect only if query params are empty because want to
|
|
// redirect to the same URL with added query params after parsing the x-apple-aspen-deviceinfo
|
|
// header. Whenever we see a request with any query params already present, we'll
|
|
// skip this step and just continue to the next handler.
|
|
newURL := *r.URL
|
|
q.Set("deviceinfo", di)
|
|
newURL.RawQuery = q.Encode()
|
|
logger.InfoContext(r.Context(), "handling mdm sso: redirect with deviceinfo", "host_uuid", parsed.UDID, "serial", parsed.Serial)
|
|
http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
if len(v) > 0 && v[0] != di {
|
|
// something is wrong, the device info in the query params does not match
|
|
// the one in the header, so we just log the error and continue to next
|
|
logger.ErrorContext(r.Context(), "device info in query params does not match header", "header", di, "query", v[0])
|
|
}
|
|
logger.InfoContext(r.Context(), "handling mdm sso: proceed to next", "host_uuid", parsed.UDID, "serial", parsed.Serial)
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
}
|