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 #43047 # Checklist for submitter - [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. - [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. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually See https://github.com/fleetdm/fleet/issues/42960#issuecomment-4244206563 and subsequent comments. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Apple DDM declarations support a vetted subset of Fleet variables with per-host substitution; premium license required. Declaration tokens and resend behavior now reflect variable changes; unresolved host substitutions mark that host’s declaration as failed. * **Bug Fixes** * Clearer errors for unsupported or license-restricted Fleet variables and more consistent DDM resend/update semantics when variables change. * **Tests** * Added extensive unit and integration tests covering Fleet variable validation, substitution, token changes, resends, and failure states. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
871 lines
30 KiB
Go
871 lines
30 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/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,
|
|
Type: "dynamic",
|
|
},
|
|
}, 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
|
|
}
|
|
ds.GetHostRecoveryLockPasswordStatusFunc = func(ctx context.Context, hostUUID string) (*fleet.HostMDMRecoveryLockPassword, error) {
|
|
return nil, nil
|
|
}
|
|
ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) {
|
|
return &fleet.TeamMDM{}, 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)
|
|
case strings.HasSuffix(r.URL.Path, ".pkg"):
|
|
pkgDir := getPathRelative("../testdata/gitops/lib/")
|
|
http.ServeFile(w, r, filepath.Join(pkgDir, filepath.Base(r.URL.Path)))
|
|
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.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) 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, automationFilter string,
|
|
) (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, usesFleetVars []fleet.FleetVarName) (
|
|
*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.SetAsideLabelsFunc = func(ctx context.Context, teamID *uint, names []string, user fleet.User) error {
|
|
return nil
|
|
}
|
|
|
|
ds.LabelByNameFunc = func(ctx context.Context, name string, filter fleet.TeamFilter) (*fleet.Label, error) {
|
|
return &fleet.Label{ID: 1, Name: name}, 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 ""
|
|
}
|