Files
fleet/server/service/testing_utils_test.go
T
Juan Fernandez ed14c5385c Fix API endpoint validation for prefix-mounted SCIM routes
The SCIM endpoints are served by the elimity-com/scim library mounted as
a single prefix handler on the root ServeMux, so they are never
registered as individual gorilla/mux routes.

Since the routes can't be discovered, supply them to the validator
instead: add scim.RegisterValidationRoutes, a FeatureRouteFunc that
registers stub routes for the SCIM endpoints (handlers are never
invoked, only their path templates and methods are inspected). Wire it
into the three Validate call sites (production serve, test helper,
svctest).
2026-06-30 09:10:06 -04:00

1465 lines
43 KiB
Go

package service
import (
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/WatchBeam/clock"
ma "github.com/fleetdm/fleet/v4/ee/maintained-apps"
"github.com/fleetdm/fleet/v4/ee/server/scim"
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
"github.com/fleetdm/fleet/v4/ee/server/service/condaccess"
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
"github.com/fleetdm/fleet/v4/ee/server/service/est"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
"github.com/fleetdm/fleet/v4/ee/server/service/scep"
"github.com/fleetdm/fleet/v4/server/acl/acmeacl"
"github.com/fleetdm/fleet/v4/server/acl/activityacl"
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
activity_bootstrap "github.com/fleetdm/fleet/v4/server/activity/bootstrap"
apiendpoints "github.com/fleetdm/fleet/v4/server/api_endpoints"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/datastore/cached_mysql"
"github.com/fleetdm/fleet/v4/server/datastore/filesystem"
"github.com/fleetdm/fleet/v4/server/datastore/redis"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/dev_mode"
"github.com/fleetdm/fleet/v4/server/errorstore"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/logging"
"github.com/fleetdm/fleet/v4/server/mail"
acme_bootstrap "github.com/fleetdm/fleet/v4/server/mdm/acme/bootstrap"
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
nanodep_storage "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
"github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
fleet_mock "github.com/fleetdm/fleet/v4/server/mock"
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async"
"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/mock"
"github.com/fleetdm/fleet/v4/server/service/redis_key_value"
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
"github.com/fleetdm/fleet/v4/server/sso"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/go-kit/kit/endpoint"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/throttled/throttled/v2"
"github.com/throttled/throttled/v2/store/memstore"
"google.golang.org/api/androidmanagement/v1"
)
func newTestService(t *testing.T, ds fleet.Datastore, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) {
return newTestServiceWithConfig(t, ds, config.TestConfig(), rs, lq, opts...)
}
func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig config.FleetConfig, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) {
lic := &fleet.LicenseInfo{Tier: fleet.TierFree}
logger := slog.New(slog.DiscardHandler)
writer, err := logging.NewFilesystemLogWriter(t.Context(), fleetConfig.Filesystem.StatusLogFile, logger, fleetConfig.Filesystem.EnableLogRotation,
fleetConfig.Filesystem.EnableLogCompression, 500, 28, 3)
require.NoError(t, err)
osqlogger := &OsqueryLogger{Status: writer, Result: writer}
var (
failingPolicySet fleet.FailingPolicySet = NewMemFailingPolicySet()
enrollHostLimiter fleet.EnrollHostLimiter = nopEnrollHostLimiter{}
depStorage nanodep_storage.AllDEPStorage = &nanodep_mock.Storage{}
mailer fleet.MailService = &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }}
c clock.Clock = clock.C
scepConfigService = scep.NewSCEPConfigService(logger, nil)
digiCertService = digicert.NewService(digicert.WithLogger(logger))
estCAService = est.NewService(est.WithLogger(logger))
conditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
mdmStorage fleet.MDMAppleStore
mdmPusher nanomdm_push.Pusher
ssoStore sso.SessionStore
profMatcher fleet.ProfileMatcher
softwareInstallStore fleet.SoftwareInstallerStore
bootstrapPackageStore fleet.MDMBootstrapPackageStore
softwareTitleIconStore fleet.SoftwareTitleIconStore
distributedLock fleet.Lock
keyValueStore fleet.KeyValueStore
androidService android.Service
)
if len(opts) > 0 {
if opts[0].Clock != nil {
c = opts[0].Clock
}
}
if len(opts) > 0 && opts[0].KeyValueStore != nil {
keyValueStore = opts[0].KeyValueStore
}
task := async.NewTask(ds, nil, c, nil)
if len(opts) > 0 {
if opts[0].Task != nil {
task = opts[0].Task
} else {
opts[0].Task = task
}
}
if len(opts) > 0 {
if opts[0].Logger != nil {
logger = opts[0].Logger
}
if opts[0].License != nil {
lic = opts[0].License
}
if opts[0].Pool != nil {
ssoStore = sso.NewSessionStore(opts[0].Pool)
profMatcher = apple_mdm.NewProfileMatcher(opts[0].Pool)
distributedLock = redis_lock.NewLock(opts[0].Pool)
keyValueStore = redis_key_value.New(opts[0].Pool)
}
if opts[0].ProfileMatcher != nil {
profMatcher = opts[0].ProfileMatcher
}
if opts[0].FailingPolicySet != nil {
failingPolicySet = opts[0].FailingPolicySet
}
if opts[0].EnrollHostLimiter != nil {
enrollHostLimiter = opts[0].EnrollHostLimiter
}
if opts[0].UseMailService {
mailer, err = mail.NewService(config.TestConfig())
require.NoError(t, err)
}
if opts[0].SoftwareInstallStore != nil {
softwareInstallStore = opts[0].SoftwareInstallStore
}
if opts[0].BootstrapPackageStore != nil {
bootstrapPackageStore = opts[0].BootstrapPackageStore
}
if opts[0].SoftwareTitleIconStore != nil {
softwareTitleIconStore = opts[0].SoftwareTitleIconStore
}
// allow to explicitly set MDM storage to nil
mdmStorage = opts[0].MDMStorage
if opts[0].DEPStorage != nil {
depStorage = opts[0].DEPStorage
}
// allow to explicitly set mdm pusher to nil
mdmPusher = opts[0].MDMPusher
}
ctx := license.NewContext(context.Background(), lic)
cronSchedulesService := fleet.NewCronSchedules()
var eh *errorstore.Handler
if len(opts) > 0 {
if opts[0].Pool != nil {
eh = errorstore.NewHandler(ctx, opts[0].Pool, logger, time.Minute*10)
ctx = ctxerr.NewContext(ctx, eh)
}
if opts[0].StartCronSchedules != nil {
for _, fn := range opts[0].StartCronSchedules {
err = cronSchedulesService.StartCronSchedule(fn(ctx, ds))
require.NoError(t, err)
}
}
}
if len(opts) > 0 && opts[0].SCEPConfigService != nil {
scepConfigService = opts[0].SCEPConfigService
}
if len(opts) > 0 && opts[0].DigiCertService != nil {
digiCertService = opts[0].DigiCertService
}
if len(opts) > 0 && opts[0].ConditionalAccessMicrosoftProxy != nil {
conditionalAccessMicrosoftProxy = opts[0].ConditionalAccessMicrosoftProxy
fleetConfig.MicrosoftCompliancePartner.ProxyAPIKey = "insecure" // setting this so the feature is "enabled".
}
if len(opts) > 0 && opts[0].AndroidModule != nil {
androidService = opts[0].AndroidModule
}
var wstepManager microsoft_mdm.CertManager
if fleetConfig.MDM.WindowsWSTEPIdentityCert != "" && fleetConfig.MDM.WindowsWSTEPIdentityKey != "" {
rawCert, err := os.ReadFile(fleetConfig.MDM.WindowsWSTEPIdentityCert)
require.NoError(t, err)
rawKey, err := os.ReadFile(fleetConfig.MDM.WindowsWSTEPIdentityKey)
require.NoError(t, err)
wstepManager, err = microsoft_mdm.NewCertManager(ds, rawCert, rawKey)
require.NoError(t, err)
}
orgLogoStore, err := filesystem.NewOrgLogoStore(t.TempDir())
require.NoError(t, err)
svc, err := NewService(
ctx,
ds,
task,
rs,
logger,
osqlogger,
fleetConfig,
mailer,
c,
ssoStore,
lq,
ds,
failingPolicySet,
&fleet.NoOpGeoIP{},
enrollHostLimiter,
depStorage,
mdmStorage,
mdmPusher,
cronSchedulesService,
wstepManager,
scepConfigService,
digiCertService,
conditionalAccessMicrosoftProxy,
keyValueStore,
androidService,
orgLogoStore,
)
if err != nil {
panic(err)
}
if lic.IsPremium() {
if softwareInstallStore == nil {
// default to file-based
dir := t.TempDir()
store, err := filesystem.NewSoftwareInstallerStore(dir)
if err != nil {
panic(err)
}
softwareInstallStore = store
}
var androidModule android.Service
if len(opts) > 0 {
androidModule = opts[0].AndroidModule
}
svc, err = eeservice.NewService(
svc,
ds,
logger,
fleetConfig,
mailer,
c,
depStorage,
apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher),
ssoStore,
profMatcher,
softwareInstallStore,
bootstrapPackageStore,
softwareTitleIconStore,
distributedLock,
keyValueStore,
scepConfigService,
digiCertService,
androidModule,
estCAService,
)
if err != nil {
panic(err)
}
}
// Set up mock activity service for unit tests. When DBConns is provided,
// RunServerForTestsWithServiceWithDS will overwrite this with the real bounded context.
activityMock := &fleet_mock.MockActivityService{
NewActivityFunc: func(_ context.Context, _ *activity_api.User, _ activity_api.ActivityDetails) error {
return nil
},
}
svc.SetActivityService(activityMock)
if len(opts) > 0 {
opts[0].ActivityMock = activityMock
}
// Set up mock ACME service for unit tests. When DBConns is provided,
// RunServerForTestsWithServiceWithDS will overwrite this with the real service module.
svc.SetACMEService(&fleet_mock.MockACMEService{})
return svc, ctx
}
func newTestServiceWithClock(t *testing.T, ds fleet.Datastore, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, c clock.Clock) (fleet.Service, context.Context) {
testConfig := config.TestConfig()
return newTestServiceWithConfig(t, ds, testConfig, rs, lq, &TestServerOpts{
Clock: c,
})
}
func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User {
users := make(map[string]fleet.User)
// Map iteration is random so we sort and iterate using the testUsers keys.
var keys []string
for key := range testUsers {
keys = append(keys, key)
}
sort.Strings(keys)
userID := uint(1)
for _, key := range keys {
u := testUsers[key]
user := &fleet.User{
ID: userID, // We need to set this in case ds is a mocked Datastore.
Name: "Test Name " + u.Email,
Email: u.Email,
GlobalRole: u.GlobalRole,
}
err := user.SetPassword(u.PlaintextPassword, 10, 10)
require.Nil(t, err)
user, err = ds.NewUser(context.Background(), user)
require.Nil(t, err)
users[user.Email] = *user
userID++
}
return users
}
var testUsers = map[string]struct {
Email string
PlaintextPassword string
GlobalRole *string
}{
"admin1": {
PlaintextPassword: test.GoodPassword,
Email: TestAdminUserEmail,
GlobalRole: ptr.String(fleet.RoleAdmin),
},
"user1": {
PlaintextPassword: test.GoodPassword,
Email: TestMaintainerUserEmail,
GlobalRole: ptr.String(fleet.RoleMaintainer),
},
"user2": {
PlaintextPassword: test.GoodPassword,
Email: TestObserverUserEmail,
GlobalRole: ptr.String(fleet.RoleObserver),
},
}
func createEnrollSecrets(t *testing.T, count int) []*fleet.EnrollSecret {
secrets := make([]*fleet.EnrollSecret, count)
for i := 0; i < count; i++ {
secrets[i] = &fleet.EnrollSecret{Secret: fmt.Sprintf("testSecret%d", i)}
}
return secrets
}
type mockMailService struct {
SendEmailFn func(e fleet.Email) error
Invoked bool
}
func (svc *mockMailService) SendEmail(ctx context.Context, e fleet.Email) error {
svc.Invoked = true
return svc.SendEmailFn(e)
}
func (svc *mockMailService) CanSendEmail(smtpSettings fleet.SMTPSettings) bool {
return smtpSettings.SMTPConfigured
}
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
if len(opts) > 0 && opts[0].EnableCachedDS {
ds = cached_mysql.New(ds)
}
cfg := config.TestConfig()
if len(opts) > 0 && opts[0].FleetConfig != nil {
cfg = *opts[0].FleetConfig
}
svc, ctx := NewTestService(t, ds, cfg, opts...)
return RunServerForTestsWithServiceWithDS(t, ctx, ds, svc, opts...)
}
func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fleet.Datastore, svc fleet.Service,
opts ...*TestServerOpts,
) (map[string]fleet.User, *httptest.Server) {
var cfg config.FleetConfig
if len(opts) > 0 && opts[0].FleetConfig != nil {
cfg = *opts[0].FleetConfig
} else {
cfg = config.TestConfig()
}
users := map[string]fleet.User{}
if len(opts) == 0 || (len(opts) > 0 && !opts[0].SkipCreateTestUsers) {
users = createTestUsers(t, ds)
}
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
if len(opts) > 0 && opts[0].Logger != nil {
logger = opts[0].Logger
}
if len(opts) > 0 {
opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, android_service.GetRoutes(svc, opts[0].AndroidModule))
}
// Activity routes. If DBConns is provided, wire the real bounded context into
// the main handler. Otherwise, build a path-only stub from the same registration
// code and surface it to apiendpoints.Validate for catalog validation only.
var extraInitFeatureRoutes []apiendpoints.FeatureRouteFunc
if len(opts) > 0 && opts[0].DBConns != nil {
legacyAuthorizer, err := authz.NewAuthorizer()
require.NoError(t, err)
activityAuthorizer := authz.NewAuthorizerAdapter(legacyAuthorizer)
activityACLAdapter := activityacl.NewFleetServiceAdapter(svc, ds)
activitySvc, activityRoutesFn := activity_bootstrap.New(
opts[0].DBConns,
activityAuthorizer,
activityACLAdapter,
logger,
)
svc.SetActivityService(activitySvc)
activityAuthMiddleware := func(next endpoint.Endpoint) endpoint.Endpoint {
return auth.AuthenticatedUser(svc, next)
}
opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, activityRoutesFn(activityAuthMiddleware))
} else {
// DBConns is not available (e.g. mock-backed tests). The activity bounded context
// only dereferences its dependencies when an endpoint is actually served, so we
// can pass empty conns + nil deps just to extract the route declarations.
_, activityRoutesFn := activity_bootstrap.New(
&common_mysql.DBConnections{},
nil,
nil,
logger,
)
noopAuth := func(next endpoint.Endpoint) endpoint.Endpoint { return next }
extraInitFeatureRoutes = append(extraInitFeatureRoutes, apiendpoints.FeatureRouteFunc(activityRoutesFn(noopAuth)))
}
var mdmPusher nanomdm_push.Pusher
if len(opts) > 0 && opts[0].MDMPusher != nil {
mdmPusher = opts[0].MDMPusher
}
rootMux := http.NewServeMux()
memLimitStore, _ := memstore.New(0)
var limitStore throttled.GCRAStore = memLimitStore
var redisPool fleet.RedisPool
if len(opts) > 0 && opts[0].Pool != nil {
redisPool = opts[0].Pool
limitStore = &redis.ThrottledStore{
Pool: opts[0].Pool,
KeyPrefix: "ratelimit::",
}
} else {
redisPool = redistest.SetupRedis(t, t.Name(), false, false, false) // We are good to initalize a redis pool here as it is only called by integration tests
}
// Wire real ACME service module if DBConns is provided (overrides the mock set in newTestServiceWithConfig).
if len(opts) > 0 && opts[0].DBConns != nil {
var acmeOpts []acme_bootstrap.ServiceOption
if opts[0].ACMECertCA != nil && opts[0].ACMECertKey != nil {
rootCAPool := x509.NewCertPool()
rootCAPool.AddCert(opts[0].ACMECertCA)
acmeOpts = append(acmeOpts, acme_bootstrap.WithTestAppleRootCAs(rootCAPool))
}
acmeSigner := &acmeCSRSigner{signer: depot.NewSigner(opts[0].SCEPStorage, depot.WithValidityDays(365), depot.WithAllowRenewalDays(14))}
acmeSvc, acmeRoutes := acme_bootstrap.New(opts[0].DBConns, redisPool, acmeacl.NewFleetDatastoreAdapter(ds, acmeSigner), logger, acmeOpts...)
svc.SetACMEService(acmeSvc)
opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, acmeRoutes(log.Logged))
}
if len(opts) > 0 {
mdmStorage := opts[0].MDMStorage
scepStorage := opts[0].SCEPStorage
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher)
if mdmStorage != nil && scepStorage != nil {
vppInstaller := svc.(fleet.AppleMDMVPPInstaller)
checkInAndCommand := NewMDMAppleCheckinAndCommandService(ds, commander, vppInstaller, opts[0].License.IsPremium(), logger, redis_key_value.New(redisPool), svc.NewActivity)
checkInAndCommand.RegisterResultsHandler("InstalledApplicationList", NewInstalledApplicationListResultsHandler(ds, commander, logger, cfg.Server.VPPVerifyTimeout, cfg.Server.VPPVerifyRequestDelay, svc.NewActivity))
checkInAndCommand.RegisterResultsHandler(fleet.DeviceLocationCmdName, NewDeviceLocationResultsHandler(ds, commander, logger))
checkInAndCommand.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, NewSetRecoveryLockResultsHandler(ds, logger, svc.NewActivity))
err := RegisterAppleMDMProtocolServices(
rootMux,
cfg.MDM,
mdmStorage,
scepStorage,
logger,
checkInAndCommand,
&MDMAppleDDMService{
ds: ds,
logger: logger,
},
commander,
"https://test-url.com",
cfg,
)
require.NoError(t, err)
}
if opts[0].EnableSCEPProxy {
var timeout *time.Duration
if opts[0].SCEPConfigService != nil {
scepConfig, ok := opts[0].SCEPConfigService.(*scep.SCEPConfigService)
if ok {
// In tests, we share the same Timeout pointer between SCEPConfigService and SCEPProxy
timeout = scepConfig.Timeout
}
}
err := RegisterSCEPProxy(
rootMux,
ds,
logger,
timeout,
&cfg,
)
require.NoError(t, err)
}
}
if len(opts) > 0 && opts[0].WithDEPWebview {
frontendHandler := WithMDMEnrollmentMiddleware(svc, logger, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do nothing and return 200
w.WriteHeader(http.StatusOK)
}))
rootMux.Handle("/", frontendHandler)
}
var featureRoutes []endpointer.HandlerRoutesFunc
if len(opts) > 0 && len(opts[0].FeatureRoutes) > 0 {
featureRoutes = opts[0].FeatureRoutes
}
var extra []ExtraHandlerOption
extra = append(extra, WithLoginRateLimit(throttled.PerMin(1000)))
if len(opts) > 0 && opts[0].HostIdentity != nil {
require.NoError(t, hostidentity.RegisterSCEP(rootMux, opts[0].HostIdentity.SCEPStorage, ds, logger, &cfg))
var httpSigVerifier func(http.Handler) http.Handler
httpSigVerifier, err := httpsig.Middleware(ds, opts[0].HostIdentity.RequireHTTPMessageSignature,
logger.With("component", "http-sig-verifier"))
require.NoError(t, err)
extra = append(extra, WithHTTPSigVerifier(httpSigVerifier))
}
if len(opts) > 0 && opts[0].ConditionalAccess != nil {
require.NoError(t, condaccess.RegisterSCEP(ctx, rootMux, opts[0].ConditionalAccess.SCEPStorage, ds, logger, &cfg))
require.NoError(t, condaccess.RegisterIdP(rootMux, ds, logger, &cfg, limitStore))
}
var carveStore fleet.CarveStore = ds // In tests, we use MySQL as storage for carves.
apiHandler := MakeHandler(svc, cfg, logger, limitStore, redisPool, carveStore, featureRoutes, extra...)
// SCIM endpoints are served by a prefix-mounted handler (see scim.RegisterSCIM)
// that gorilla/mux can't introspect, so surface their routes to the validator
// explicitly. They're always in the catalog, regardless of opts[0].EnableSCIM.
extraInitFeatureRoutes = append(extraInitFeatureRoutes, scim.RegisterValidationRoutes)
if err := apiendpoints.Validate(apiHandler, extraInitFeatureRoutes...); err != nil {
t.Fatalf("error initializing API endpoints: %v", err)
}
rootMux.Handle("/api/", apiHandler)
var errHandler *errorstore.Handler
ctxErrHandler := ctxerr.FromContext(ctx)
if ctxErrHandler != nil {
errHandler = ctxErrHandler.(*errorstore.Handler)
}
debugHandler := MakeDebugHandler(svc, cfg, logger, errHandler, ds)
rootMux.Handle("/debug/", debugHandler)
rootMux.Handle("/enroll", ServeEndUserEnrollOTA(svc, "", ds, logger, false))
if len(opts) > 0 && opts[0].EnableSCIM {
require.NoError(t, scim.RegisterSCIM(rootMux, ds, svc, logger, &cfg))
rootMux.Handle("/api/v1/fleet/scim/details", apiHandler)
rootMux.Handle("/api/latest/fleet/scim/details", apiHandler)
}
server := httptest.NewUnstartedServer(rootMux)
server.Config = cfg.Server.DefaultHTTPServer(ctx, rootMux)
// WriteTimeout is set for security purposes.
// If we don't set it, (bugy or malignant) clients making long running
// requests could DDOS Fleet.
require.NotZero(t, server.Config.WriteTimeout)
if len(opts) > 0 && opts[0].HTTPServerConfig != nil {
server.Config = opts[0].HTTPServerConfig
// make sure we use the application handler we just created
server.Config.Handler = rootMux
}
server.Start()
t.Cleanup(func() {
server.Close()
})
return users, server
}
func NewTestService(t *testing.T, ds fleet.Datastore, cfg config.FleetConfig, opts ...*TestServerOpts) (fleet.Service, context.Context) {
var rs fleet.QueryResultStore
if len(opts) > 0 && opts[0].Rs != nil {
rs = opts[0].Rs
}
var lq fleet.LiveQueryStore
if len(opts) > 0 && opts[0].Lq != nil {
lq = opts[0].Lq
}
return newTestServiceWithConfig(t, ds, cfg, rs, lq, opts...)
}
func testSESPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Email = config.EmailConfig{EmailBackend: "ses"}
c.SES = config.SESConfig{
Region: "us-east-1",
AccessKeyID: "foo",
SecretAccessKey: "bar",
StsAssumeRoleArn: "baz",
SourceArn: "qux",
SenderDomain: "email.example.com",
}
return c
}
func testKinesisPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "kinesis"
c.Osquery.StatusLogPlugin = "kinesis"
c.Activity.AuditLogPlugin = "kinesis"
c.Kinesis = config.KinesisConfig{
Region: "us-east-1",
AccessKeyID: "foo",
SecretAccessKey: "bar",
StsAssumeRoleArn: "baz",
StatusStream: "test-status-stream",
ResultStream: "test-result-stream",
AuditStream: "test-audit-stream",
}
return c
}
func testFirehosePluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "firehose"
c.Osquery.StatusLogPlugin = "firehose"
c.Activity.AuditLogPlugin = "firehose"
c.Firehose = config.FirehoseConfig{
Region: "us-east-1",
AccessKeyID: "foo",
SecretAccessKey: "bar",
StsAssumeRoleArn: "baz",
StatusStream: "test-status-firehose",
ResultStream: "test-result-firehose",
AuditStream: "test-audit-firehose",
}
return c
}
func testLambdaPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "lambda"
c.Osquery.StatusLogPlugin = "lambda"
c.Activity.AuditLogPlugin = "lambda"
c.Lambda = config.LambdaConfig{
Region: "us-east-1",
AccessKeyID: "foo",
SecretAccessKey: "bar",
StsAssumeRoleArn: "baz",
ResultFunction: "result-func",
StatusFunction: "status-func",
AuditFunction: "audit-func",
}
return c
}
func testPubSubPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "pubsub"
c.Osquery.StatusLogPlugin = "pubsub"
c.Activity.AuditLogPlugin = "pubsub"
c.PubSub = config.PubSubConfig{
Project: "test",
StatusTopic: "status-topic",
ResultTopic: "result-topic",
AuditTopic: "audit-topic",
AddAttributes: false,
}
return c
}
func testStdoutPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "stdout"
c.Osquery.StatusLogPlugin = "stdout"
c.Activity.AuditLogPlugin = "stdout"
return c
}
func testUnrecognizedPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery = config.OsqueryConfig{
ResultLogPlugin: "bar",
StatusLogPlugin: "bar",
}
c.Activity.AuditLogPlugin = "bar"
return c
}
func assertBodyContains(t *testing.T, resp *http.Response, expected string) {
bodyBytes, err := io.ReadAll(resp.Body)
require.Nil(t, err)
bodyString := string(bodyBytes)
assert.Contains(t, bodyString, expected)
}
func getJSON(r *http.Response, target interface{}) error {
return json.NewDecoder(r.Body).Decode(target)
}
func assertErrorCodeAndMessage(t *testing.T, resp *http.Response, code int, message string) {
err := &fleet.Error{}
require.Nil(t, getJSON(resp, err))
assert.Equal(t, code, err.Code)
assert.Equal(t, message, err.Message)
}
type nopEnrollHostLimiter struct{}
func (nopEnrollHostLimiter) CanEnrollNewHost(ctx context.Context) (bool, error) {
return true, nil
}
func (nopEnrollHostLimiter) SyncEnrolledHostIDs(ctx context.Context) error {
return nil
}
func newMockAPNSPushProviderFactory() (*mock.APNSPushProviderFactory, *mock.APNSPushProvider) {
provider := &mock.APNSPushProvider{}
provider.PushFunc = mockSuccessfulPush
factory := &mock.APNSPushProviderFactory{}
factory.NewPushProviderFunc = func(*tls.Certificate) (push.PushProvider, error) {
return provider, nil
}
return factory, provider
}
func mockSuccessfulPush(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
res := make(map[string]*push.Response, len(pushes))
for _, p := range pushes {
res[p.Token.String()] = &push.Response{
Id: uuid.New().String(),
Err: nil,
}
}
return res, nil
}
func mdmConfigurationRequiredEndpoints() []struct {
method, path string
deviceAuthenticated bool
premiumOnly bool
} {
return []struct {
method, path string
deviceAuthenticated bool
premiumOnly bool
}{
{"POST", "/api/latest/fleet/mdm/apple/enqueue", false, false},
{"GET", "/api/latest/fleet/mdm/apple/commandresults", false, false},
{"GET", "/api/latest/fleet/mdm/apple/installers/1", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/installers/1", false, false},
{"GET", "/api/latest/fleet/mdm/apple/installers", false, false},
{"GET", "/api/latest/fleet/mdm/apple/devices", false, false},
{"GET", "/api/latest/fleet/mdm/apple/profiles", false, false},
{"GET", "/api/latest/fleet/mdm/apple/profiles/1", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/profiles/1", false, false},
{"GET", "/api/latest/fleet/mdm/apple/profiles/summary", false, false},
{"PATCH", "/api/latest/fleet/mdm/hosts/1/unenroll", false, false},
{"DELETE", "/api/latest/fleet/hosts/1/mdm", false, false},
{"GET", "/api/latest/fleet/mdm/hosts/1/profiles", false, true},
{"GET", "/api/latest/fleet/hosts/1/configuration_profiles", false, true},
{"POST", "/api/latest/fleet/mdm/hosts/1/lock", false, false},
{"POST", "/api/latest/fleet/mdm/hosts/1/wipe", false, false},
{"PATCH", "/api/latest/fleet/mdm/apple/settings", false, false},
{"GET", "/api/latest/fleet/mdm/apple", false, false},
{"GET", "/api/latest/fleet/apns", false, false},
{"GET", apple_mdm.EnrollPath + "?token=test", false, false},
{"GET", apple_mdm.InstallerPath + "?token=test", false, false},
{"GET", "/api/latest/fleet/mdm/setup/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, true},
{"GET", "/api/latest/fleet/setup_experience/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, true},
{"DELETE", "/api/latest/fleet/mdm/setup/eula/token", false, true},
{"DELETE", "/api/latest/fleet/setup_experience/eula/token", false, true},
{"GET", "/api/latest/fleet/mdm/setup/eula/metadata", false, true},
{"GET", "/api/latest/fleet/setup_experience/eula/metadata", false, true},
{"GET", "/api/latest/fleet/mdm/apple/setup/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/setup/eula/token", false, false},
{"GET", "/api/latest/fleet/mdm/apple/setup/eula/metadata", false, false},
{"GET", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false},
{"GET", "/api/latest/fleet/enrollment_profiles/automatic", false, false},
{"POST", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false},
{"POST", "/api/latest/fleet/enrollment_profiles/automatic", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false},
{"DELETE", "/api/latest/fleet/enrollment_profiles/automatic", false, false},
{"POST", "/api/latest/fleet/device/%s/migrate_mdm", true, true},
{"POST", "/api/latest/fleet/mdm/apple/profiles/preassign", false, true},
{"POST", "/api/latest/fleet/mdm/apple/profiles/match", false, true},
{"POST", "/api/latest/fleet/mdm/commands/run", false, false},
{"POST", "/api/latest/fleet/commands/run", false, false},
{"GET", "/api/latest/fleet/mdm/commandresults", false, false},
{"GET", "/api/latest/fleet/commands/results", false, false},
{"GET", "/api/latest/fleet/mdm/commands", false, false},
{"GET", "/api/latest/fleet/commands", false, false},
{"POST", "/api/fleet/orbit/disk_encryption_key", false, false},
{"GET", "/api/latest/fleet/mdm/profiles/1", false, false},
{"GET", "/api/latest/fleet/configuration_profiles/1", false, false},
// TODO: those endpoints accept multipart/form data that gets
// parsed before the MDM check, we need to refactor this
// function to return more information to the caller, or find a
// better way to test these endpoints.
// {"POST", "/api/latest/fleet/mdm/profiles", false, false},
// {"POST", "/api/latest/fleet/configuration_profiles", false, false},
// {"POST", "/api/latest/fleet/mdm/setup/eula"},
// {"POST", "/api/latest/fleet/setup_experience/eula"},
// {"POST", "/api/latest/fleet/mdm/bootstrap", false, true},
// {"POST", "/api/latest/fleet/bootstrap", false, true},
{"GET", "/api/latest/fleet/mdm/profiles", false, false},
{"GET", "/api/latest/fleet/configuration_profiles", false, false},
{"GET", "/api/latest/fleet/mdm/manual_enrollment_profile", false, true},
{"GET", "/api/latest/fleet/enrollment_profiles/manual", false, true},
{"GET", "/api/latest/fleet/mdm/bootstrap/1/metadata", false, true},
{"GET", "/api/latest/fleet/bootstrap/1/metadata", false, true},
{"DELETE", "/api/latest/fleet/mdm/bootstrap/1", false, true},
{"DELETE", "/api/latest/fleet/bootstrap/1", false, true},
{"GET", "/api/latest/fleet/mdm/bootstrap?token=1", false, true},
{"GET", "/api/latest/fleet/bootstrap?token=1", false, true},
{"GET", "/api/latest/fleet/mdm/bootstrap/summary", false, true},
{"GET", "/api/latest/fleet/mdm/apple/bootstrap/summary", false, true},
{"GET", "/api/latest/fleet/bootstrap/summary", false, true},
{"PATCH", "/api/latest/fleet/mdm/apple/setup", false, true},
{"PATCH", "/api/latest/fleet/setup_experience", false, true},
{"POST", "/api/fleet/orbit/setup_experience/status", false, true},
{"POST", "/api/latest/fleet/software/web_apps", false, true},
}
}
func windowsMDMConfigurationRequiredEndpoints() []string {
return []string{
"/api/fleet/orbit/disk_encryption_key",
}
}
func androidMDMConfigurationRequiredEndpoints() []string {
return []string{
"/api/latest/fleet/software/web_apps",
}
}
// getURLSchemas returns a list of all valid URI schemas
func getURISchemas() []string {
return []string{
"aaa",
"aaas",
"about",
"acap",
"acct",
"acd",
"acr",
"adiumxtra",
"adt",
"afp",
"afs",
"aim",
"amss",
"android",
"appdata",
"apt",
"ar",
"ark",
"at",
"attachment",
"aw",
"barion",
"bb",
"beshare",
"bitcoin",
"bitcoincash",
"blob",
"bolo",
"browserext",
"cabal",
"calculator",
"callto",
"cap",
"cast",
"casts",
"chrome",
"chrome-extension",
"cid",
"coap",
"coap+tcp",
"coap+ws",
"coaps",
"coaps+tcp",
"coaps+ws",
"com-eventbrite-attendee",
"content",
"content-type",
"crid",
"cstr",
"cvs",
"dab",
"dat",
"data",
"dav",
"dhttp",
"diaspora",
"dict",
"did",
"dis",
"dlna-playcontainer",
"dlna-playsingle",
"dns",
"dntp",
"doi",
"dpp",
"drm",
"drop",
"dtmi",
"dtn",
"dvb",
"dvx",
"dweb",
"ed2k",
"eid",
"elsi",
"embedded",
"ens",
"ethereum",
"example",
"facetime",
"fax",
"feed",
"feedready",
"fido",
"file",
"filesystem",
"finger",
"first-run-pen-experience",
"fish",
"fm",
"ftp",
"fuchsia-pkg",
"geo",
"gg",
"git",
"gitoid",
"gizmoproject",
"go",
"gopher",
"graph",
"grd",
"gtalk",
"h323",
"ham",
"hcap",
"hcp",
"http",
"https",
"hxxp",
"hxxps",
"hydrazone",
"hyper",
"iax",
"icap",
"icon",
"im",
"imap",
"info",
"iotdisco",
"ipfs",
"ipn",
"ipns",
"ipp",
"ipps",
"irc",
"irc6",
"ircs",
"iris",
"iris.beep",
"iris.lwz",
"iris.xpc",
"iris.xpcs",
"isostore",
"itms",
"jabber",
"jar",
"jms",
"keyparc",
"lastfm",
"lbry",
"ldap",
"ldaps",
"leaptofrogans",
"lorawan",
"lpa",
"lvlt",
"magnet",
"mailserver",
"mailto",
"maps",
"market",
"matrix",
"message",
"microsoft.windows.camera",
"microsoft.windows.camera.multipicker",
"microsoft.windows.camera.picker",
"mid",
"mms",
"modem",
"mongodb",
"moz",
"ms-access",
"ms-appinstaller",
"ms-browser-extension",
"ms-calculator",
"ms-drive-to",
"ms-enrollment",
"ms-excel",
"ms-eyecontrolspeech",
"ms-gamebarservices",
"ms-gamingoverlay",
"ms-getoffice",
"ms-help",
"ms-infopath",
"ms-inputapp",
"ms-launchremotedesktop",
"ms-lockscreencomponent-config",
"ms-media-stream-id",
"ms-meetnow",
"ms-mixedrealitycapture",
"ms-mobileplans",
"ms-newsandinterests",
"ms-officeapp",
"ms-people",
"ms-project",
"ms-powerpoint",
"ms-publisher",
"ms-remotedesktop",
"ms-remotedesktop-launch",
"ms-restoretabcompanion",
"ms-screenclip",
"ms-screensketch",
"ms-search",
"ms-search-repair",
"ms-secondary-screen-controller",
"ms-secondary-screen-setup",
"ms-settings",
"ms-settings-airplanemode",
"ms-settings-bluetooth",
"ms-settings-camera",
"ms-settings-cellular",
"ms-settings-cloudstorage",
"ms-settings-connectabledevices",
"ms-settings-displays-topology",
"ms-settings-emailandaccounts",
"ms-settings-language",
"ms-settings-location",
"ms-settings-lock",
"ms-settings-nfctransactions",
"ms-settings-notifications",
"ms-settings-power",
"ms-settings-privacy",
"ms-settings-proximity",
"ms-settings-screenrotation",
"ms-settings-wifi",
"ms-settings-workplace",
"ms-spd",
"ms-stickers",
"ms-sttoverlay",
"ms-transit-to",
"ms-useractivityset",
"ms-virtualtouchpad",
"ms-visio",
"ms-walk-to",
"ms-whiteboard",
"ms-whiteboard-cmd",
"ms-word",
"msnim",
"msrp",
"msrps",
"mss",
"mt",
"mtqp",
"mumble",
"mupdate",
"mvn",
"news",
"nfs",
"ni",
"nih",
"nntp",
"notes",
"num",
"ocf",
"oid",
"onenote",
"onenote-cmd",
"opaquelocktoken",
"openpgp4fpr",
"otpauth",
"p1",
"pack",
"palm",
"paparazzi",
"payment",
"payto",
"pkcs11",
"platform",
"pop",
"pres",
"prospero",
"proxy",
"pwid",
"psyc",
"pttp",
"qb",
"query",
"quic-transport",
"redis",
"rediss",
"reload",
"res",
"resource",
"rmi",
"rsync",
"rtmfp",
"rtmp",
"rtsp",
"rtsps",
"rtspu",
"sarif",
"secondlife",
"secret-token",
"service",
"session",
"sftp",
"sgn",
"shc",
"shttp",
"sieve",
"simpleledger",
"simplex",
"sip",
"sips",
"skype",
"smb",
"smp",
"sms",
"smtp",
"snews",
"snmp",
"soap.beep",
"soap.beeps",
"soldat",
"spiffe",
"spotify",
"ssb",
"ssh",
"starknet",
"steam",
"stun",
"stuns",
"submit",
"svn",
"swh",
"swid",
"swidpath",
"tag",
"taler",
"teamspeak",
"tel",
"teliaeid",
"telnet",
"tftp",
"things",
"thismessage",
"tip",
"tn3270",
"tool",
"turn",
"turns",
"tv",
"udp",
"unreal",
"upt",
"urn",
"ut2004",
"uuid-in-package",
"v-event",
"vemmi",
"ventrilo",
"ves",
"videotex",
"vnc",
"view-source",
"vscode",
"vscode-insiders",
"vsls",
"w3",
"wais",
"web3",
"wcr",
"webcal",
"web+ap",
"wifi",
"wpid",
"ws",
"wss",
"wtai",
"wyciwyg",
"xcon",
"xcon-userid",
"xfire",
"xmlrpc.beep",
"xmlrpc.beeps",
"xmpp",
"xri",
"ymsgr",
"z39.50",
"z39.50r",
"z39.50s",
}
}
func createAndroidDeviceID(name string) string {
return "enterprises/mock-enterprise-id/devices/" + name
}
func statusReportMessageWithEnterpriseSpecificID(t *testing.T, deviceInfo androidmanagement.Device, enterpriseSpecificID string) *android.PubSubMessage {
return messageWithAndroidIdentifiers(t, android.PubSubStatusReport, deviceInfo, enterpriseSpecificID, "")
}
func enrollmentMessageWithEnterpriseSpecificID(t *testing.T, deviceInfo androidmanagement.Device, enterpriseSpecificID string) *android.PubSubMessage {
return messageWithAndroidIdentifiers(t, android.PubSubEnrollment, deviceInfo, enterpriseSpecificID, "")
}
func statusReportMessageWithSerialNumber(t *testing.T, deviceInfo androidmanagement.Device, serialNumber string) *android.PubSubMessage {
return messageWithAndroidIdentifiers(t, android.PubSubStatusReport, deviceInfo, "", serialNumber)
}
func enrollmentMessageWithSerialNumber(t *testing.T, deviceInfo androidmanagement.Device, serialNumber string) *android.PubSubMessage {
return messageWithAndroidIdentifiers(t, android.PubSubEnrollment, deviceInfo, "", serialNumber)
}
func messageWithAndroidIdentifiers(t *testing.T, notificationType android.NotificationType, deviceInfo androidmanagement.Device, enterpriseSpecificID, serialNumber string) *android.PubSubMessage {
if serialNumber == "" && enterpriseSpecificID == "" || serialNumber != "" && enterpriseSpecificID != "" {
t.Fatalf("exactly one of serialNumber or enterpriseSpecificID must be provided")
}
deviceInfo.HardwareInfo = &androidmanagement.HardwareInfo{
Brand: "TestBrand",
Model: "TestModel",
Hardware: "test-hardware",
}
if enterpriseSpecificID != "" {
deviceInfo.Ownership = android_service.DeviceOwnershipPersonallyOwned
deviceInfo.HardwareInfo.EnterpriseSpecificId = enterpriseSpecificID
deviceInfo.HardwareInfo.SerialNumber = enterpriseSpecificID
}
if serialNumber != "" {
deviceInfo.Ownership = android_service.DeviceOwnershipCompanyOwned
deviceInfo.HardwareInfo.SerialNumber = serialNumber
}
deviceInfo.SoftwareInfo = &androidmanagement.SoftwareInfo{
AndroidBuildNumber: "test-build",
AndroidVersion: "1",
}
deviceInfo.MemoryInfo = &androidmanagement.MemoryInfo{
TotalRam: int64(8 * 1024 * 1024 * 1024), // 8GB RAM in bytes
TotalInternalStorage: int64(64 * 1024 * 1024 * 1024), // 64GB system partition
}
deviceInfo.MemoryEvents = []*androidmanagement.MemoryEvent{
{
EventType: "EXTERNAL_STORAGE_DETECTED",
ByteCount: int64(64 * 1024 * 1024 * 1024), // 64GB external/built-in storage total capacity
CreateTime: "2024-01-15T09:00:00Z",
},
{
EventType: "INTERNAL_STORAGE_MEASURED",
ByteCount: int64(10 * 1024 * 1024 * 1024), // 10GB free in system partition
CreateTime: "2024-01-15T10:00:00Z",
},
{
EventType: "EXTERNAL_STORAGE_MEASURED",
ByteCount: int64(25 * 1024 * 1024 * 1024), // 25GB free in external/built-in storage
CreateTime: "2024-01-15T10:00:00Z",
},
}
data, err := json.Marshal(deviceInfo)
require.NoError(t, err)
encodedData := base64.StdEncoding.EncodeToString(data)
return &android.PubSubMessage{
Attributes: map[string]string{
"notificationType": string(notificationType),
},
Data: encodedData,
}
}
type fmaTestState struct {
version string
installerBytes []byte
sha256 string
installerPath string
patchQuery string
}
func (s *fmaTestState) ComputeSHA(b []byte) {
h := sha256.New()
h.Write(b)
s.sha256 = hex.EncodeToString(h.Sum(nil))
}
func startFMAServers(t *testing.T, ds fleet.Datastore, states map[string]*fmaTestState) {
if len(states) == 0 {
states = make(map[string]*fmaTestState, 1)
states["/zoom/windows.json"] = &fmaTestState{
version: "1.0",
installerBytes: []byte("xyz"),
installerPath: "/zoom.msi",
}
}
statesByInstallerPath := make(map[string]*fmaTestState, len(states))
for _, state := range states {
state.ComputeSHA(state.installerBytes)
statesByInstallerPath[state.installerPath] = state
}
var downloadMu sync.Mutex
// Mock installer server — routes by path to serve per-FMA bytes.
installerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
downloadMu.Lock()
defer downloadMu.Unlock()
state, found := statesByInstallerPath[r.URL.Path]
if !found {
http.NotFound(w, r)
return
}
_, _ = w.Write(state.installerBytes)
}))
// Locate the repo's apps.json so the manifest server can serve it.
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatalf("could not locate myself to serve apps.json file")
}
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(thisFile)))
appsJSONPath := filepath.Join(repoRoot, "ee", "maintained-apps", "outputs", "apps.json")
manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/apps.json" {
b, err := os.ReadFile(appsJSONPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, _ = w.Write(b)
return
}
var state *fmaTestState
state, found := states[r.URL.Path]
if !found {
http.NotFound(w, r)
return
}
versions := []*ma.FMAManifestApp{
{
Version: state.version,
Queries: ma.FMAQueries{
Exists: "SELECT 1 FROM osquery_info;",
Patched: state.patchQuery,
},
InstallerURL: installerServer.URL + state.installerPath,
InstallScriptRef: "foobaz",
UninstallScriptRef: "foobaz",
SHA256: state.sha256,
DefaultCategories: []string{"Productivity"},
},
}
manifest := ma.FMAManifestFile{
Versions: versions,
Refs: map[string]string{"foobaz": "Hello World!"},
}
require.NoError(t, json.NewEncoder(w).Encode(manifest))
}))
t.Cleanup(func() {
manifestServer.Close()
installerServer.Close()
})
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL, t)
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", manifestServer.URL, t)
require.NoError(t, maintained_apps.SyncAppsList(t.Context(), ds))
}
// acmeCSRSigner adapts a depot.Signer to the acme.CSRSigner interface.
type acmeCSRSigner struct {
signer *depot.Signer
}
func (a *acmeCSRSigner) SignCSR(_ context.Context, csr *x509.CertificateRequest) (*x509.Certificate, error) {
return a.signer.Signx509CSR(csr)
}
// mockRoundTripper is a custom http.RoundTripper that redirects requests to a mock server.
type mockRoundTripper struct {
mockServer string
origBaseURL string
next http.RoundTripper
}
func (rt *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if strings.Contains(req.URL.String(), rt.origBaseURL) {
path := strings.TrimPrefix(req.URL.Path, "/")
newURL := fmt.Sprintf("%s/%s", rt.mockServer, path)
newReq, err := http.NewRequestWithContext(req.Context(), req.Method, newURL, req.Body) //nolint:gosec // test helper, URL is from mock server
if err != nil {
return nil, err
}
newReq.Header = req.Header
return rt.next.RoundTrip(newReq)
}
return rt.next.RoundTrip(req)
}