mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #38536 This PR moves all logic to create new activities to activity bounded context. The old service and ActivityModule methods are not facades that route to the new activity bounded context. The facades will be removed in a subsequent PR. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added webhook support for activity events with configurable endpoint and enable/disable settings. * Enhanced automation-initiated activity creation without requiring a user context. * Improved activity service architecture with centralized creation and management. * **Improvements** * Refactored activity creation to use a dedicated service layer for better separation of concerns. * Added support for host-specific and automation-originated activities. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
854 lines
29 KiB
Go
854 lines
29 KiB
Go
package testing_utils
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/smithy-go/ptr"
|
|
"github.com/docker/go-units"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/cached_mysql"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
|
"github.com/fleetdm/fleet/v4/server/dev_mode"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/android"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
|
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
mock2 "github.com/fleetdm/fleet/v4/server/mock/mdm"
|
|
"github.com/fleetdm/fleet/v4/server/service"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
teamName = "Team Test"
|
|
fleetServerURL = "https://fleet.example.com"
|
|
orgName = "GitOps Test"
|
|
)
|
|
|
|
// RunServerWithMockedDS runs the fleet server with several mocked DS methods.
|
|
//
|
|
// NOTE: Assumes the current session is always from the admin user (see ds.SessionByKeyFunc below).
|
|
func RunServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*httptest.Server, *mock.Store) {
|
|
ds := new(mock.Store)
|
|
var users []*fleet.User
|
|
var admin *fleet.User
|
|
ds.NewUserFunc = func(ctx context.Context, user *fleet.User) (*fleet.User, error) {
|
|
if user.GlobalRole != nil && *user.GlobalRole == fleet.RoleAdmin {
|
|
admin = user
|
|
}
|
|
users = append(users, user)
|
|
return user, nil
|
|
}
|
|
ds.SessionByKeyFunc = func(ctx context.Context, key string) (*fleet.Session, error) {
|
|
return &fleet.Session{
|
|
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
|
|
ID: 1,
|
|
AccessedAt: time.Now(),
|
|
UserID: admin.ID,
|
|
Key: key,
|
|
}, nil
|
|
}
|
|
ds.MarkSessionAccessedFunc = func(ctx context.Context, session *fleet.Session) error {
|
|
return nil
|
|
}
|
|
ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
|
|
return admin, nil
|
|
}
|
|
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
|
return users, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.NewGlobalPolicyFunc = func(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
|
|
return &fleet.Policy{
|
|
PolicyData: fleet.PolicyData{
|
|
Name: args.Name,
|
|
Query: args.Query,
|
|
Critical: args.Critical,
|
|
Platform: args.Platform,
|
|
Description: args.Description,
|
|
Resolution: &args.Resolution,
|
|
AuthorID: authorID,
|
|
ConditionalAccessBypassEnabled: ptr.Bool(true),
|
|
},
|
|
}, nil
|
|
}
|
|
ds.NewScriptFunc = func(ctx context.Context, script *fleet.Script) (*fleet.Script, error) {
|
|
if !strings.HasPrefix(script.ScriptContents, "#!/") {
|
|
return nil, errors.New("script not uploaded properly")
|
|
}
|
|
return &fleet.Script{
|
|
ID: 1,
|
|
}, nil
|
|
}
|
|
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) { return nil, nil }
|
|
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
|
|
require.NoError(t, err)
|
|
certPEM, keyPEM, tokenBytes, err := mysql.GenerateTestABMAssets(t)
|
|
require.NoError(t, err)
|
|
ds.GetAllMDMConfigAssetsHashesFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) {
|
|
return map[fleet.MDMAssetName]string{
|
|
fleet.MDMAssetABMCert: "abmcert",
|
|
fleet.MDMAssetABMKey: "abmkey",
|
|
fleet.MDMAssetABMTokenDeprecated: "abmtoken",
|
|
fleet.MDMAssetAPNSCert: "apnscert",
|
|
fleet.MDMAssetAPNSKey: "apnskey",
|
|
fleet.MDMAssetCACert: "scepcert",
|
|
fleet.MDMAssetCAKey: "scepkey",
|
|
}, nil
|
|
}
|
|
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
|
_ sqlx.QueryerContext,
|
|
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
fleet.MDMAssetABMCert: {Name: fleet.MDMAssetABMCert, Value: certPEM},
|
|
fleet.MDMAssetABMKey: {Name: fleet.MDMAssetABMKey, Value: keyPEM},
|
|
fleet.MDMAssetABMTokenDeprecated: {Name: fleet.MDMAssetABMTokenDeprecated, Value: tokenBytes},
|
|
fleet.MDMAssetAPNSCert: {Name: fleet.MDMAssetAPNSCert, Value: apnsCert},
|
|
fleet.MDMAssetAPNSKey: {Name: fleet.MDMAssetAPNSKey, Value: apnsKey},
|
|
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: certPEM},
|
|
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: keyPEM},
|
|
}, nil
|
|
}
|
|
|
|
ds.ApplyYaraRulesFunc = func(context.Context, []fleet.YaraRule) error {
|
|
return nil
|
|
}
|
|
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
|
|
return nil
|
|
}
|
|
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ConditionalAccessBypassedAtFunc = func(ctx context.Context, hostID uint) (*time.Time, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
|
|
return &fleet.GroupedCertificateAuthorities{}, nil
|
|
}
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
ds.GetEnterpriseFunc = func(ctx context.Context) (*android.Enterprise, error) {
|
|
return nil, nil
|
|
}
|
|
var cachedDS fleet.Datastore
|
|
if len(opts) > 0 && opts[0].NoCacheDatastore {
|
|
cachedDS = ds
|
|
} else {
|
|
cachedDS = cached_mysql.New(ds)
|
|
}
|
|
_, server := service.RunServerForTestsWithDS(t, cachedDS, opts...)
|
|
os.Setenv("FLEET_SERVER_ADDRESS", server.URL)
|
|
|
|
return server, ds
|
|
}
|
|
|
|
func getPathRelative(relativePath string) string {
|
|
_, currentFile, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
panic("failed to get runtime caller info")
|
|
}
|
|
sourceDir := filepath.Dir(currentFile)
|
|
return filepath.Join(sourceDir, relativePath)
|
|
}
|
|
|
|
func ServeMDMBootstrapPackage(t *testing.T, pkgPath, pkgName string) (*httptest.Server, int) {
|
|
pkgBytes, err := os.ReadFile(pkgPath)
|
|
require.NoError(t, err)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(pkgBytes)))
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, pkgName))
|
|
if n, err := w.Write(pkgBytes); err != nil {
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(pkgBytes), n)
|
|
}
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv, len(pkgBytes)
|
|
}
|
|
|
|
func StartSoftwareInstallerServer(t *testing.T) {
|
|
// load the ruby installer to use as base bytes to repeat for the "too large" case
|
|
b, err := os.ReadFile(getPathRelative("../../../../server/service/testdata/software-installers/ruby.deb"))
|
|
require.NoError(t, err)
|
|
|
|
// get the base dir of all installers
|
|
baseDir := getPathRelative("../../../../server/service/testdata/software-installers/")
|
|
|
|
// start the web server that will serve the installer
|
|
srv := httptest.NewServer(
|
|
http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "notfound"):
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
case strings.HasSuffix(r.URL.Path, ".txt"):
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
_, _ = w.Write([]byte(`a simple text file`))
|
|
return
|
|
case strings.Contains(r.URL.Path, "toolarge"):
|
|
w.Header().Set("Content-Type", "application/vnd.debian.binary-package")
|
|
var sz int
|
|
for sz < 513*units.MiB {
|
|
n, _ := w.Write(b)
|
|
sz += n
|
|
}
|
|
case strings.Contains(r.URL.Path, "other.deb"):
|
|
// serve same content as ruby.deb
|
|
w.Header().Set("Content-Type", "application/vnd.debian.binary-package")
|
|
_, _ = w.Write(b)
|
|
default:
|
|
http.ServeFile(w, r, filepath.Join(baseDir, filepath.Base(r.URL.Path)))
|
|
}
|
|
},
|
|
),
|
|
)
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("SOFTWARE_INSTALLER_URL", srv.URL)
|
|
}
|
|
|
|
func SetupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, map[string]**fleet.Team) {
|
|
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
|
|
require.NoError(t, err)
|
|
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
|
|
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
|
|
fleetCfg := config.TestConfig()
|
|
config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, getPathRelative("../../../../server/service/testdata"))
|
|
|
|
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
|
_, ds := RunServerWithMockedDS(
|
|
t, &service.TestServerOpts{
|
|
MDMStorage: new(mock2.MDMAppleStore),
|
|
MDMPusher: MockPusher{},
|
|
FleetConfig: &fleetCfg,
|
|
License: license,
|
|
NoCacheDatastore: true,
|
|
KeyValueStore: NewMemKeyValueStore(),
|
|
},
|
|
)
|
|
|
|
// Mock appConfig
|
|
savedAppConfig := &fleet.AppConfig{
|
|
MDM: fleet.MDM{
|
|
EnabledAndConfigured: true,
|
|
AndroidEnabledAndConfigured: true,
|
|
},
|
|
}
|
|
AddLabelMocks(ds)
|
|
|
|
// Add DefaultTeamConfig mocks for enterprise features
|
|
ds.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
|
|
return &fleet.TeamConfig{}, nil
|
|
}
|
|
ds.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
|
|
return nil
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appConfigCopy := *savedAppConfig
|
|
return &appConfigCopy, nil
|
|
}
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
|
appConfigCopy := *config
|
|
savedAppConfig = &appConfigCopy
|
|
return nil
|
|
}
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam, _ map[string]uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.ListSoftwareAutoUpdateSchedulesFunc = func(ctx context.Context, teamID uint, source string, optionalFilter ...fleet.SoftwareAutoUpdateScheduleFilter) ([]fleet.SoftwareAutoUpdateSchedule, error) {
|
|
return []fleet.SoftwareAutoUpdateSchedule{}, nil
|
|
}
|
|
|
|
savedTeams := map[string]**fleet.Team{}
|
|
|
|
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
|
return nil
|
|
}
|
|
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
|
|
return nil
|
|
}
|
|
ds.ApplyQueriesFunc = func(
|
|
ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
|
|
) error {
|
|
return nil
|
|
}
|
|
ds.BatchSetMDMProfilesFunc = func(
|
|
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
|
macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
|
|
return []fleet.ScriptResponse{}, nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
|
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
|
|
return nil
|
|
}
|
|
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
|
|
return nil
|
|
}
|
|
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
|
|
return nil
|
|
}
|
|
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
|
|
return &fleet.MDMAppleBootstrapPackage{}, nil
|
|
}
|
|
ds.DeleteMDMAppleBootstrapPackageFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
|
return nil, nil
|
|
}
|
|
ds.DeleteMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) error {
|
|
return nil
|
|
}
|
|
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
|
|
return true, nil
|
|
}
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
|
require.ElementsMatch(t, names, []string{fleet.BuiltinLabelMacOS14Plus})
|
|
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
|
|
}
|
|
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
|
|
ds.ListTeamPoliciesFunc = func(
|
|
ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
|
|
) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
|
|
return nil, nil, nil
|
|
}
|
|
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
|
|
if savedTeams != nil {
|
|
var result []*fleet.Team
|
|
for _, t := range savedTeams {
|
|
result = append(result, *t)
|
|
}
|
|
return result, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
|
|
summary := make([]*fleet.TeamSummary, 0, len(savedTeams))
|
|
for _, team := range savedTeams {
|
|
summary = append(summary, &fleet.TeamSummary{
|
|
ID: (*team).ID,
|
|
Name: (*team).Name,
|
|
Description: (*team).Description,
|
|
})
|
|
}
|
|
return summary, nil
|
|
}
|
|
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
|
|
return nil, 0, 0, nil, nil
|
|
}
|
|
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, p fleet.MDMAppleConfigProfile, vars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
|
|
return nil, nil
|
|
}
|
|
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
|
job.ID = 1
|
|
return job, nil
|
|
}
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
team.ID = uint(len(savedTeams) + 1) //nolint:gosec // dismiss G115
|
|
savedTeams[team.Name] = &team
|
|
return team, nil
|
|
}
|
|
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
for _, tm := range savedTeams {
|
|
if (*tm).ID == tid {
|
|
return *tm, nil
|
|
}
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
|
if tid == 0 {
|
|
return &fleet.TeamLite{}, nil
|
|
}
|
|
for _, tm := range savedTeams {
|
|
if (*tm).ID == tid {
|
|
teamToCopy := *tm
|
|
return &fleet.TeamLite{
|
|
ID: teamToCopy.ID,
|
|
Filename: teamToCopy.Filename,
|
|
CreatedAt: teamToCopy.CreatedAt,
|
|
Name: teamToCopy.Name,
|
|
Description: teamToCopy.Description,
|
|
Config: teamToCopy.Config.ToLite(),
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
for _, tm := range savedTeams {
|
|
if (*tm).Name == name {
|
|
return *tm, nil
|
|
}
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
|
|
for _, tm := range savedTeams {
|
|
if (*tm).Filename != nil && *(*tm).Filename == filename {
|
|
return *tm, nil
|
|
}
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
savedTeams[team.Name] = &team
|
|
return team, nil
|
|
}
|
|
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (
|
|
*fleet.MDMAppleDeclaration, error,
|
|
) {
|
|
declaration.DeclarationUUID = uuid.NewString()
|
|
return declaration, nil
|
|
}
|
|
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
|
return nil
|
|
}
|
|
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
|
return nil
|
|
}
|
|
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
ds.InsertVPPTokenFunc = func(ctx context.Context, tok *fleet.VPPTokenData) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{}, nil
|
|
}
|
|
ds.GetVPPTokenFunc = func(ctx context.Context, tokenID uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{}, err
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{}, nil
|
|
}
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return nil, nil
|
|
}
|
|
ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{}, nil
|
|
}
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}}, nil
|
|
}
|
|
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions,
|
|
tmFilter fleet.TeamFilter,
|
|
) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
|
|
return nil, 0, nil, nil
|
|
}
|
|
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
|
return nil
|
|
}
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{}, nil
|
|
}
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{}, nil
|
|
}
|
|
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
|
|
return nil
|
|
}
|
|
ds.SetSetupExperienceScriptFunc = func(ctx context.Context, script *fleet.Script) error {
|
|
return nil
|
|
}
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
|
return document, nil, nil
|
|
}
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return nil, ¬FoundError{} // No existing EULA
|
|
}
|
|
ds.BatchApplyCertificateAuthoritiesFunc = func(ctx context.Context, ops fleet.CertificateAuthoritiesBatchOperations) error {
|
|
return nil
|
|
}
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
|
|
t.Setenv("ORG_NAME", orgName)
|
|
t.Setenv("TEST_TEAM_NAME", teamName)
|
|
t.Setenv("APPLE_BM_DEFAULT_TEAM", teamName)
|
|
|
|
return ds, &savedAppConfig, savedTeams
|
|
}
|
|
|
|
type AppleVPPConfigSrvConf struct {
|
|
Assets []vpp.Asset
|
|
SerialNumbers []string
|
|
}
|
|
|
|
func StartVPPApplyServer(t *testing.T, config *AppleVPPConfigSrvConf) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.URL.Path, "associate") {
|
|
var associations vpp.AssociateAssetsRequest
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
if err := decoder.Decode(&associations); err != nil {
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if len(associations.Assets) == 0 {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
res := vpp.ErrorResponse{
|
|
ErrorNumber: 9718,
|
|
ErrorMessage: "This request doesn't contain an asset, which is a required argument. Change the request to provide an asset.",
|
|
}
|
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
|
panic(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if len(associations.SerialNumbers) == 0 {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
res := vpp.ErrorResponse{
|
|
ErrorNumber: 9719,
|
|
ErrorMessage: "Either clientUserIds or serialNumbers are required arguments. Change the request to provide assignable users and devices.",
|
|
}
|
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
|
panic(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
var badAssets []vpp.Asset
|
|
for _, reqAsset := range associations.Assets {
|
|
var found bool
|
|
for _, goodAsset := range config.Assets {
|
|
if reqAsset == goodAsset {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
badAssets = append(badAssets, reqAsset)
|
|
}
|
|
}
|
|
|
|
var badSerials []string
|
|
for _, reqSerial := range associations.SerialNumbers {
|
|
var found bool
|
|
for _, goodSerial := range config.SerialNumbers {
|
|
if reqSerial == goodSerial {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
badSerials = append(badSerials, reqSerial)
|
|
}
|
|
}
|
|
|
|
if len(badAssets) != 0 || len(badSerials) != 0 {
|
|
errMsg := "error associating assets."
|
|
if len(badAssets) > 0 {
|
|
var badAdamIds []string
|
|
for _, asset := range badAssets {
|
|
badAdamIds = append(badAdamIds, asset.AdamID)
|
|
}
|
|
errMsg += fmt.Sprintf(" assets don't exist on account: %s.", strings.Join(badAdamIds, ", "))
|
|
}
|
|
if len(badSerials) > 0 {
|
|
errMsg += fmt.Sprintf(" bad serials: %s.", strings.Join(badSerials, ", "))
|
|
}
|
|
res := vpp.ErrorResponse{
|
|
ErrorInfo: vpp.ResponseErrorInfo{
|
|
Assets: badAssets,
|
|
ClientUserIds: []string{"something"},
|
|
SerialNumbers: badSerials,
|
|
},
|
|
// Not sure what error should be returned on each
|
|
// error type
|
|
ErrorNumber: 1,
|
|
ErrorMessage: errMsg,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
if strings.Contains(r.URL.Path, "assets") {
|
|
// Then we're responding to GetAssets
|
|
w.Header().Set("Content-Type", "application/json")
|
|
encoder := json.NewEncoder(w)
|
|
err := encoder.Encode(map[string][]vpp.Asset{"assets": config.Assets})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
resp := []byte(`{"locationName": "Fleet Location One"}`)
|
|
if strings.Contains(r.URL.RawQuery, "invalidToken") {
|
|
// This replicates the response sent back from Apple's VPP endpoints when an invalid
|
|
// token is passed. For more details see:
|
|
// https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes
|
|
// https://developer.apple.com/documentation/devicemanagement/client_config
|
|
// https://developer.apple.com/documentation/devicemanagement/errorresponse
|
|
// Note that the Apple server returns 200 in this case.
|
|
resp = []byte(`{"errorNumber": 9622,"errorMessage": "Invalid authentication token"}`)
|
|
}
|
|
|
|
if strings.Contains(r.URL.RawQuery, "serverError") {
|
|
resp = []byte(`{"errorNumber": 9603,"errorMessage": "Internal server error"}`)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
|
|
_, _ = w.Write(resp)
|
|
}))
|
|
|
|
dev_mode.SetOverride("FLEET_DEV_VPP_URL", srv.URL, t)
|
|
t.Cleanup(srv.Close)
|
|
}
|
|
|
|
func StartAndServeVPPServer(t *testing.T) {
|
|
config := &AppleVPPConfigSrvConf{
|
|
Assets: []vpp.Asset{
|
|
{
|
|
AdamID: "1",
|
|
PricingParam: "STDQ",
|
|
AvailableCount: 12,
|
|
},
|
|
{
|
|
AdamID: "2",
|
|
PricingParam: "STDQ",
|
|
AvailableCount: 3,
|
|
},
|
|
},
|
|
SerialNumbers: []string{"123", "456"},
|
|
}
|
|
|
|
StartVPPApplyServer(t, config)
|
|
|
|
// Set up the VPP proxy metadata server using the new format
|
|
// This replaces the old iTunes API format
|
|
vppProxySrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "Bearer test-bearer-token" || r.Header.Get("vpp-token") == "" {
|
|
w.WriteHeader(401)
|
|
_, _ = w.Write([]byte(`{"error": "unauthorized"}`))
|
|
return
|
|
}
|
|
|
|
// deviceFamilies: "mac" -> osx platform, "iphone" -> ios platform, "ipad" -> ios platform
|
|
db := map[string]string{
|
|
// macos app
|
|
"1": `{
|
|
"id": "1",
|
|
"attributes": {
|
|
"name": "App 1",
|
|
"platformAttributes": {
|
|
"osx": {
|
|
"bundleId": "a-1",
|
|
"artwork": {"url": "https://example.com/images/1/{w}x{h}.{f}"},
|
|
"latestVersionInfo": {"versionDisplay": "1.0.0"}
|
|
}
|
|
},
|
|
"deviceFamilies": ["mac"]
|
|
}
|
|
}`,
|
|
// macos, ios, ipados app
|
|
"2": `{
|
|
"id": "2",
|
|
"attributes": {
|
|
"name": "App 2",
|
|
"platformAttributes": {
|
|
"osx": {
|
|
"bundleId": "b-2",
|
|
"artwork": {"url": "https://example.com/images/2-mac/{w}x{h}.{f}"},
|
|
"latestVersionInfo": {"versionDisplay": "1.2.3"}
|
|
},
|
|
"ios": {
|
|
"bundleId": "b-2",
|
|
"artwork": {"url": "https://example.com/images/2/{w}x{h}.{f}"},
|
|
"latestVersionInfo": {"versionDisplay": "2.0.0"}
|
|
}
|
|
},
|
|
"deviceFamilies": ["mac", "iphone", "ipad"]
|
|
}
|
|
}`,
|
|
// ipados app
|
|
"3": `{
|
|
"id": "3",
|
|
"attributes": {
|
|
"name": "App 3",
|
|
"platformAttributes": {
|
|
"ios": {
|
|
"bundleId": "c-3",
|
|
"artwork": {"url": "https://example.com/images/3/{w}x{h}.{f}"},
|
|
"latestVersionInfo": {"versionDisplay": "3.0.0"}
|
|
}
|
|
},
|
|
"deviceFamilies": ["ipad"]
|
|
}
|
|
}`,
|
|
}
|
|
|
|
adamIDString := r.URL.Query().Get("ids")
|
|
adamIDs := strings.Split(adamIDString, ",")
|
|
|
|
var objs []string
|
|
for _, a := range adamIDs {
|
|
if obj, ok := db[a]; ok {
|
|
objs = append(objs, obj)
|
|
}
|
|
}
|
|
|
|
_, _ = w.Write(fmt.Appendf(nil, `{"data": [%s]}`, strings.Join(objs, ",")))
|
|
}))
|
|
|
|
vppProxyAuthSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"fleetServerSecret": "test-bearer-token"}`))
|
|
}))
|
|
|
|
t.Cleanup(vppProxySrv.Close)
|
|
t.Cleanup(vppProxyAuthSrv.Close)
|
|
dev_mode.SetOverride("FLEET_DEV_STOKEN_AUTHENTICATED_APPS_URL", vppProxySrv.URL, t)
|
|
dev_mode.SetOverride("FLEET_DEV_VPP_PROXY_AUTH_URL", vppProxyAuthSrv.URL, t)
|
|
}
|
|
|
|
type MockPusher struct{}
|
|
|
|
func (MockPusher) Push(ctx context.Context, ids []string) (map[string]*push.Response, error) {
|
|
m := make(map[string]*push.Response, len(ids))
|
|
for _, id := range ids {
|
|
m[id] = &push.Response{Id: id}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
type MemKeyValueStore struct {
|
|
m sync.Map
|
|
}
|
|
|
|
func NewMemKeyValueStore() *MemKeyValueStore {
|
|
return &MemKeyValueStore{}
|
|
}
|
|
|
|
func (m *MemKeyValueStore) Set(ctx context.Context, key string, value string, expireTime time.Duration) error {
|
|
m.m.Store(key, value)
|
|
return nil
|
|
}
|
|
|
|
func (m *MemKeyValueStore) Get(ctx context.Context, key string) (*string, error) {
|
|
v, ok := m.m.Load(key)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
vAsString := v.(string)
|
|
return &vAsString, nil
|
|
}
|
|
|
|
func AddLabelMocks(ds *mock.Store) {
|
|
var deletedLabels []string
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) {
|
|
return []*fleet.LabelSpec{
|
|
{
|
|
Name: "a",
|
|
Description: "A global label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
|
Hosts: []string{"host2", "host3"},
|
|
},
|
|
{
|
|
Name: "b",
|
|
Description: "Another global label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
|
Query: "SELECT 1 from osquery_info",
|
|
},
|
|
}, nil
|
|
}
|
|
ds.ApplyLabelSpecsWithAuthorFunc = func(ctx context.Context, specs []*fleet.LabelSpec, authorID *uint) (err error) {
|
|
return nil
|
|
}
|
|
|
|
ds.DeleteLabelFunc = func(ctx context.Context, name string, filter fleet.TeamFilter) error {
|
|
deletedLabels = append(deletedLabels, name)
|
|
return nil
|
|
}
|
|
ds.LabelsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]*fleet.Label, error) {
|
|
validLabels := map[string]*fleet.Label{
|
|
"a": {
|
|
ID: 1,
|
|
Name: "a",
|
|
},
|
|
"b": {
|
|
ID: 2,
|
|
Name: "b",
|
|
},
|
|
}
|
|
|
|
found := make(map[string]*fleet.Label)
|
|
for _, l := range names {
|
|
if label, ok := validLabels[l]; ok {
|
|
found[l] = label
|
|
}
|
|
}
|
|
return found, nil
|
|
}
|
|
}
|
|
|
|
type notFoundError struct{}
|
|
|
|
var _ fleet.NotFoundError = (*notFoundError)(nil)
|
|
|
|
func (e *notFoundError) IsNotFound() bool {
|
|
return true
|
|
}
|
|
|
|
func (e *notFoundError) Error() string {
|
|
return ""
|
|
}
|