fleet/cmd/fleetctl/gitops_test.go
Lucas Manuel Rodriguez 8d664bd456
Make software batch endpoint asynchronous (#22258)
#22069

API changes: https://github.com/fleetdm/fleet/pull/22259

QAd by applying 10 pieces of software on a team, which took 3+ minutes
in total (which, before these changes was timing out at 100s.)

With this approach, a GitOps CI run timing out might leave the
background process running (which will eventually be applied to the
database). The team discussed and agreed that we can fix this edge case
later.

- [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/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)
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
2024-09-20 11:55:47 -03:00

2942 lines
104 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
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/mock"
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
teamName = "Team Test"
fleetServerURL = "https://fleet.example.com"
orgName = "GitOps Test"
)
func TestGitOpsFilenameValidation(t *testing.T) {
filename := strings.Repeat("a", filenameMaxLength+1)
_, err := runAppNoChecks([]string{"gitops", "-f", filename})
assert.ErrorContains(t, err, "file name must be less than")
}
func TestGitOpsBasicGlobalFree(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
_, ds := runServerWithMockedDS(t)
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, 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.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
// Mock appConfig
savedAppConfig := &fleet.AppConfig{}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
var enrolledSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
enrolledSecrets = secrets
return nil
}
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
const (
fleetServerURL = "https://fleet.example.com"
orgName = "GitOps Test"
)
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
_, err = tmpFile.WriteString(
`
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets:
`,
)
require.NoError(t, err)
// No file
var errWriter strings.Builder
_, err = runAppNoChecks([]string{"gitops", tmpFile.Name()})
require.Error(t, err)
assert.Equal(t, `Required flag "f" not set`, err.Error())
// Blank file
errWriter.Reset()
_, err = runAppNoChecks([]string{"gitops", "-f", ""})
require.Error(t, err)
assert.Contains(t, err.Error(), "file name cannot be empty")
// Bad file
errWriter.Reset()
_, err = runAppNoChecks([]string{"gitops", "-f", "fileDoesNotExist.yml"})
require.Error(t, err)
assert.Contains(t, err.Error(), "no such file or directory")
// Empty file
errWriter.Reset()
badFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = runAppNoChecks([]string{"gitops", "-f", badFile.Name()})
require.Error(t, err)
assert.Contains(t, err.Error(), "errors occurred")
// DoGitOps error
t.Setenv("ORG_NAME", "")
_, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
assert.Contains(t, err.Error(), "organization name must be present")
// Missing controls.
tmpFile2, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = tmpFile2.WriteString(
`
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: https://example.com
org_info:
contact_url: https://example.com/contact
org_name: Foobar
secrets:
`,
)
require.NoError(t, err)
_, err = runAppNoChecks([]string{"gitops", "-f", tmpFile2.Name()})
require.Error(t, err)
assert.Equal(t, `'controls' must be set on global config`, err.Error())
// Dry run
t.Setenv("ORG_NAME", orgName)
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
// Real run
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
assert.Empty(t, enrolledSecrets)
}
func TestGitOpsBasicGlobalPremium(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
License: license,
KeyValueStore: newMemKeyValueStore(),
},
)
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, 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.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
// Mock appConfig
savedAppConfig := &fleet.AppConfig{}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
var enrolledSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
enrolledSecrets = secrets
return nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
return map[string]uint{labels[0]: 1}, nil
}
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
return &fleet.MDMAppleDeclaration{}, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return &fleet.Job{}, nil
}
ds.BatchSetSoftwareInstallersFunc = 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
}
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
const (
fleetServerURL = "https://fleet.example.com"
orgName = "GitOps Premium Test"
)
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
_, err = tmpFile.WriteString(
`
controls:
ios_updates:
deadline: "2022-02-02"
minimum_version: "17.6"
ipados_updates:
deadline: "2023-03-03"
minimum_version: "18.0"
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets:
software:
`,
)
require.NoError(t, err)
// Dry run
t.Setenv("ORG_NAME", orgName)
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
// Real run
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
assert.Empty(t, enrolledSecrets)
}
func TestGitOpsBasicTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
License: license,
KeyValueStore: newMemKeyValueStore(),
},
)
const secret = "TestSecret"
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, 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.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return 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.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
team := &fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: teamName,
}
var savedTeam *fleet.Team
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == teamName && savedTeam != nil {
return savedTeam, nil
}
return nil, &notFoundError{}
}
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
if savedTeam != nil && *savedTeam.Filename == filename {
return savedTeam, nil
}
return nil, &notFoundError{}
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
if tid == team.ID {
return savedTeam, nil
}
return nil, nil
}
var enrolledTeamSecrets []*fleet.EnrollSecret
ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
newTeam.ID = team.ID
savedTeam = newTeam
enrolledTeamSecrets = newTeam.Secrets
return newTeam, nil
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) {
return true, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
savedTeam = team
return team, nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
require.Len(t, labels, 1)
switch labels[0] {
case fleet.BuiltinLabelMacOS14Plus:
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
case fleet.BuiltinLabelIOS:
return map[string]uint{fleet.BuiltinLabelIOS: 2}, nil
case fleet.BuiltinLabelIPadOS:
return map[string]uint{fleet.BuiltinLabelIPadOS: 3}, nil
default:
return nil, &notFoundError{}
}
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.BatchSetSoftwareInstallersFunc = 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.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
enrolledTeamSecrets = secrets
return nil
}
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
return &fleet.MDMAppleDeclaration{}, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return &fleet.Job{}, nil
}
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
t.Setenv("TEST_SECRET", "")
_, err = tmpFile.WriteString(
`
controls:
ios_updates:
deadline: "2024-10-10"
minimum_version: "18.0"
ipados_updates:
deadline: "2025-11-11"
minimum_version: "17.6"
queries:
policies:
agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: ${TEST_SECRET}
software:
`,
)
require.NoError(t, err)
// DoGitOps error
t.Setenv("TEST_TEAM_NAME", "")
_, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
assert.Contains(t, err.Error(), "'name' is required")
// Invalid name for "No team" file (dry and real).
t.Setenv("TEST_TEAM_NAME", "no TEam")
_, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
require.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name()))
t.Setenv("TEST_TEAM_NAME", "no TEam")
_, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name()))
t.Setenv("TEST_TEAM_NAME", "All teams")
_, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
require.Error(t, err)
assert.Contains(t, err.Error(), `"All teams" is a reserved team name`)
t.Setenv("TEST_TEAM_NAME", "All TEAMS")
_, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
assert.Contains(t, err.Error(), `"All teams" is a reserved team name`)
// Dry run
t.Setenv("TEST_TEAM_NAME", teamName)
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
assert.Nil(t, savedTeam)
// Real run
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
require.NotNil(t, savedTeam)
assert.Equal(t, teamName, savedTeam.Name)
assert.Empty(t, enrolledTeamSecrets)
// The previous run created the team, so let's rerun with an existing team
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
assert.Empty(t, enrolledTeamSecrets)
// Add a secret
t.Setenv("TEST_SECRET", fmt.Sprintf("[{\"secret\":\"%s\"}]", secret))
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
require.Len(t, enrolledTeamSecrets, 1)
assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
}
func TestGitOpsFullGlobal(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
// mdm test configuration must be set so that activating windows MDM works.
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, "../../server/service/testdata")
// License is not needed because we are not using any premium features in our config.
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
MDMStorage: new(mdmmock.MDMAppleStore),
MDMPusher: mockPusher{},
FleetConfig: &fleetCfg,
},
)
var appliedScripts []*fleet.Script
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
appliedScripts = scripts
return nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
var appliedMacProfiles []*fleet.MDMAppleConfigProfile
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
) (updates fleet.MDMProfilesUpdates, err error) {
appliedMacProfiles = macProfiles
appliedWinProfiles = winProfiles
return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
}
// Policies
policy := fleet.Policy{}
policy.ID = 1
policy.Name = "Policy to delete"
policyDeleted := false
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) {
return []*fleet.Policy{&policy}, nil
}
ds.PoliciesByIDFunc = func(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error) {
if slices.Contains(ids, 1) {
return map[uint]*fleet.Policy{1: &policy}, nil
}
return nil, nil
}
ds.DeleteGlobalPoliciesFunc = func(ctx context.Context, ids []uint) ([]uint, error) {
policyDeleted = true
assert.Equal(t, []uint{policy.ID}, ids)
return ids, nil
}
var appliedPolicySpecs []*fleet.PolicySpec
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
appliedPolicySpecs = specs
return nil
}
// Queries
query := fleet.Query{}
query.ID = 1
query.Name = "Query to delete"
queryDeleted := false
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) {
return []*fleet.Query{&query}, nil
}
ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) {
queryDeleted = true
assert.Equal(t, []uint{query.ID}, ids)
return 1, nil
}
ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
if id == query.ID {
return &query, nil
}
return nil, nil
}
var appliedQueries []*fleet.Query
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
return nil, &notFoundError{}
}
ds.ApplyQueriesFunc = func(
ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
) error {
appliedQueries = queries
return nil
}
// Mock appConfig
savedAppConfig := &fleet.AppConfig{}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) {
return true, nil
}
var enrolledSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
enrolledSecrets = secrets
return nil
}
const (
fleetServerURL = "https://fleet.example.com"
orgName = "GitOps Test"
)
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
t.Setenv("SOFTWARE_INSTALLER_URL", fleetServerURL)
file := "./testdata/gitops/global_config_no_paths.yml"
// Dry run
_ = runAppForTest(t, []string{"gitops", "-f", file, "--dry-run"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
assert.Len(t, enrolledSecrets, 0)
assert.Len(t, appliedPolicySpecs, 0)
assert.Len(t, appliedQueries, 0)
assert.Len(t, appliedScripts, 0)
assert.Len(t, appliedMacProfiles, 0)
assert.Len(t, appliedWinProfiles, 0)
// Real run
_ = runAppForTest(t, []string{"gitops", "-f", file})
assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
assert.Contains(t, string(*savedAppConfig.AgentOptions), "distributed_denylist_duration")
assert.Equal(t, 2000, savedAppConfig.ServerSettings.QueryReportCap)
assert.Len(t, enrolledSecrets, 2)
assert.True(t, policyDeleted)
assert.Len(t, appliedPolicySpecs, 5)
assert.True(t, queryDeleted)
assert.Len(t, appliedQueries, 3)
assert.Len(t, appliedScripts, 1)
assert.Len(t, appliedMacProfiles, 1)
assert.Len(t, appliedWinProfiles, 1)
require.Len(t, savedAppConfig.Integrations.GoogleCalendar, 1)
assert.Equal(t, "service@example.com", savedAppConfig.Integrations.GoogleCalendar[0].ApiKey["client_email"])
assert.True(t, savedAppConfig.ActivityExpirySettings.ActivityExpiryEnabled)
assert.Equal(t, 60, savedAppConfig.ActivityExpirySettings.ActivityExpiryWindow)
assert.True(t, savedAppConfig.ServerSettings.AIFeaturesDisabled)
assert.True(t, savedAppConfig.WebhookSettings.ActivitiesWebhook.Enable)
assert.Equal(t, "https://activities_webhook_url", savedAppConfig.WebhookSettings.ActivitiesWebhook.DestinationURL)
}
func TestGitOpsFullTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
// mdm test configuration must be set so that activating windows MDM works.
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, "../../server/service/testdata")
// License is not needed because we are not using any premium features in our config.
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
License: license,
MDMStorage: new(mdmmock.MDMAppleStore),
MDMPusher: mockPusher{},
FleetConfig: &fleetCfg,
NoCacheDatastore: true,
KeyValueStore: newMemKeyValueStore(),
},
)
appConfig := fleet.AppConfig{
// During dry run, the global calendar integration setting may not be set
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
},
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &appConfig, nil
}
var appliedScripts []*fleet.Script
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
appliedScripts = scripts
return nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
var appliedMacProfiles []*fleet.MDMAppleConfigProfile
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
) (updates fleet.MDMProfilesUpdates, err error) {
appliedMacProfiles = macProfiles
appliedWinProfiles = winProfiles
return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
}
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {
return &profile, nil
}
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
return declaration, nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus})
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
}
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
declaration.DeclarationUUID = uuid.NewString()
return declaration, nil
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
// Team
var savedTeam *fleet.Team
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == "Conflict" {
return &fleet.Team{}, nil
}
if savedTeam != nil && savedTeam.Name == name {
return savedTeam, nil
}
return nil, &notFoundError{}
}
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
if savedTeam != nil && *savedTeam.Filename == filename {
return savedTeam, nil
}
return nil, &notFoundError{}
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
if tid == savedTeam.ID {
return savedTeam, nil
}
return nil, nil
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) {
return true, nil
}
const teamID = uint(123)
var enrolledSecrets []*fleet.EnrollSecret
ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
newTeam.ID = teamID
savedTeam = newTeam
enrolledSecrets = newTeam.Secrets
return newTeam, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
if team.ID == teamID {
savedTeam = team
} else {
assert.Fail(t, "unexpected team ID when saving team")
}
return team, nil
}
// Policies
policy := fleet.Policy{}
policy.ID = 1
policy.Name = "Policy to delete"
policy.TeamID = ptr.Uint(teamID)
policyDeleted := false
ds.ListTeamPoliciesFunc = func(
ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
return []*fleet.Policy{&policy}, nil, nil
}
ds.PoliciesByIDFunc = func(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error) {
if slices.Contains(ids, 1) {
return map[uint]*fleet.Policy{1: &policy}, nil
}
return nil, nil
}
ds.DeleteTeamPoliciesFunc = func(ctx context.Context, teamID uint, IDs []uint) ([]uint, error) {
policyDeleted = true
assert.Equal(t, []uint{policy.ID}, IDs)
return []uint{policy.ID}, nil
}
var appliedPolicySpecs []*fleet.PolicySpec
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
appliedPolicySpecs = specs
return nil
}
// Queries
query := fleet.Query{}
query.ID = 1
query.TeamID = ptr.Uint(teamID)
query.Name = "Query to delete"
queryDeleted := false
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) {
return []*fleet.Query{&query}, nil
}
ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) {
queryDeleted = true
assert.Equal(t, []uint{query.ID}, ids)
return 1, nil
}
ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
if id == query.ID {
return &query, nil
}
return nil, nil
}
var appliedQueries []*fleet.Query
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
return nil, &notFoundError{}
}
ds.ApplyQueriesFunc = func(
ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
) error {
appliedQueries = queries
return nil
}
var appliedSoftwareInstallers []*fleet.UploadSoftwareInstallerPayload
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
appliedSoftwareInstallers = installers
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
enrolledSecrets = secrets
return nil
}
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
startSoftwareInstallerServer(t)
t.Setenv("TEST_TEAM_NAME", teamName)
// Dry run
const baseFilename = "team_config_no_paths.yml"
gitopsFile := "./testdata/gitops/" + baseFilename
_ = runAppForTest(t, []string{"gitops", "-f", gitopsFile, "--dry-run"})
assert.Nil(t, savedTeam)
assert.Len(t, enrolledSecrets, 0)
assert.Len(t, appliedPolicySpecs, 0)
assert.Len(t, appliedQueries, 0)
assert.Len(t, appliedScripts, 0)
assert.Len(t, appliedMacProfiles, 0)
assert.Len(t, appliedWinProfiles, 0)
assert.Empty(t, appliedSoftwareInstallers)
// Real run
// Setting global calendar config
appConfig.Integrations = fleet.Integrations{
GoogleCalendar: []*fleet.GoogleCalendarIntegration{{}},
}
_ = runAppForTest(t, []string{"gitops", "-f", gitopsFile})
require.NotNil(t, savedTeam)
assert.Equal(t, teamName, savedTeam.Name)
assert.Contains(t, string(*savedTeam.Config.AgentOptions), "distributed_denylist_duration")
assert.True(t, savedTeam.Config.Features.EnableHostUsers)
assert.Equal(t, 30, savedTeam.Config.HostExpirySettings.HostExpiryWindow)
assert.True(t, savedTeam.Config.MDM.EnableDiskEncryption)
assert.Len(t, enrolledSecrets, 2)
assert.True(t, policyDeleted)
assert.Len(t, appliedPolicySpecs, 5)
assert.True(t, queryDeleted)
assert.Len(t, appliedQueries, 3)
assert.Len(t, appliedScripts, 1)
assert.Len(t, appliedMacProfiles, 1)
assert.Len(t, appliedWinProfiles, 1)
assert.True(t, savedTeam.Config.WebhookSettings.HostStatusWebhook.Enable)
assert.Equal(t, "https://example.com/host_status_webhook", savedTeam.Config.WebhookSettings.HostStatusWebhook.DestinationURL)
require.NotNil(t, savedTeam.Config.Integrations.GoogleCalendar)
assert.True(t, savedTeam.Config.Integrations.GoogleCalendar.Enable)
assert.Equal(t, baseFilename, *savedTeam.Filename)
require.Len(t, appliedSoftwareInstallers, 2)
packageID := `"ruby"`
uninstallScriptProcessed := strings.ReplaceAll(file.GetUninstallScript("deb"), "$PACKAGE_ID", packageID)
assert.ElementsMatch(t, []string{fmt.Sprintf("echo 'uninstall' %s\n", packageID), uninstallScriptProcessed},
[]string{appliedSoftwareInstallers[0].UninstallScript, appliedSoftwareInstallers[1].UninstallScript})
// Change team name
newTeamName := "New Team Name"
t.Setenv("TEST_TEAM_NAME", newTeamName)
_ = runAppForTest(t, []string{"gitops", "-f", gitopsFile, "--dry-run"})
_ = runAppForTest(t, []string{"gitops", "-f", gitopsFile})
require.NotNil(t, savedTeam)
assert.Equal(t, newTeamName, savedTeam.Name)
assert.Equal(t, baseFilename, *savedTeam.Filename)
// Try to change team name again, but this time the new name conflicts with an existing team
t.Setenv("TEST_TEAM_NAME", "Conflict")
_, err = runAppNoChecks([]string{"gitops", "-f", gitopsFile, "--dry-run"})
assert.ErrorContains(t, err, "team name already exists")
_, err = runAppNoChecks([]string{"gitops", "-f", gitopsFile})
assert.ErrorContains(t, err, "team name already exists")
// Now clear the settings
t.Setenv("TEST_TEAM_NAME", newTeamName)
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
secret := "TestSecret"
t.Setenv("TEST_SECRET", secret)
_, err = tmpFile.WriteString(
`
controls:
queries:
policies:
agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"}]
software:
`,
)
require.NoError(t, err)
// Dry run
savedTeam = nil
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
assert.Nil(t, savedTeam)
// Real run
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()})
require.NotNil(t, savedTeam)
assert.Equal(t, newTeamName, savedTeam.Name)
require.Len(t, enrolledSecrets, 1)
assert.Equal(t, secret, enrolledSecrets[0].Secret)
assert.False(t, savedTeam.Config.WebhookSettings.HostStatusWebhook.Enable)
assert.Equal(t, "", savedTeam.Config.WebhookSettings.HostStatusWebhook.DestinationURL)
assert.NotNil(t, savedTeam.Config.Integrations.GoogleCalendar)
assert.False(t, savedTeam.Config.Integrations.GoogleCalendar.Enable)
assert.Empty(t, savedTeam.Config.Integrations.GoogleCalendar)
assert.Empty(t, savedTeam.Config.MDM.MacOSSettings.CustomSettings)
assert.Empty(t, savedTeam.Config.MDM.WindowsSettings.CustomSettings.Value)
assert.Empty(t, savedTeam.Config.MDM.MacOSUpdates.Deadline.Value)
assert.Empty(t, savedTeam.Config.MDM.MacOSUpdates.MinimumVersion.Value)
assert.Empty(t, savedTeam.Config.MDM.MacOSSetup.BootstrapPackage.Value)
assert.False(t, savedTeam.Config.MDM.EnableDiskEncryption)
assert.Equal(t, filepath.Base(tmpFile.Name()), *savedTeam.Filename)
}
func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
License: license,
KeyValueStore: newMemKeyValueStore(),
},
)
// Mock appConfig
savedAppConfig := &fleet.AppConfig{}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
const (
fleetServerURL = "https://fleet.example.com"
orgName = "GitOps Test"
secret = "TestSecret"
)
var enrolledSecrets []*fleet.EnrollSecret
var enrolledTeamSecrets []*fleet.EnrollSecret
var savedTeam *fleet.Team
team := &fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: teamName,
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) {
return true, nil
}
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
if teamID == nil {
enrolledSecrets = secrets
} else {
enrolledTeamSecrets = secrets
}
return nil
}
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration,
) (updates fleet.MDMProfilesUpdates, err error) {
assert.Empty(t, macProfiles)
assert.Empty(t, winProfiles)
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
assert.Empty(t, scripts)
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
assert.Empty(t, profileUUIDs)
return fleet.MDMProfilesUpdates{}, nil
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
require.ElementsMatch(t, labels, []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) {
return nil, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
job.ID = 1
return job, nil
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
if tid == team.ID {
return savedTeam, nil
}
return nil, nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == teamName && savedTeam != nil {
return savedTeam, nil
}
return nil, &notFoundError{}
}
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
if savedTeam != nil && *savedTeam.Filename == filename {
return savedTeam, nil
}
return nil, &notFoundError{}
}
ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
newTeam.ID = team.ID
savedTeam = newTeam
enrolledTeamSecrets = newTeam.Secrets
return newTeam, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
savedTeam = team
return team, nil
}
ds.BatchSetSoftwareInstallersFunc = 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.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
_, err = globalFile.WriteString(
`
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
`,
)
require.NoError(t, err)
teamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
t.Setenv("TEST_TEAM_NAME", teamName)
t.Setenv("TEST_SECRET", secret)
_, err = teamFile.WriteString(
`
controls:
queries:
policies:
agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"}]
software:
`,
)
require.NoError(t, err)
teamFileDupSecret, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = teamFileDupSecret.WriteString(
`
controls:
queries:
policies:
agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"},{"secret":"globalSecret"}]
software:
`,
)
require.NoError(t, err)
// Files out of order
_, err = runAppNoChecks([]string{"gitops", "-f", teamFile.Name(), "-f", globalFile.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "must be the global config"))
// Global file specified multiple times
_, err = runAppNoChecks([]string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "-f", globalFile.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "only the first file can be the global config"))
// Duplicate secret
_, err = runAppNoChecks([]string{"gitops", "-f", globalFile.Name(), "-f", teamFileDupSecret.Name(), "--dry-run"})
require.Error(t, err)
assert.ErrorContains(t, err, "duplicate enroll secret found")
// Dry run
_ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
// Dry run, deleting other teams
assert.False(t, ds.ListTeamsFuncInvoked)
_ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run", "--delete-other-teams"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
assert.True(t, ds.ListTeamsFuncInvoked)
// Real run
_ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name()})
assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
assert.Len(t, enrolledSecrets, 1)
require.NotNil(t, savedTeam)
assert.Equal(t, teamName, savedTeam.Name)
require.Len(t, enrolledTeamSecrets, 1)
assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
// Now, set up a team to delete
teamToDeleteID := uint(999)
teamToDelete := &fleet.Team{
ID: teamToDeleteID,
CreatedAt: time.Now(),
Name: "Team to delete",
}
ds.ListTeamsFuncInvoked = false
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
return []*fleet.Team{teamToDelete, team}, nil
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
switch tid {
case team.ID:
return team, nil
case teamToDeleteID:
return teamToDelete, nil
}
assert.Fail(t, fmt.Sprintf("unexpected team ID %d", tid))
return teamToDelete, nil
}
ds.DeleteTeamFunc = func(ctx context.Context, tid uint) error {
assert.Equal(t, teamToDeleteID, tid)
return nil
}
ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
return nil, nil
}
// Real run, deleting other teams
_ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--delete-other-teams"})
assert.True(t, ds.ListTeamsFuncInvoked)
assert.True(t, ds.DeleteTeamFuncInvoked)
}
func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
// Cannot run t.Parallel() because runServerWithMockedDS sets the FLEET_SERVER_ADDRESS
// environment variable.
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
License: license,
KeyValueStore: newMemKeyValueStore(),
},
)
// Mock appConfig
savedAppConfig := &fleet.AppConfig{}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
const (
fleetServerURL = "https://fleet.example.com"
orgName = "GitOps Test"
secret = "TestSecret"
)
var enrolledSecrets []*fleet.EnrollSecret
var enrolledTeamSecrets []*fleet.EnrollSecret
var savedTeam *fleet.Team
team := &fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: teamName,
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) {
return true, nil
}
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
if teamID == nil {
enrolledSecrets = secrets
} else {
enrolledTeamSecrets = secrets
}
return nil
}
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration,
) (updates fleet.MDMProfilesUpdates, err error) {
assert.Empty(t, macProfiles)
assert.Empty(t, winProfiles)
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
assert.Empty(t, scripts)
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
assert.Empty(t, profileUUIDs)
return fleet.MDMProfilesUpdates{}, nil
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
require.ElementsMatch(t, labels, []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) {
return nil, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
job.ID = 1
return job, nil
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
if tid == team.ID {
return savedTeam, nil
}
return nil, nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == teamName && savedTeam != nil {
return savedTeam, nil
}
return nil, &notFoundError{}
}
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
if savedTeam != nil && *savedTeam.Filename == filename {
return savedTeam, nil
}
return nil, &notFoundError{}
}
ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
newTeam.ID = team.ID
savedTeam = newTeam
enrolledTeamSecrets = newTeam.Secrets
return newTeam, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
savedTeam = team
return team, nil
}
ds.BatchSetSoftwareInstallersFunc = 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.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
globalFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = globalFileBasic.WriteString(fmt.Sprintf(
`
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: %s
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: %s
secrets: [{"secret":"globalSecret"}]
software:
`, fleetServerURL, orgName),
)
require.NoError(t, err)
globalFileWithSoftware, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = globalFileWithSoftware.WriteString(fmt.Sprintf(
`
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: %s
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: %s
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: https://example.com
`, fleetServerURL, orgName),
)
require.NoError(t, err)
globalFileWithControls, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = globalFileWithControls.WriteString(fmt.Sprintf(
`
controls:
ios_updates:
deadline: "2022-02-02"
minimum_version: "17.6"
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: %s
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: %s
secrets: [{"secret":"globalSecret"}]
software:
`, fleetServerURL, orgName),
)
require.NoError(t, err)
globalFileWithoutControlsAndSoftwareKeys, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = globalFileWithoutControlsAndSoftwareKeys.WriteString(fmt.Sprintf(
`
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: %s
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: %s
secrets: [{"secret":"globalSecret"}]
`, fleetServerURL, orgName),
)
require.NoError(t, err)
teamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = teamFile.WriteString(fmt.Sprintf(`
controls:
queries:
policies:
agent_options:
name: %s
team_settings:
secrets: [{"secret":"%s"}]
software:
`, teamName, secret),
)
require.NoError(t, err)
noTeamFilePath := filepath.Join(t.TempDir(), "no-team.yml")
noTeamFile, err := os.Create(noTeamFilePath)
require.NoError(t, err)
_, err = noTeamFile.WriteString(`
controls:
policies:
name: No team
software:
`)
require.NoError(t, err)
noTeamFilePathPoliciesCalendarPath := filepath.Join(t.TempDir(), "no-team.yml")
noTeamFilePathPoliciesCalendar, err := os.Create(noTeamFilePathPoliciesCalendarPath)
require.NoError(t, err)
_, err = noTeamFilePathPoliciesCalendar.WriteString(`
controls:
policies:
- name: Foobar
query: SELECT 1 FROM osquery_info WHERE start_time < 0;
calendar_events_enabled: true
name: No team
software:
`)
require.NoError(t, err)
noTeamFilePathWithControls := filepath.Join(t.TempDir(), "no-team.yml")
noTeamFileWithControls, err := os.Create(noTeamFilePathWithControls)
require.NoError(t, err)
_, err = noTeamFileWithControls.WriteString(`
controls:
ipados_updates:
deadline: "2023-03-03"
minimum_version: "18.0"
policies:
name: No team
software:
`)
require.NoError(t, err)
noTeamFilePathWithoutControls := filepath.Join(t.TempDir(), "no-team.yml")
noTeamFileWithoutControls, err := os.Create(noTeamFilePathWithoutControls)
require.NoError(t, err)
_, err = noTeamFileWithoutControls.WriteString(`
policies:
name: No team
software:
`)
require.NoError(t, err)
// Dry run, global defines software, should fail.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'software' cannot be set on global file"))
// Real run, global defines software, should fail.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'software' cannot be set on global file"))
// Dry run, both global and no-team.yml define controls.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithControls.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml"))
// Real run, both global and no-team.yml define controls.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithControls.Name()})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml"))
// Dry run, both global and no-team.yml defines policy with calendar events enabled.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFilePathPoliciesCalendar.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
// Real run, both global and no-team.yml define controls.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFilePathPoliciesCalendar.Name()})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
// Dry run, controls should be defined somewhere, either in no-team.yml or global.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml"))
// Real run, both global and no-team.yml define controls.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name()})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml"))
// Dry run, global file without controls and software keys.
_ = runAppForTest(t, []string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
// Real run, global file without controls and software keys.
_ = runAppForTest(t, []string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()})
assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
assert.Len(t, enrolledSecrets, 1)
require.NotNil(t, savedTeam)
assert.Equal(t, teamName, savedTeam.Name)
require.Len(t, enrolledTeamSecrets, 1)
assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
// Restore to test below.
savedAppConfig = &fleet.AppConfig{}
// Dry run
_ = runAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
// Real run
_ = runAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()})
assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
assert.Len(t, enrolledSecrets, 1)
require.NotNil(t, savedTeam)
assert.Equal(t, teamName, savedTeam.Name)
require.Len(t, enrolledTeamSecrets, 1)
assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
}
func TestGitOpsFullGlobalAndTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
// mdm test configuration must be set so that activating windows MDM works.
ds, savedAppConfigPtr, savedTeams := setupFullGitOpsPremiumServer(t)
startSoftwareInstallerServer(t)
var enrolledSecrets []*fleet.EnrollSecret
var enrolledTeamSecrets []*fleet.EnrollSecret
var appliedPolicySpecs []*fleet.PolicySpec
var appliedQueries []*fleet.Query
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
if teamID == nil {
enrolledSecrets = secrets
} else {
enrolledTeamSecrets = secrets
}
return nil
}
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
appliedPolicySpecs = specs
return nil
}
ds.ApplyQueriesFunc = func(
ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
) error {
appliedQueries = queries
return nil
}
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
team.ID = 1
enrolledTeamSecrets = team.Secrets
savedTeams[team.Name] = &team
return team, nil
}
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes()
require.NoError(t, err)
crt, key, err := apple_mdm.NewSCEPCACertKey()
require.NoError(t, err)
scepCert := tokenpki.PEMCertificate(crt.Raw)
scepKey := tokenpki.PEMRSAPrivateKey(key)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Value: scepCert},
fleet.MDMAssetCAKey: {Value: scepKey},
fleet.MDMAssetAPNSKey: {Value: apnsKey},
fleet.MDMAssetAPNSCert: {Value: apnsCert},
}, nil
}
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
globalFile := "./testdata/gitops/global_config_no_paths.yml"
teamFile := "./testdata/gitops/team_config_no_paths.yml"
// Dry run
_ = runAppForTest(t, []string{"gitops", "-f", globalFile, "-f", teamFile, "--dry-run", "--delete-other-teams"})
assert.False(t, ds.SaveAppConfigFuncInvoked)
assert.Len(t, enrolledSecrets, 0)
assert.Len(t, enrolledTeamSecrets, 0)
assert.Len(t, appliedPolicySpecs, 0)
assert.Len(t, appliedQueries, 0)
// Real run
_ = runAppForTest(t, []string{"gitops", "-f", globalFile, "-f", teamFile, "--delete-other-teams"})
assert.Equal(t, orgName, (*savedAppConfigPtr).OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, (*savedAppConfigPtr).ServerSettings.ServerURL)
assert.Len(t, enrolledSecrets, 2)
require.NotNil(t, *savedTeams[teamName])
assert.Equal(t, teamName, (*savedTeams[teamName]).Name)
require.Len(t, enrolledTeamSecrets, 2)
}
func TestGitOpsTeamSofwareInstallers(t *testing.T) {
startSoftwareInstallerServer(t)
cases := []struct {
file string
wantErr string
}{
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
{"testdata/gitops/team_software_installer_valid.yml", ""},
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries_apply.yml", "should have only one query."},
{"testdata/gitops/team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_uninstall_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_post_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_no_url.yml", "software URL is required"},
{"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"},
}
for _, c := range cases {
t.Run(filepath.Base(c.file), func(t *testing.T) {
setupFullGitOpsPremiumServer(t)
_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
if c.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, c.wantErr)
}
})
}
}
func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {
startSoftwareInstallerServer(t)
ds, _, _ := setupFullGitOpsPremiumServer(t)
t.Setenv("QUERY_VAR", "IT_WORKS")
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
if installers[0].PreInstallQuery != "select IT_WORKS" {
return fmt.Errorf("Missing env var, got %s", installers[0].PreInstallQuery)
}
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
_, err := runAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_software_installer_valid_env_query.yml"})
require.NoError(t, err)
}
func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
startSoftwareInstallerServer(t)
cases := []struct {
noTeamFile string
wantErr string
}{
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
{"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
{"testdata/gitops/no_team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
{"testdata/gitops/no_team_software_installer_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/no_team_software_installer_uninstall_not_found.yml", "no such file or directory"},
{"testdata/gitops/no_team_software_installer_post_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/no_team_software_installer_no_url.yml", "software URL is required"},
{"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"},
}
for _, c := range cases {
t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) {
setupFullGitOpsPremiumServer(t)
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
globalFile := "./testdata/gitops/global_config_no_paths.yml"
dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml")
t.Cleanup(func() {
os.Remove(dstPath)
})
err := file.Copy(c.noTeamFile, dstPath, 0o755)
require.NoError(t, err)
_, err = runAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath})
if c.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, c.wantErr)
}
})
}
}
func TestGitOpsTeamVPPApps(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)
appleITunesSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// a map of apps we can respond with
db := map[string]string{
// macos app
"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
// macos, ios, ipados app
"2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2,
"supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
// ipados app
"3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3,
"supportedDevices": ["iPadAir-iPadAir"] }`,
}
adamIDString := r.URL.Query().Get("id")
adamIDs := strings.Split(adamIDString, ",")
var objs []string
for _, a := range adamIDs {
objs = append(objs, db[a])
}
_, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ","))))
}))
t.Setenv("FLEET_DEV_ITUNES_URL", appleITunesSrv.URL)
cases := []struct {
file string
wantErr string
tokenExpiration time.Time
}{
{"testdata/gitops/team_vpp_valid_app.yml", "", time.Now().Add(24 * time.Hour)},
{"testdata/gitops/team_vpp_valid_app_self_service.yml", "", time.Now().Add(24 * time.Hour)},
{"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(24 * time.Hour)},
{"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(-24 * time.Hour)},
{"testdata/gitops/team_vpp_valid_app.yml", "VPP token expired", time.Now().Add(-24 * time.Hour)},
{"testdata/gitops/team_vpp_invalid_app.yml", "app not available on vpp account", time.Now().Add(24 * time.Hour)},
{"testdata/gitops/team_vpp_incorrect_type.yml", "\"app_store_apps.app_store_id\" must be a string, found number", time.Now().Add(24 * time.Hour)},
{"testdata/gitops/team_vpp_empty_adamid.yml", "software app store id required", time.Now().Add(24 * time.Hour)},
}
for _, c := range cases {
t.Run(filepath.Base(c.file), func(t *testing.T) {
ds, _, _ := setupFullGitOpsPremiumServer(t)
token, err := test.CreateVPPTokenEncoded(c.tokenExpiration, "fleet", "ca")
require.NoError(t, err)
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
return &fleet.VPPTokenDB{
ID: 1,
OrgName: "Fleet",
Location: "Earth",
RenewDate: c.tokenExpiration,
Token: string(token),
Teams: nil,
}, nil
}
_, err = runAppNoChecks([]string{"gitops", "-f", c.file})
if c.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, c.wantErr)
}
})
}
}
func TestGitOpsCustomSettings(t *testing.T) {
cases := []struct {
file string
wantErr string
}{
{"testdata/gitops/global_macos_windows_custom_settings_valid.yml", ""},
{"testdata/gitops/global_macos_custom_settings_valid_deprecated.yml", ""},
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included`},
{"testdata/gitops/global_windows_custom_settings_unknown_label.yml", `some or all the labels provided don't exist`},
{"testdata/gitops/team_macos_windows_custom_settings_valid.yml", ""},
{"testdata/gitops/team_macos_custom_settings_valid_deprecated.yml", ""},
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml", `For each profile, only one of "labels_exclude_any", "labels_include_all" or "labels" can be included.`},
{"testdata/gitops/team_macos_windows_custom_settings_unknown_label.yml", `some or all the labels provided don't exist`},
}
for _, c := range cases {
t.Run(filepath.Base(c.file), func(t *testing.T) {
ds, appCfgPtr, _ := setupFullGitOpsPremiumServer(t)
(*appCfgPtr).MDM.EnabledAndConfigured = true
(*appCfgPtr).MDM.WindowsEnabledAndConfigured = true
labelToIDs := map[string]uint{
fleet.BuiltinLabelMacOS14Plus: 1,
"A": 2,
"B": 3,
"C": 4,
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
// for this test, recognize labels A, B and C (as well as the built-in macos 14+ one)
ret := make(map[string]uint)
for _, lbl := range labels {
id, ok := labelToIDs[lbl]
if ok {
ret[lbl] = id
}
}
return ret, nil
}
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
if c.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, c.wantErr)
}
})
}
}
func startSoftwareInstallerServer(t *testing.T) {
// start the web server that will serve the installer
b, err := os.ReadFile(filepath.Join("..", "..", "server", "service", "testdata", "software-installers", "ruby.deb"))
require.NoError(t, err)
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 < 500*1024*1024 {
n, _ := w.Write(b)
sz += n
}
default:
w.Header().Set("Content-Type", "application/vnd.debian.binary-package")
_, _ = w.Write(b)
}
},
),
)
t.Cleanup(srv.Close)
t.Setenv("SOFTWARE_INSTALLER_URL", srv.URL)
}
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)
}))
t.Setenv("FLEET_DEV_VPP_URL", srv.URL)
t.Cleanup(srv.Close)
}
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, "../../server/service/testdata")
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
MDMStorage: new(mdmmock.MDMAppleStore),
MDMPusher: mockPusher{},
FleetConfig: &fleetCfg,
License: license,
NoCacheDatastore: true,
KeyValueStore: newMemKeyValueStore(),
},
)
// Mock appConfig
savedAppConfig := &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
},
}
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) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return 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,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return 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.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) {
return true, nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
require.ElementsMatch(t, labels, []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.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, p fleet.MDMAppleConfigProfile) (*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)
savedTeams[team.Name] = &team
return team, nil
}
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
return nil, &notFoundError{}
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
for _, tm := range savedTeams {
if (*tm).ID == tid {
return *tm, nil
}
}
return nil, &notFoundError{}
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
for _, tm := range savedTeams {
if (*tm).Name == name {
return *tm, nil
}
}
return nil, &notFoundError{}
}
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
for _, tm := range savedTeams {
if *(*tm).Filename == filename {
return *tm, nil
}
}
return nil, &notFoundError{}
}
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.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
}
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
}
func TestGitOpsABM(t *testing.T) {
global := func(mdm string) string {
return fmt.Sprintf(`
controls:
queries:
policies:
agent_options:
software:
org_settings:
server_settings:
server_url: "https://foo.example.com"
org_info:
org_name: GitOps Test
secrets:
- secret: "global"
mdm:
%s
`, mdm)
}
team := func(name string) string {
return fmt.Sprintf(`
name: %s
team_settings:
secrets:
- secret: "%s-secret"
agent_options:
controls:
policies:
queries:
software:
`, name, name)
}
workstations := team("💻 Workstations")
iosTeam := team("📱🏢 Company-owned iPhones")
ipadTeam := team("🔳🏢 Company-owned iPads")
cases := []struct {
name string
cfgs []string
dryRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
realRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
tokens []*fleet.ABMToken
}{
{
name: "backwards compat",
cfgs: []string{
global("apple_bm_default_team: 💻 Workstations"),
workstations,
},
tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
assert.Equal(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam, "💻 Workstations")
assert.Contains(t, out, "[!] gitops succeeded")
},
},
// {
// name: "deprecated config with two tokens in the db fails",
// cfgs: []string{
// global("apple_bm_default_team: 💻 Workstations"),
// workstations,
// },
// tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Second Token LLC"}},
// dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
// require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
// assert.Empty(t, appCfg.MDM.AppleBussinessManager.Value)
// assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
// assert.NotContains(t, out, "[!] gitops dry run succeeded")
// },
// realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
// require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
// assert.Empty(t, appCfg.MDM.AppleBussinessManager.Value)
// assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
// assert.NotContains(t, out, "[!] gitops dry run succeeded")
// },
// },
{
name: "new key all valid",
cfgs: []string{
global(`
apple_business_manager:
- organization_name: Fleet Device Management Inc.
macos_team: "💻 Workstations"
ios_team: "📱🏢 Company-owned iPhones"
ipados_team: "🔳🏢 Company-owned iPads"`),
workstations,
iosTeam,
ipadTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.ElementsMatch(
t,
appCfg.MDM.AppleBusinessManager.Value,
[]fleet.MDMAppleABMAssignmentInfo{
{
OrganizationName: "Fleet Device Management Inc.",
MacOSTeam: "💻 Workstations",
IOSTeam: "📱🏢 Company-owned iPhones",
IpadOSTeam: "🔳🏢 Company-owned iPads",
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "new key multiple elements",
cfgs: []string{
global(`
apple_business_manager:
- organization_name: Foo Inc.
macos_team: "💻 Workstations"
ios_team: "📱🏢 Company-owned iPhones"
ipados_team: "🔳🏢 Company-owned iPads"
- organization_name: Fleet Device Management Inc.
macos_team: "💻 Workstations"
ios_team: "📱🏢 Company-owned iPhones"
ipados_team: "🔳🏢 Company-owned iPads"`),
workstations,
iosTeam,
ipadTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.ElementsMatch(
t,
appCfg.MDM.AppleBusinessManager.Value,
[]fleet.MDMAppleABMAssignmentInfo{
{
OrganizationName: "Fleet Device Management Inc.",
MacOSTeam: "💻 Workstations",
IOSTeam: "📱🏢 Company-owned iPhones",
IpadOSTeam: "🔳🏢 Company-owned iPads",
},
{
OrganizationName: "Foo Inc.",
MacOSTeam: "💻 Workstations",
IOSTeam: "📱🏢 Company-owned iPhones",
IpadOSTeam: "🔳🏢 Company-owned iPads",
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "both keys errors",
cfgs: []string{
global(`
apple_bm_default_team: "💻 Workstations"
apple_business_manager:
- organization_name: Fleet Device Management Inc.
macos_team: "💻 Workstations"
ios_team: "📱🏢 Company-owned iPhones"
ipados_team: "🔳🏢 Company-owned iPads"`),
workstations,
iosTeam,
ipadTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
assert.NotContains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
assert.NotContains(t, out, "[!] gitops succeeded")
},
},
{
name: "using an undefined team errors",
cfgs: []string{
global(`
apple_business_manager:
- organization_name: Fleet Device Management Inc.
macos_team: "💻 Workstations"
ios_team: "📱🏢 Company-owned iPhones"
ipados_team: "🔳🏢 Company-owned iPads"`),
workstations,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.ErrorContains(t, err, "apple_business_manager team \"📱🏢 Company-owned iPhones\" not found in team configs")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.ErrorContains(t, err, "apple_business_manager team \"📱🏢 Company-owned iPhones\" not found in team configs")
},
},
{
name: "no team is supported",
cfgs: []string{
global(`
apple_business_manager:
- organization_name: Fleet Device Management Inc.
macos_team: "No team"
ios_team: "No team"
ipados_team: "No team"`),
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.ElementsMatch(
t,
appCfg.MDM.AppleBusinessManager.Value,
[]fleet.MDMAppleABMAssignmentInfo{
{
OrganizationName: "Fleet Device Management Inc.",
MacOSTeam: "No team",
IOSTeam: "No team",
IpadOSTeam: "No team",
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "not provided teams defaults to no team",
cfgs: []string{
global(`
apple_business_manager:
- organization_name: Fleet Device Management Inc.
macos_team: "No team"
ios_team: ""`),
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.ElementsMatch(
t,
appCfg.MDM.AppleBusinessManager.Value,
[]fleet.MDMAppleABMAssignmentInfo{
{
OrganizationName: "Fleet Device Management Inc.",
MacOSTeam: "No team",
IOSTeam: "",
IpadOSTeam: "",
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "non existent org name fails",
cfgs: []string{
global(`
apple_business_manager:
- organization_name: Does not exist
macos_team: "No team"`),
},
tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.ErrorContains(t, err, "token with organization name Does not exist doesn't exist")
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.NotContains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.ErrorContains(t, err, "token with organization name Does not exist doesn't exist")
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
assert.NotContains(t, out, "[!] gitops dry run succeeded")
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ds, savedAppConfigPtr, savedTeams := setupFullGitOpsPremiumServer(t)
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
if len(tt.tokens) > 0 {
return tt.tokens, nil
}
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
}
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
var res []*fleet.TeamSummary
for _, tm := range savedTeams {
res = append(res, &fleet.TeamSummary{Name: (*tm).Name, ID: (*tm).ID})
}
return res, nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
args := []string{"gitops"}
for _, cfg := range tt.cfgs {
if cfg != "" {
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = tmpFile.WriteString(cfg)
require.NoError(t, err)
args = append(args, "-f", tmpFile.Name())
}
}
// Dry run
out, err := runAppNoChecks(append(args, "--dry-run"))
tt.dryRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
if t.Failed() {
t.FailNow()
}
// Real run
out, err = runAppNoChecks(args)
tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
// Second real run, now that all the teams are saved
out, err = runAppNoChecks(args)
tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
})
}
}
func TestGitOpsVPP(t *testing.T) {
global := func(mdm string) string {
return fmt.Sprintf(`
controls:
queries:
policies:
agent_options:
software:
org_settings:
server_settings:
server_url: "https://foo.example.com"
org_info:
org_name: GitOps Test
secrets:
- secret: "global"
mdm:
%s
`, mdm)
}
team := func(name string) string {
return fmt.Sprintf(`
name: %s
team_settings:
secrets:
- secret: "%s-secret"
agent_options:
controls:
policies:
queries:
software:
`, name, name)
}
workstations := team("💻 Workstations")
iosTeam := team("📱🏢 Company-owned iPhones")
ipadTeam := team("🔳🏢 Company-owned iPads")
cases := []struct {
name string
cfgs []string
dryRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
realRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
}{
{
name: "new key all valid",
cfgs: []string{
global(`
volume_purchasing_program:
- location: Fleet Device Management Inc.
teams:
- "💻 Workstations"
- "📱🏢 Company-owned iPhones"
- "🔳🏢 Company-owned iPads"`),
workstations,
iosTeam,
ipadTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.ElementsMatch(
t,
appCfg.MDM.VolumePurchasingProgram.Value,
[]fleet.MDMAppleVolumePurchasingProgramInfo{
{
Location: "Fleet Device Management Inc.",
Teams: []string{
"💻 Workstations",
"📱🏢 Company-owned iPhones",
"🔳🏢 Company-owned iPads",
},
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "new key multiple elements",
cfgs: []string{
global(`
volume_purchasing_program:
- location: Acme Inc.
teams:
- "💻 Workstations"
- location: Fleet Device Management Inc.
teams:
- "📱🏢 Company-owned iPhones"
- "🔳🏢 Company-owned iPads"`),
workstations,
iosTeam,
ipadTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.ElementsMatch(
t,
appCfg.MDM.VolumePurchasingProgram.Value,
[]fleet.MDMAppleVolumePurchasingProgramInfo{
{
Location: "Acme Inc.",
Teams: []string{
"💻 Workstations",
},
},
{
Location: "Fleet Device Management Inc.",
Teams: []string{
"📱🏢 Company-owned iPhones",
"🔳🏢 Company-owned iPads",
},
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "using an undefined team errors",
cfgs: []string{
global(`
volume_purchasing_program:
- location: Fleet Device Management Inc.
teams:
- "💻 Workstations"
- "📱🏢 Company-owned iPhones"
- "🔳🏢 Company-owned iPads"`),
workstations,
ipadTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.ErrorContains(t, err, "volume_purchasing_program team 📱🏢 Company-owned iPhones not found in team configs")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.ErrorContains(t, err, "volume_purchasing_program team 📱🏢 Company-owned iPhones not found in team configs")
},
},
{
name: "no team is supported",
cfgs: []string{
global(`
volume_purchasing_program:
- location: Fleet Device Management Inc.
teams:
- "💻 Workstations"
- "📱🏢 Company-owned iPhones"
- "No team"`),
workstations,
iosTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.ElementsMatch(
t,
appCfg.MDM.VolumePurchasingProgram.Value,
[]fleet.MDMAppleVolumePurchasingProgramInfo{
{
Location: "Fleet Device Management Inc.",
Teams: []string{
"💻 Workstations",
"📱🏢 Company-owned iPhones",
"No team",
},
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "all teams is supported",
cfgs: []string{
global(`
volume_purchasing_program:
- location: Fleet Device Management Inc.
teams:
- "All teams"`),
workstations,
iosTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.ElementsMatch(
t,
appCfg.MDM.VolumePurchasingProgram.Value,
[]fleet.MDMAppleVolumePurchasingProgramInfo{
{
Location: "Fleet Device Management Inc.",
Teams: []string{
"All teams",
},
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "not provided teams defaults to no team",
cfgs: []string{
global(`
volume_purchasing_program:
- location: Fleet Device Management Inc.
teams:`),
workstations,
ipadTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
assert.Contains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.NoError(t, err)
assert.ElementsMatch(
t,
appCfg.MDM.VolumePurchasingProgram.Value,
[]fleet.MDMAppleVolumePurchasingProgramInfo{
{
Location: "Fleet Device Management Inc.",
Teams: nil,
},
},
)
assert.Contains(t, out, "[!] gitops succeeded")
},
},
{
name: "non existent location fails",
cfgs: []string{
global(`
volume_purchasing_program:
- location: Does not exist
teams:`),
workstations,
ipadTeam,
},
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.ErrorContains(t, err, "token with location Does not exist doesn't exist")
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
assert.NotContains(t, out, "[!] gitops dry run succeeded")
},
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
assert.ErrorContains(t, err, "token with location Does not exist doesn't exist")
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
assert.NotContains(t, out, "[!] gitops dry run succeeded")
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ds, savedAppConfigPtr, savedTeams := setupFullGitOpsPremiumServer(t)
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{{Location: "Fleet Device Management Inc."}, {Location: "Acme Inc."}}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
}
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
var res []*fleet.TeamSummary
for _, tm := range savedTeams {
res = append(res, &fleet.TeamSummary{Name: (*tm).Name, ID: (*tm).ID})
}
return res, nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
args := []string{"gitops"}
for _, cfg := range tt.cfgs {
if cfg != "" {
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = tmpFile.WriteString(cfg)
require.NoError(t, err)
args = append(args, "-f", tmpFile.Name())
}
}
// Dry run
out, err := runAppNoChecks(append(args, "--dry-run"))
tt.dryRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
if t.Failed() {
t.FailNow()
}
// Real run
out, err = runAppNoChecks(args)
tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
// Second real run, now that all the teams are saved
out, err = runAppNoChecks(args)
tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
})
}
}
type memKeyValueStore struct {
m map[string]string
}
func newMemKeyValueStore() *memKeyValueStore {
return &memKeyValueStore{
m: make(map[string]string),
}
}
func (m *memKeyValueStore) Set(ctx context.Context, key string, value string, expireTime time.Duration) error {
m.m[key] = value
return nil
}
func (m *memKeyValueStore) Get(ctx context.Context, key string) (*string, error) {
v, ok := m.m[key]
if !ok {
return nil, nil
}
return &v, nil
}