mirror of
https://github.com/fleetdm/fleet
synced 2026-05-10 18:51:03 +00:00
1744 lines
60 KiB
Go
1744 lines
60 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/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 TestFilenameValidation(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 TestBasicGlobalFreeGitOps(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,
|
|
) error {
|
|
return nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
|
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
|
) error {
|
|
return 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")
|
|
|
|
// 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 TestBasicGlobalPremiumGitOps(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,
|
|
},
|
|
)
|
|
|
|
ds.BatchSetMDMProfilesFunc = func(
|
|
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
|
macDecls []*fleet.MDMAppleDeclaration,
|
|
) error {
|
|
return nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
|
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
|
) error {
|
|
return 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
|
|
}
|
|
|
|
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:
|
|
`,
|
|
)
|
|
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 TestBasicTeamGitOps(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,
|
|
},
|
|
)
|
|
|
|
const secret = "TestSecret"
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) 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,
|
|
) error {
|
|
return nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
|
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
|
) error {
|
|
return 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, ¬FoundError{}
|
|
}
|
|
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
|
|
if savedTeam != nil && *savedTeam.Filename == filename {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
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, ¬FoundError{}
|
|
}
|
|
}
|
|
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.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
|
|
}
|
|
|
|
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}
|
|
`,
|
|
)
|
|
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")
|
|
|
|
// 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 TestFullGlobalGitOps(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,
|
|
) error {
|
|
appliedMacProfiles = macProfiles
|
|
appliedWinProfiles = winProfiles
|
|
return nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
|
return 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, ¬FoundError{}
|
|
}
|
|
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("APPLE_BM_DEFAULT_TEAM", teamName)
|
|
file := "./testdata/gitops/global_config_no_paths.yml"
|
|
|
|
// Dry run should fail because Apple BM Default Team does not exist and premium license is not set
|
|
_, err = runAppNoChecks([]string{"gitops", "-f", file, "--dry-run"})
|
|
require.Error(t, err)
|
|
assert.True(t, strings.Contains(err.Error(), "missing or invalid license"))
|
|
|
|
// Dry run
|
|
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
|
|
_ = 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 TestFullTeamGitOps(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,
|
|
},
|
|
)
|
|
|
|
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,
|
|
) error {
|
|
appliedMacProfiles = macProfiles
|
|
appliedWinProfiles = winProfiles
|
|
return nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
|
return 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 savedTeam != nil && savedTeam.Name == name {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
|
|
if savedTeam != nil && *savedTeam.Filename == filename {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
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, ¬FoundError{}
|
|
}
|
|
ds.ApplyQueriesFunc = func(
|
|
ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
|
|
) error {
|
|
appliedQueries = queries
|
|
return nil
|
|
}
|
|
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
|
return nil
|
|
}
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) 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
|
|
}
|
|
|
|
startSoftwareInstallerServer(t)
|
|
|
|
t.Setenv("TEST_TEAM_NAME", teamName)
|
|
|
|
// Dry run
|
|
const baseFilename = "team_config_no_paths.yml"
|
|
file := "./testdata/gitops/" + baseFilename
|
|
_ = runAppForTest(t, []string{"gitops", "-f", file, "--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)
|
|
|
|
// Real run
|
|
// Setting global calendar config
|
|
appConfig.Integrations = fleet.Integrations{
|
|
GoogleCalendar: []*fleet.GoogleCalendarIntegration{{}},
|
|
}
|
|
_ = runAppForTest(t, []string{"gitops", "-f", file})
|
|
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)
|
|
|
|
// Change team name
|
|
newTeamName := "New Team Name"
|
|
t.Setenv("TEST_TEAM_NAME", newTeamName)
|
|
_ = runAppForTest(t, []string{"gitops", "-f", file, "--dry-run"})
|
|
_ = runAppForTest(t, []string{"gitops", "-f", file})
|
|
require.NotNil(t, savedTeam)
|
|
assert.Equal(t, newTeamName, savedTeam.Name)
|
|
assert.Equal(t, baseFilename, *savedTeam.Filename)
|
|
|
|
// Now clear the settings
|
|
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}"}]
|
|
`,
|
|
)
|
|
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 TestBasicGlobalAndTeamGitOps(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,
|
|
},
|
|
)
|
|
|
|
// 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.VPPAppID) 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,
|
|
) error {
|
|
assert.Empty(t, macProfiles)
|
|
assert.Empty(t, winProfiles)
|
|
return 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,
|
|
) error {
|
|
assert.Empty(t, profileUUIDs)
|
|
return 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, ¬FoundError{}
|
|
}
|
|
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
|
|
if savedTeam != nil && *savedTeam.Filename == filename {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
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
|
|
}
|
|
|
|
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"}]
|
|
`,
|
|
)
|
|
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}"}]
|
|
`,
|
|
)
|
|
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"}]
|
|
`,
|
|
)
|
|
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 TestFullGlobalAndTeamGitOps(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, savedTeamPtr := 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
|
|
*savedTeamPtr = team
|
|
enrolledTeamSecrets = team.Secrets
|
|
return *savedTeamPtr, 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.VPPAppID) 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 on global file should fail because Apple BM Default Team does not exist (and has not been provided)
|
|
_, err = runAppNoChecks([]string{"gitops", "-f", globalFile, "--dry-run"})
|
|
require.Error(t, err)
|
|
assert.True(t, strings.Contains(err.Error(), "team name not found"))
|
|
|
|
// 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, *savedTeamPtr)
|
|
assert.Equal(t, teamName, (*savedTeamPtr).Name)
|
|
require.Len(t, enrolledTeamSecrets, 2)
|
|
}
|
|
|
|
func TestTeamSofwareInstallersGitOps(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 publicy accessible to the internet."},
|
|
{"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 MB"},
|
|
{"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_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", "cannot unmarshal string into Go struct field TeamSpecSoftware.packages of type bool"},
|
|
}
|
|
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 TestTeamSoftwareInstallersGitopsQueryEnv(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
|
|
}
|
|
|
|
_, err := runAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_software_installer_valid_env_query.yml"})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTeamVPPAppsGitOps(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.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)},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
|
ds, _, _ := setupFullGitOpsPremiumServer(t)
|
|
token, err := createVPPDataToken(c.tokenExpiration, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
|
|
return nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
|
|
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
asset := map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
fleet.MDMAssetVPPToken: {
|
|
Name: fleet.MDMAssetVPPToken,
|
|
Value: token,
|
|
},
|
|
}
|
|
return asset, nil
|
|
}
|
|
|
|
_, err = runAppNoChecks([]string{"gitops", "-f", c.file})
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func createVPPDataToken(expiration time.Time, orgName, location string) ([]byte, error) {
|
|
var randBytes [32]byte
|
|
_, err := rand.Read(randBytes[:])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generating random bytes: %w", err)
|
|
}
|
|
token := base64.StdEncoding.EncodeToString(randBytes[:])
|
|
raw := fleet.VPPTokenRaw{
|
|
OrgName: orgName,
|
|
Token: token,
|
|
ExpDate: expiration.Format("2006-01-02T15:04:05Z0700"),
|
|
}
|
|
rawJson, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshalling vpp raw token: %w", err)
|
|
}
|
|
|
|
base64Token := base64.StdEncoding.EncodeToString(rawJson)
|
|
|
|
dataToken := fleet.VPPTokenData{Token: base64Token, Location: location}
|
|
dataTokenJson, err := json.Marshal(dataToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshalling vpp data token: %w", err)
|
|
}
|
|
|
|
return dataTokenJson, nil
|
|
}
|
|
|
|
func TestCustomSettingsGitOps(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.VPPAppID) 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, **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,
|
|
},
|
|
)
|
|
|
|
// 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.VPPAppID) error {
|
|
return nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
|
|
var savedTeam *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,
|
|
) error {
|
|
return 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,
|
|
) error {
|
|
return 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 savedTeam != nil {
|
|
return []*fleet.Team{savedTeam}, 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 = 1
|
|
savedTeam = team
|
|
return savedTeam, nil
|
|
}
|
|
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
if savedTeam != nil && tid == savedTeam.ID {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
if savedTeam != nil && name == teamName {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
|
|
if savedTeam != nil && *savedTeam.Filename == filename {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
savedTeam = 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
|
|
}
|
|
|
|
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, &savedTeam
|
|
}
|