mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #32432 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
3639 lines
129 KiB
Go
3639 lines
129 KiB
Go
package fleetctl
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
|
"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"
|
|
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
digicert_mock "github.com/fleetdm/fleet/v4/server/mock/digicert"
|
|
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
|
|
scep_mock "github.com/fleetdm/fleet/v4/server/mock/scep"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/service"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
teamName = "Team Test"
|
|
fleetServerURL = "https://fleet.example.com"
|
|
orgName = "GitOps Test"
|
|
)
|
|
|
|
// setupDefaultTeamConfigMocks sets up the mock functions for DefaultTeamConfig operations
|
|
// needed for No Team webhook settings in premium license tests
|
|
func setupDefaultTeamConfigMocks(ds interface{}) {
|
|
switch d := ds.(type) {
|
|
case *mock.DataStore:
|
|
d.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
|
|
return &fleet.TeamConfig{}, nil
|
|
}
|
|
d.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
|
|
return nil
|
|
}
|
|
case *mock.Store:
|
|
// mock.Store embeds DataStore, so we can set the functions on the embedded struct
|
|
d.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
|
|
return &fleet.TeamConfig{}, nil
|
|
}
|
|
d.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
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 := testing_utils.RunServerWithMockedDS(t)
|
|
|
|
ds.BatchSetMDMProfilesFunc = func(
|
|
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
|
macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.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) ([]fleet.ScriptResponse, error) {
|
|
return []fleet.ScriptResponse{}, 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, int, *fleet.PaginationMetadata, error) {
|
|
return nil, 0, 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
|
|
}
|
|
|
|
// 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.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
var enrolledSecrets []*fleet.EnrollSecret
|
|
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
|
enrolledSecrets = secrets
|
|
return nil
|
|
}
|
|
|
|
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
|
return nil
|
|
}
|
|
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{}, nil
|
|
}
|
|
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{}, nil
|
|
}
|
|
|
|
// Mock DefaultTeamConfig functions for No Team webhook settings
|
|
setupDefaultTeamConfigMocks(ds)
|
|
|
|
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:
|
|
labels:
|
|
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)}
|
|
scepConfig := &scep_mock.SCEPConfigService{}
|
|
scepConfig.ValidateSCEPURLFunc = func(_ context.Context, _ string) error { return nil }
|
|
scepConfig.ValidateNDESSCEPAdminURLFunc = func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { return nil }
|
|
digiCertService := &digicert_mock.Service{}
|
|
digiCertService.VerifyProfileIDFunc = func(_ context.Context, _ fleet.DigiCertCA) error { return nil }
|
|
_, ds := testing_utils.RunServerWithMockedDS(
|
|
t, &service.TestServerOpts{
|
|
License: license,
|
|
KeyValueStore: testing_utils.NewMemKeyValueStore(),
|
|
EnableSCEPProxy: true,
|
|
SCEPConfigService: scepConfig,
|
|
DigiCertService: digiCertService,
|
|
},
|
|
)
|
|
|
|
ds.BatchSetMDMProfilesFunc = func(
|
|
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
|
macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.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) ([]fleet.ScriptResponse, error) {
|
|
return []fleet.ScriptResponse{}, 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, int, *fleet.PaginationMetadata, error) {
|
|
return nil, 0, nil, nil
|
|
}
|
|
|
|
// Mock DefaultTeamConfig functions for No Team webhook settings
|
|
setupDefaultTeamConfigMocks(ds)
|
|
|
|
// Mock appConfig
|
|
savedAppConfig := &fleet.AppConfig{}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
// Set a GitOps UI mode to verify that applying GitOps config won't overwrite it.
|
|
UIGitOpsMode: fleet.UIGitOpsModeConfig{
|
|
GitopsModeEnabled: true,
|
|
RepositoryURL: "https://didsomeonesaygitops.biz",
|
|
},
|
|
}, nil
|
|
}
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
|
savedAppConfig = config
|
|
return nil
|
|
}
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
|
|
return nil, 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
|
|
}
|
|
|
|
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
|
return nil
|
|
}
|
|
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{}, nil
|
|
}
|
|
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{}, nil
|
|
}
|
|
|
|
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
|
|
return nil
|
|
}
|
|
ds.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.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
|
|
return nil
|
|
}
|
|
|
|
// we'll use to mock datastore persistence
|
|
var storedCAs fleet.GroupedCertificateAuthorities
|
|
muStoredCAs := sync.Mutex{}
|
|
|
|
ds.BatchApplyCertificateAuthoritiesFunc = func(ctx context.Context, ops fleet.CertificateAuthoritiesBatchOperations) error {
|
|
muStoredCAs.Lock()
|
|
defer muStoredCAs.Unlock()
|
|
if len(ops.Delete) > 0 {
|
|
storedCAs = fleet.GroupedCertificateAuthorities{}
|
|
}
|
|
upserts := make([]*fleet.CertificateAuthority, 0, len(ops.Add)+len(ops.Update))
|
|
upserts = append(upserts, ops.Add...)
|
|
upserts = append(upserts, ops.Update...)
|
|
g, err := fleet.GroupCertificateAuthoritiesByType(upserts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
storedCAs = *g
|
|
return nil
|
|
}
|
|
|
|
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
|
|
require.True(t, includeSecrets) // for gitops flows we expect all calls to include secrets
|
|
|
|
muStoredCAs.Lock()
|
|
defer muStoredCAs.Unlock()
|
|
return &storedCAs, nil
|
|
}
|
|
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
|
|
return 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"
|
|
enable_disk_encryption: true
|
|
windows_require_bitlocker_pin: true
|
|
queries:
|
|
policies:
|
|
labels:
|
|
agent_options:
|
|
org_settings:
|
|
certificate_authorities:
|
|
ndes_scep_proxy:
|
|
url: https://ndes.example.com/scep
|
|
admin_url: https://ndes.example.com/admin
|
|
username: ndes_user
|
|
password: ndes_password
|
|
digicert:
|
|
- name: DigiCert
|
|
url: https://one.digicert.com
|
|
api_token: digicert_api_token
|
|
profile_id: digicert_profile_id
|
|
certificate_common_name: digicert_cn
|
|
certificate_user_principal_names: ["digicert_upn"]
|
|
certificate_seat_id: digicert_seat_id
|
|
- name: DigiCert2
|
|
url: https://two.digicert.com
|
|
api_token: digicert_api_token2
|
|
profile_id: digicert_profile_id2
|
|
certificate_common_name: digicert_cn2
|
|
certificate_seat_id: digicert_seat_id2
|
|
custom_scep_proxy:
|
|
- name: CustomScepProxy
|
|
url: https://custom.scep.proxy.com
|
|
challenge: challenge
|
|
- name: CustomScepProxy2
|
|
url: https://custom.scep.proxy.com2
|
|
challenge: challenge2
|
|
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")
|
|
|
|
// Dry run will get existing CAs but won't apply anything
|
|
assert.True(t, ds.GetGroupedCertificateAuthoritiesFuncInvoked)
|
|
ds.GetGroupedCertificateAuthoritiesFuncInvoked = false
|
|
assert.False(t, ds.BatchApplyCertificateAuthoritiesFuncInvoked)
|
|
|
|
// 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)
|
|
|
|
// GitOps should not overwrite GitOps UI Mode.
|
|
assert.Equal(t, savedAppConfig.UIGitOpsMode.GitopsModeEnabled, true)
|
|
assert.Equal(t, savedAppConfig.UIGitOpsMode.RepositoryURL, "https://didsomeonesaygitops.biz")
|
|
|
|
// Check MDM settings
|
|
require.True(t, savedAppConfig.MDM.EnableDiskEncryption.Value)
|
|
require.True(t, savedAppConfig.MDM.RequireBitLockerPIN.Value)
|
|
|
|
// Check certificate authorities
|
|
assert.True(t, ds.GetGroupedCertificateAuthoritiesFuncInvoked)
|
|
assert.True(t, ds.BatchApplyCertificateAuthoritiesFuncInvoked)
|
|
muStoredCAs.Lock()
|
|
defer muStoredCAs.Unlock()
|
|
|
|
// check ndes
|
|
require.NotNil(t, storedCAs.NDESSCEP)
|
|
assert.Equal(t, "https://ndes.example.com/scep", storedCAs.NDESSCEP.URL)
|
|
|
|
// check digicert
|
|
assert.True(t, digiCertService.VerifyProfileIDFuncInvoked)
|
|
assert.Len(t, storedCAs.DigiCert, 2)
|
|
digicertByName := make(map[string]fleet.DigiCertCA, 2)
|
|
for _, d := range storedCAs.DigiCert {
|
|
digicertByName[d.Name] = d
|
|
}
|
|
d1, ok := digicertByName["DigiCert"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "DigiCert", d1.Name)
|
|
assert.Equal(t, "https://one.digicert.com", d1.URL)
|
|
assert.Equal(t, "digicert_api_token", d1.APIToken)
|
|
assert.Equal(t, "digicert_profile_id", d1.ProfileID)
|
|
assert.Equal(t, "digicert_cn", d1.CertificateCommonName)
|
|
assert.Equal(t, []string{"digicert_upn"}, d1.CertificateUserPrincipalNames)
|
|
assert.Equal(t, "digicert_seat_id", d1.CertificateSeatID)
|
|
d2, ok := digicertByName["DigiCert2"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "DigiCert2", d2.Name)
|
|
assert.Equal(t, "https://two.digicert.com", d2.URL)
|
|
assert.Equal(t, "digicert_api_token2", d2.APIToken)
|
|
assert.Equal(t, "digicert_profile_id2", d2.ProfileID)
|
|
assert.Equal(t, "digicert_cn2", d2.CertificateCommonName)
|
|
assert.Empty(t, d2.CertificateUserPrincipalNames)
|
|
assert.Equal(t, "digicert_seat_id2", d2.CertificateSeatID)
|
|
|
|
// check custom SCEP proxies
|
|
assert.Len(t, storedCAs.CustomScepProxy, 2)
|
|
customSCEPByName := make(map[string]fleet.CustomSCEPProxyCA, 2)
|
|
for _, scep := range storedCAs.CustomScepProxy {
|
|
customSCEPByName[scep.Name] = scep
|
|
}
|
|
cs1, ok := customSCEPByName["CustomScepProxy"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "CustomScepProxy", cs1.Name)
|
|
assert.Equal(t, "https://custom.scep.proxy.com", cs1.URL)
|
|
assert.Equal(t, "challenge", cs1.Challenge)
|
|
cs2, ok := customSCEPByName["CustomScepProxy2"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "CustomScepProxy2", cs2.Name)
|
|
assert.Equal(t, "https://custom.scep.proxy.com2", cs2.URL)
|
|
assert.Equal(t, "challenge2", cs2.Challenge)
|
|
|
|
// // TODO(hca): if/when we have mock hydrant service, add yaml below to tmpFile and include
|
|
// // following tests
|
|
// // ```yaml
|
|
// // hydrant:
|
|
// // - name: Hydrant
|
|
// // url: https://hydrant.example.com
|
|
// // client_id: hydrant_id
|
|
// // client_secret: hydrant_secret
|
|
// // - name: Hydrant2
|
|
// // url: https://hydrant2.example.com
|
|
// // client_id: hydrant2_id
|
|
// // client_secret: hydrant2_secret
|
|
// // ````
|
|
|
|
// // check hydrant
|
|
// assert.Len(t, storedCAs.Hydrant, 2)
|
|
// hydrantByName := make(map[string]fleet.HydrantCA, 2)
|
|
// for _, h := range storedCAs.Hydrant {
|
|
// hydrantByName[h.Name] = h
|
|
// }
|
|
// h1, ok := hydrantByName["Hydrant"]
|
|
// assert.True(t, ok)
|
|
// assert.Equal(t, "Hydrant", h1.Name)
|
|
// assert.Equal(t, "https://hydrant.example.com", h1.URL)
|
|
// assert.Equal(t, "hydrant_id", h1.ClientID)
|
|
// assert.Equal(t, "hydrant_secret", h1.ClientSecret)
|
|
// h2, ok := hydrantByName["Hydrant2"]
|
|
// assert.True(t, ok)
|
|
// assert.Equal(t, "Hydrant2", h2.Name)
|
|
// assert.Equal(t, "https://hydrant2.example.com", h2.URL)
|
|
// assert.Equal(t, "hydrant2_id", h2.ClientID)
|
|
// assert.Equal(t, "hydrant2_secret", h2.ClientSecret)
|
|
}
|
|
|
|
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 := testing_utils.RunServerWithMockedDS(
|
|
t, &service.TestServerOpts{
|
|
License: license,
|
|
KeyValueStore: testing_utils.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) ([]fleet.ScriptResponse, error) {
|
|
return []fleet.ScriptResponse{}, nil
|
|
}
|
|
ds.BatchSetMDMProfilesFunc = func(
|
|
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.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, int, *fleet.PaginationMetadata, error) {
|
|
return nil, 0, nil, nil
|
|
}
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
|
|
return nil, nil
|
|
}
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
|
|
// Mock DefaultTeamConfig functions for No Team webhook settings
|
|
setupDefaultTeamConfigMocks(ds)
|
|
|
|
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{}
|
|
}
|
|
// Track default team config for team 0
|
|
defaultTeamConfig := &fleet.TeamConfig{}
|
|
|
|
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
if tid == 0 {
|
|
// Return a mock team 0 with the default config
|
|
return &fleet.Team{
|
|
ID: 0,
|
|
Name: fleet.ReservedNameNoTeam,
|
|
Config: *defaultTeamConfig,
|
|
}, nil
|
|
}
|
|
if tid == team.ID {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
ds.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
|
|
return defaultTeamConfig, nil
|
|
}
|
|
|
|
ds.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
|
|
defaultTeamConfig = config
|
|
return 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, isNew 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.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
|
|
}
|
|
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
|
|
return 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:
|
|
labels:
|
|
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)
|
|
assert.True(t, savedTeam.Config.Features.EnableSoftwareInventory)
|
|
|
|
// 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 := testing_utils.RunServerWithMockedDS(
|
|
t, &service.TestServerOpts{
|
|
MDMStorage: new(mdmmock.MDMAppleStore),
|
|
MDMPusher: testing_utils.MockPusher{},
|
|
FleetConfig: &fleetCfg,
|
|
},
|
|
)
|
|
|
|
var appliedScripts []*fleet.Script
|
|
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
|
|
appliedScripts = scripts
|
|
var scriptResponses []fleet.ScriptResponse
|
|
for _, script := range scripts {
|
|
scriptResponses = append(scriptResponses, fleet.ScriptResponse{
|
|
ID: script.ID,
|
|
Name: script.Name,
|
|
TeamID: script.TeamID,
|
|
})
|
|
}
|
|
|
|
return scriptResponses, 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, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
|
|
) (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.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
|
|
// Policies
|
|
policy := fleet.Policy{}
|
|
policy.ID = 1
|
|
policy.Name = "Policy to delete"
|
|
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 nil, nil, nil
|
|
}
|
|
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, int, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.Query{&query}, 1, nil, 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
|
|
}
|
|
|
|
var appliedLabelSpecs []*fleet.LabelSpec
|
|
var deletedLabels []string
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
|
|
return []*fleet.LabelSpec{
|
|
{
|
|
Name: "a",
|
|
Description: "A global label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
|
Hosts: []string{"host2", "host3"},
|
|
},
|
|
{
|
|
Name: "c",
|
|
Description: "A label that should be deleted",
|
|
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
|
Query: "SELECT 1 from osquery_info",
|
|
},
|
|
}, nil
|
|
}
|
|
ds.ApplyLabelSpecsWithAuthorFunc = func(ctx context.Context, specs []*fleet.LabelSpec, authorID *uint) (err error) {
|
|
appliedLabelSpecs = specs
|
|
return nil
|
|
}
|
|
|
|
ds.DeleteLabelFunc = func(ctx context.Context, name string) error {
|
|
deletedLabels = append(deletedLabels, name)
|
|
return nil
|
|
}
|
|
|
|
ds.LabelsByNameFunc = func(ctx context.Context, names []string) (map[string]*fleet.Label, error) {
|
|
return map[string]*fleet.Label{
|
|
"a": {
|
|
ID: 1,
|
|
Name: "a",
|
|
},
|
|
"b": {
|
|
ID: 2,
|
|
Name: "b",
|
|
},
|
|
}, 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, isNew 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
|
|
}
|
|
|
|
// Needed for checking tokens
|
|
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
|
return nil
|
|
}
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{}, nil
|
|
}
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{}, nil
|
|
}
|
|
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
|
return document, nil, 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)
|
|
|
|
// Dry run w/ top-level labels key
|
|
logs := RunAppForTest(t, []string{"gitops", "-f", "./testdata/gitops/global_config_no_paths.yml", "--dry-run"})
|
|
fmt.Printf("%s", logs)
|
|
fmt.Printf("-----------\n")
|
|
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)
|
|
assert.Len(t, appliedLabelSpecs, 0)
|
|
assert.Len(t, deletedLabels, 0)
|
|
|
|
// Dry run w/out top-level labels key
|
|
logs = RunAppForTest(t, []string{"gitops", "-f", "./testdata/gitops/global_config_no_paths_no_labels.yml", "--dry-run"})
|
|
fmt.Printf("%s", logs)
|
|
fmt.Printf("-----------\n")
|
|
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)
|
|
assert.Len(t, appliedLabelSpecs, 0)
|
|
assert.Len(t, deletedLabels, 0)
|
|
|
|
// Real run w/ top-level labels key
|
|
logs = RunAppForTest(t, []string{"gitops", "-f", "./testdata/gitops/global_config_no_paths.yml"})
|
|
fmt.Printf("%s", logs)
|
|
fmt.Printf("-----------\n")
|
|
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.Len(t, appliedPolicySpecs[0].LabelsIncludeAny, 1)
|
|
assert.Len(t, appliedPolicySpecs[0].LabelsExcludeAny, 0)
|
|
assert.Equal(t, appliedPolicySpecs[0].LabelsIncludeAny[0], "a")
|
|
assert.Len(t, appliedPolicySpecs[1].LabelsIncludeAny, 0)
|
|
assert.Len(t, appliedPolicySpecs[1].LabelsExcludeAny, 1)
|
|
assert.Equal(t, appliedPolicySpecs[1].LabelsExcludeAny[0], "b")
|
|
|
|
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)
|
|
assert.Len(t, appliedLabelSpecs, 2)
|
|
assert.Len(t, deletedLabels, 1)
|
|
assert.Len(t, appliedQueries[0].LabelsIncludeAny, 2)
|
|
assert.Contains(t, []string{appliedQueries[0].LabelsIncludeAny[0].LabelName, appliedQueries[0].LabelsIncludeAny[1].LabelName}, "a")
|
|
assert.Contains(t, []string{appliedQueries[0].LabelsIncludeAny[0].LabelName, appliedQueries[0].LabelsIncludeAny[1].LabelName}, "b")
|
|
|
|
// Reset labels arrays
|
|
deletedLabels = make([]string, 0)
|
|
appliedLabelSpecs = make([]*fleet.LabelSpec, 0)
|
|
// Real run w/out top-level labels key
|
|
logs = RunAppForTest(t, []string{"gitops", "-f", "./testdata/gitops/global_config_no_paths_no_labels.yml"})
|
|
fmt.Printf("%s", logs)
|
|
assert.Len(t, appliedLabelSpecs, 0)
|
|
assert.Len(t, deletedLabels, 0)
|
|
}
|
|
|
|
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 := testing_utils.RunServerWithMockedDS(
|
|
t, &service.TestServerOpts{
|
|
License: license,
|
|
MDMStorage: new(mdmmock.MDMAppleStore),
|
|
MDMPusher: testing_utils.MockPusher{},
|
|
FleetConfig: &fleetCfg,
|
|
NoCacheDatastore: true,
|
|
KeyValueStore: testing_utils.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) ([]fleet.ScriptResponse, error) {
|
|
appliedScripts = scripts
|
|
var scriptResponses []fleet.ScriptResponse
|
|
for _, script := range scripts {
|
|
scriptResponses = append(scriptResponses, fleet.ScriptResponse{
|
|
ID: script.ID,
|
|
Name: script.Name,
|
|
TeamID: script.TeamID,
|
|
})
|
|
}
|
|
|
|
return scriptResponses, 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, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
|
|
) (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, vars []fleet.FleetVarName) (*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
|
|
}
|
|
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
|
|
return &fleet.MDMAppleBootstrapPackage{}, nil
|
|
}
|
|
ds.DeleteMDMAppleBootstrapPackageFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
|
return nil, nil
|
|
}
|
|
ds.DeleteMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) error {
|
|
return nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
|
|
// Mock DefaultTeamConfig functions for No Team webhook settings
|
|
setupDefaultTeamConfigMocks(ds)
|
|
|
|
// 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, ¬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, isNew 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
|
|
}
|
|
|
|
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
|
|
return map[uint]*fleet.ExistingSoftwareInstaller{}, 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) {
|
|
if teamID != 0 {
|
|
return []*fleet.Policy{&policy}, nil, nil
|
|
}
|
|
return nil, 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
|
|
}
|
|
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
|
return document, nil, 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, int, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.Query{&query}, 1, nil, 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
|
|
}
|
|
|
|
testing_utils.AddLabelMocks(ds)
|
|
|
|
var appliedSoftwareInstallers []*fleet.UploadSoftwareInstallerPayload
|
|
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
|
if teamID != nil && *teamID != 0 {
|
|
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
|
|
}
|
|
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
|
|
return nil
|
|
}
|
|
|
|
testing_utils.StartSoftwareInstallerServer(t)
|
|
|
|
t.Setenv("TEST_TEAM_NAME", teamName)
|
|
t.Setenv("ENABLE_DISK_ENCRYPTION", "true")
|
|
t.Setenv("WINDOWS_REQUIRE_BITLOCKER_PIN", "true")
|
|
|
|
// 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.True(t, savedTeam.Config.MDM.RequireBitLockerPIN)
|
|
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 disk encryption settings
|
|
t.Setenv("ENABLE_DISK_ENCRYPTION", "false")
|
|
t.Setenv("WINDOWS_REQUIRE_BITLOCKER_PIN", "false")
|
|
_ = RunAppForTest(t, []string{"gitops", "-f", gitopsFile, "--dry-run"})
|
|
_ = RunAppForTest(t, []string{"gitops", "-f", gitopsFile})
|
|
require.NotNil(t, savedTeam)
|
|
assert.False(t, savedTeam.Config.MDM.EnableDiskEncryption)
|
|
assert.False(t, savedTeam.Config.MDM.RequireBitLockerPIN)
|
|
|
|
// 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 createFakeITunesAndVPPServices(t *testing.T) {
|
|
config := &testing_utils.AppleVPPConfigSrvConf{
|
|
Assets: []vpp.Asset{
|
|
{
|
|
AdamID: "1",
|
|
PricingParam: "STDQ",
|
|
AvailableCount: 12,
|
|
},
|
|
{
|
|
AdamID: "2",
|
|
PricingParam: "STDQ",
|
|
AvailableCount: 3,
|
|
},
|
|
},
|
|
SerialNumbers: []string{"123", "456"},
|
|
}
|
|
testing_utils.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)
|
|
}
|
|
|
|
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 := testing_utils.RunServerWithMockedDS(
|
|
t, &service.TestServerOpts{
|
|
License: license,
|
|
KeyValueStore: testing_utils.NewMemKeyValueStore(),
|
|
},
|
|
)
|
|
|
|
// Mock appConfig
|
|
savedAppConfig := &fleet.AppConfig{}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appConfig := savedAppConfig.Copy()
|
|
return 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
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, 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, isNew 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, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
assert.Empty(t, macProfiles)
|
|
assert.Empty(t, winProfiles)
|
|
assert.Empty(t, macDecls)
|
|
assert.Empty(t, androidProfiles)
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
|
|
assert.Empty(t, scripts)
|
|
return []fleet.ScriptResponse{}, 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) {
|
|
if savedTeam != nil {
|
|
return []*fleet.Team{savedTeam}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
|
|
return nil, 0, nil, nil
|
|
}
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
|
|
testing_utils.AddLabelMocks(ds)
|
|
|
|
// Mock DefaultTeamConfig functions for No Team webhook settings
|
|
setupDefaultTeamConfigMocks(ds)
|
|
|
|
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
|
|
}
|
|
// Track default team config for team 0
|
|
defaultTeamConfig := &fleet.TeamConfig{}
|
|
|
|
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
if tid == 0 {
|
|
// Return a mock team 0 with the default config
|
|
return &fleet.Team{
|
|
ID: 0,
|
|
Name: fleet.ReservedNameNoTeam,
|
|
Config: *defaultTeamConfig,
|
|
}, nil
|
|
}
|
|
if tid == team.ID {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
ds.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
|
|
return defaultTeamConfig, nil
|
|
}
|
|
|
|
ds.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
|
|
defaultTeamConfig = config
|
|
return 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
|
|
}
|
|
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
|
|
}
|
|
|
|
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
|
return nil
|
|
}
|
|
|
|
vppToken := &fleet.VPPTokenDB{
|
|
Location: "Foobar",
|
|
RenewDate: time.Now().Add(24 * 365 * time.Hour),
|
|
}
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{vppToken}, nil
|
|
}
|
|
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{}, nil
|
|
}
|
|
ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
|
|
return 0, nil
|
|
}
|
|
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
|
|
return nil
|
|
}
|
|
|
|
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
|
|
var teamsSummary []*fleet.TeamSummary
|
|
if savedTeam != nil {
|
|
teamsSummary = append(teamsSummary, &fleet.TeamSummary{
|
|
ID: savedTeam.ID,
|
|
Name: savedTeam.Name,
|
|
Description: savedTeam.Description,
|
|
})
|
|
}
|
|
return teamsSummary, nil
|
|
}
|
|
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
if teamID != nil && *teamID == savedTeam.ID {
|
|
return vppToken, nil
|
|
}
|
|
return nil, ¬FoundError{}
|
|
}
|
|
|
|
ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
|
|
return vppToken, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
ds.BatchApplyCertificateAuthoritiesFunc = func(ctx context.Context, ops fleet.CertificateAuthoritiesBatchOperations) error {
|
|
return nil
|
|
}
|
|
|
|
createFakeITunesAndVPPServices(t)
|
|
|
|
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
|
|
t.Setenv("ORG_NAME", orgName)
|
|
t.Setenv("TEST_TEAM_NAME", teamName)
|
|
t.Setenv("TEST_SECRET", secret)
|
|
|
|
_, 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}
|
|
mdm:
|
|
volume_purchasing_program:
|
|
- location: Foobar
|
|
teams:
|
|
- "${TEST_TEAM_NAME}"
|
|
secrets: [{"secret":"globalSecret"}]
|
|
software:
|
|
`,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
teamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = teamFile.WriteString(
|
|
`
|
|
controls:
|
|
queries:
|
|
policies:
|
|
agent_options:
|
|
name: ${TEST_TEAM_NAME}
|
|
team_settings:
|
|
secrets: [{"secret":"${TEST_SECRET}"}]
|
|
software:
|
|
app_store_apps:
|
|
- app_store_id: '1'
|
|
`,
|
|
)
|
|
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.NoError(t, err)
|
|
|
|
// No global file, only team file
|
|
_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name(), "--dry-run"})
|
|
require.NoError(t, err)
|
|
|
|
// Global file specified multiple times
|
|
_, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "-f", globalFile.Name(), "--dry-run"})
|
|
require.Error(t, err)
|
|
fmt.Printf("err.Error(): %v\n", err.Error())
|
|
assert.Contains(t, err.Error(), "only one global config file may be provided")
|
|
|
|
// 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")
|
|
|
|
ds.GetVPPTokenByTeamIDFuncInvoked = false
|
|
|
|
// 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 should not attempt to get the VPP token when applying VPP apps (it may not exist).
|
|
require.False(t, ds.GetVPPTokenByTeamIDFuncInvoked)
|
|
ds.ListTeamsFuncInvoked = false
|
|
|
|
// Dry run, deleting other teams
|
|
savedAppConfig = &fleet.AppConfig{}
|
|
_ = 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)
|
|
|
|
// Dry run again (after team was created by real run)
|
|
ds.GetVPPTokenByTeamIDFuncInvoked = false
|
|
ds.GetSoftwareCategoryIDsFuncInvoked = false
|
|
_ = RunAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
|
|
// Dry run should attempt to get the VPP token when applying VPP apps (it may not exist), so we want to error to the user.
|
|
// But we want to verify it does not call a method later, aka. exits early correctly.
|
|
require.True(t, ds.GetVPPTokenByTeamIDFuncInvoked)
|
|
require.False(t, ds.GetSoftwareCategoryIDsFuncInvoked)
|
|
|
|
// 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 := testing_utils.RunServerWithMockedDS(
|
|
t, &service.TestServerOpts{
|
|
License: license,
|
|
KeyValueStore: testing_utils.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
|
|
}
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) 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, isNew 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, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
assert.Empty(t, macProfiles)
|
|
assert.Empty(t, winProfiles)
|
|
assert.Empty(t, macDecls)
|
|
assert.Empty(t, androidProfiles)
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
|
|
assert.Empty(t, scripts)
|
|
return []fleet.ScriptResponse{}, 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, int, *fleet.PaginationMetadata, error) {
|
|
return nil, 0, nil, nil
|
|
}
|
|
testing_utils.AddLabelMocks(ds)
|
|
|
|
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
|
|
}
|
|
// Track default team config for team 0
|
|
defaultTeamConfig := &fleet.TeamConfig{}
|
|
|
|
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
if tid == 0 {
|
|
// Return a mock team 0 with the default config
|
|
return &fleet.Team{
|
|
ID: 0,
|
|
Name: fleet.ReservedNameNoTeam,
|
|
Config: *defaultTeamConfig,
|
|
}, nil
|
|
}
|
|
if tid == team.ID {
|
|
return savedTeam, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
ds.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
|
|
return defaultTeamConfig, nil
|
|
}
|
|
|
|
ds.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
|
|
defaultTeamConfig = config
|
|
return 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
|
|
}
|
|
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
|
|
}
|
|
|
|
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
|
return nil
|
|
}
|
|
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{}, nil
|
|
}
|
|
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{}, nil
|
|
}
|
|
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
|
|
return nil
|
|
}
|
|
ds.BatchApplyCertificateAuthoritiesFunc = func(ctx context.Context, ops fleet.CertificateAuthoritiesBatchOperations) error {
|
|
return nil
|
|
}
|
|
|
|
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
|
|
|
|
teamFileBasic := createTeamFileBasic(t, secret)
|
|
|
|
// We cannot use os.CreateTemp because the filename must be exactly "no-team.yml"
|
|
noTeamFilePath := filepath.Join(t.TempDir(), "no-team.yml")
|
|
noTeamFileBasic, err := os.Create(noTeamFilePath)
|
|
require.NoError(t, err)
|
|
_, err = noTeamFileBasic.WriteString(`
|
|
controls:
|
|
policies:
|
|
name: No team
|
|
software:
|
|
`)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("global defines software -- should fail", func(t *testing.T) {
|
|
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)
|
|
|
|
// Dry run, global defines software, should fail.
|
|
_, err = RunAppNoChecks([]string{
|
|
"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFileBasic.Name(), "-f",
|
|
noTeamFileBasic.Name(),
|
|
"--dry-run",
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "'software' cannot be set on global file")
|
|
// Real run, global defines software, should fail.
|
|
_, err = RunAppNoChecks([]string{
|
|
"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFileBasic.Name(), "-f",
|
|
noTeamFileBasic.Name(),
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "'software' cannot be set on global file")
|
|
})
|
|
|
|
t.Run("both global and no-team.yml define controls -- should fail", func(t *testing.T) {
|
|
globalFileWithControls := createGlobalFileWithControls(t, fleetServerURL, orgName)
|
|
|
|
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)
|
|
|
|
// Dry run, both global and no-team.yml define controls.
|
|
_, err = RunAppNoChecks([]string{
|
|
"gitops", "-f", globalFileWithControls.Name(), "-f", teamFileBasic.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", teamFileBasic.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"))
|
|
})
|
|
|
|
t.Run("no-team.yml defines policy with calendar events enabled -- should fail", func(t *testing.T) {
|
|
globalFileWithControls := createGlobalFileWithControls(t, fleetServerURL, orgName)
|
|
|
|
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)
|
|
|
|
// Dry run, both global and no-team.yml defines policy with calendar events enabled.
|
|
_, err = RunAppNoChecks([]string{
|
|
"gitops", "-f", globalFileWithControls.Name(), "-f", teamFileBasic.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", teamFileBasic.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())
|
|
})
|
|
|
|
t.Run("global and no-team.yml DO NOT define controls -- should fail", func(t *testing.T) {
|
|
globalFileWithoutControlsAndSoftwareKeys := createGlobalFileWithoutControlsAndSoftwareKeys(t, fleetServerURL, orgName)
|
|
|
|
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, controls should be defined somewhere, either in no-team.yml or global.
|
|
_, err = RunAppNoChecks([]string{
|
|
"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.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
|
|
_, err = RunAppNoChecks([]string{
|
|
"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.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"))
|
|
})
|
|
|
|
t.Run("controls only defined in no-team.yml", func(t *testing.T) {
|
|
savedAppConfig = &fleet.AppConfig{}
|
|
|
|
globalFileWithoutControlsAndSoftwareKeys := createGlobalFileWithoutControlsAndSoftwareKeys(t, fleetServerURL, orgName)
|
|
|
|
// Dry run, global file without controls and software keys.
|
|
_ = RunAppForTest(t,
|
|
[]string{
|
|
"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.Name(), "-f",
|
|
noTeamFileBasic.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", teamFileBasic.Name(), "-f",
|
|
noTeamFileBasic.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)
|
|
})
|
|
|
|
t.Run("basic global and no-team.yml", func(t *testing.T) {
|
|
savedAppConfig = &fleet.AppConfig{}
|
|
// Dry run
|
|
_ = RunAppForTest(t,
|
|
[]string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFileBasic.Name(), "--dry-run"})
|
|
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
|
|
// Real run
|
|
_ = RunAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFileBasic.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 createTeamFileBasic(t *testing.T, secret string) *os.File {
|
|
teamFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml")
|
|
require.NoError(t, err)
|
|
_, err = teamFileBasic.WriteString(fmt.Sprintf(`
|
|
controls:
|
|
queries:
|
|
policies:
|
|
agent_options:
|
|
name: %s
|
|
team_settings:
|
|
secrets: [{"secret":"%s"}]
|
|
software:
|
|
`, teamName, secret),
|
|
)
|
|
require.NoError(t, err)
|
|
return teamFileBasic
|
|
}
|
|
|
|
func createGlobalFileBasic(t *testing.T, fleetServerURL string, orgName string) *os.File {
|
|
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)
|
|
return globalFileBasic
|
|
}
|
|
|
|
func createGlobalFileWithoutControlsAndSoftwareKeys(t *testing.T, fleetServerURL string, orgName string) *os.File {
|
|
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)
|
|
return globalFileWithoutControlsAndSoftwareKeys
|
|
}
|
|
|
|
func createGlobalFileWithControls(t *testing.T, fleetServerURL string, orgName string) *os.File {
|
|
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)
|
|
return globalFileWithControls
|
|
}
|
|
|
|
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 := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
testing_utils.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
|
|
}
|
|
|
|
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
|
return nil
|
|
}
|
|
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{}, nil
|
|
}
|
|
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{}, nil
|
|
}
|
|
ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
|
|
return 0, nil
|
|
}
|
|
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
|
|
return map[uint]*fleet.ExistingSoftwareInstaller{}, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
|
|
return nil
|
|
}
|
|
|
|
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
|
|
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,
|
|
_ sqlx.QueryerContext,
|
|
) (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
|
|
}
|
|
|
|
ds.LabelsByNameFunc = func(ctx context.Context, names []string) (map[string]*fleet.Label, error) {
|
|
return map[string]*fleet.Label{
|
|
"a": {
|
|
ID: 1,
|
|
Name: "a",
|
|
},
|
|
"b": {
|
|
ID: 2,
|
|
Name: "b",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
globalFile := "./testdata/gitops/global_config_no_paths.yml"
|
|
teamFile := "./testdata/gitops/team_config_no_paths.yml"
|
|
|
|
t.Setenv("ENABLE_DISK_ENCRYPTION", "true")
|
|
t.Setenv("WINDOWS_REQUIRE_BITLOCKER_PIN", "true")
|
|
|
|
// 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)
|
|
|
|
t.Run("no-team.yml using relative paths", func(t *testing.T) {
|
|
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
|
|
teamFileBasic := createTeamFileBasic(t, teamName)
|
|
|
|
noTeamDir := t.TempDir()
|
|
noTeamFile, err := os.Create(filepath.Join(noTeamDir, "no-team.yml"))
|
|
require.NoError(t, err)
|
|
_, err = noTeamFile.WriteString(`
|
|
controls:
|
|
scripts:
|
|
- path: ./script.sh
|
|
windows_enabled_and_configured: true
|
|
android_enabled_and_configured: true
|
|
macos_settings:
|
|
custom_settings:
|
|
- path: ./config.json
|
|
windows_settings:
|
|
custom_settings:
|
|
- path: ./config2.xml
|
|
android_settings:
|
|
custom_settings:
|
|
- path: ./config3.json
|
|
policies:
|
|
name: No team
|
|
software:
|
|
`)
|
|
require.NoError(t, err)
|
|
|
|
ddmFile, err := os.Create(filepath.Join(noTeamDir, "config.json"))
|
|
require.NoError(t, err)
|
|
_, err = ddmFile.WriteString(`
|
|
{
|
|
"Type": "com.apple.configuration.passcode.settings",
|
|
"Identifier": "com.fleetdm.config.passcode.settings",
|
|
"Payload": {
|
|
"RequireAlphanumericPasscode": true
|
|
}
|
|
}
|
|
`)
|
|
require.NoError(t, err)
|
|
|
|
cspFile, err := os.Create(filepath.Join(noTeamDir, "config2.xml"))
|
|
require.NoError(t, err)
|
|
_, err = cspFile.WriteString(`<Replace>bozo</Replace>`)
|
|
require.NoError(t, err)
|
|
|
|
androidFile, err := os.Create(filepath.Join(noTeamDir, "config3.json"))
|
|
require.NoError(t, err)
|
|
_, err = androidFile.WriteString(`{"name":"Android profile"}`)
|
|
require.NoError(t, err)
|
|
|
|
scriptFile, err := os.Create(filepath.Join(noTeamDir, "script.sh"))
|
|
require.NoError(t, err)
|
|
_, err = scriptFile.WriteString(`echo "Hello, world!"`)
|
|
require.NoError(t, err)
|
|
|
|
// Validate that global config is required when running noTeam
|
|
_, err = RunAppNoChecks(
|
|
[]string{"gitops", "-f", noTeamFile.Name(), "--dry-run"})
|
|
assert.Error(t, err)
|
|
|
|
// Dry run
|
|
ds.SaveAppConfigFuncInvoked = false
|
|
ds.BatchSetScriptsFuncInvoked = false
|
|
_ = RunAppForTest(t,
|
|
[]string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFile.Name(), "--dry-run"})
|
|
assert.False(t, ds.SaveAppConfigFuncInvoked)
|
|
assert.False(t, ds.BatchSetScriptsFuncInvoked)
|
|
|
|
// Real run
|
|
_ = RunAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFile.Name()})
|
|
assert.Equal(t, orgName, (*savedAppConfigPtr).OrgInfo.OrgName)
|
|
assert.Equal(t, fleetServerURL, (*savedAppConfigPtr).ServerSettings.ServerURL)
|
|
require.Len(t, (*savedAppConfigPtr).MDM.MacOSSettings.CustomSettings, 1)
|
|
assert.Equal(t, filepath.Base(ddmFile.Name()), filepath.Base((*savedAppConfigPtr).MDM.MacOSSettings.CustomSettings[0].Path))
|
|
require.Len(t, (*savedAppConfigPtr).MDM.WindowsSettings.CustomSettings.Value, 1)
|
|
assert.Equal(t, filepath.Base(cspFile.Name()), filepath.Base((*savedAppConfigPtr).MDM.WindowsSettings.CustomSettings.Value[0].Path))
|
|
require.Len(t, (*savedAppConfigPtr).MDM.AndroidSettings.CustomSettings.Value, 1)
|
|
assert.Equal(t, filepath.Base(androidFile.Name()), filepath.Base((*savedAppConfigPtr).MDM.AndroidSettings.CustomSettings.Value[0].Path))
|
|
assert.True(t, ds.BatchSetScriptsFuncInvoked)
|
|
|
|
// Get applied policies for the team
|
|
teamAppliedPoliceSpecs := make([]*fleet.PolicySpec, 0)
|
|
for _, appliedPolicySpec := range appliedPolicySpecs {
|
|
if appliedPolicySpec.Team == teamName {
|
|
teamAppliedPoliceSpecs = append(teamAppliedPoliceSpecs, appliedPolicySpec)
|
|
}
|
|
}
|
|
assert.Len(t, teamAppliedPoliceSpecs, 5)
|
|
assert.Len(t, teamAppliedPoliceSpecs[0].LabelsIncludeAny, 0)
|
|
assert.Len(t, teamAppliedPoliceSpecs[0].LabelsExcludeAny, 1)
|
|
assert.Equal(t, teamAppliedPoliceSpecs[0].LabelsExcludeAny[0], "a")
|
|
assert.Len(t, teamAppliedPoliceSpecs[1].LabelsIncludeAny, 1)
|
|
assert.Len(t, teamAppliedPoliceSpecs[1].LabelsExcludeAny, 0)
|
|
assert.Equal(t, teamAppliedPoliceSpecs[1].LabelsIncludeAny[0], "b")
|
|
})
|
|
}
|
|
|
|
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", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
|
|
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
|
|
{"testdata/gitops/global_windows_custom_settings_unknown_label.yml", `Please create the missing labels, or update your settings to not refer to these labels.`},
|
|
{"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", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
|
|
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
|
|
{"testdata/gitops/team_macos_windows_custom_settings_unknown_label.yml", `Please create the missing labels, or update your settings to not refer to these labels.`},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
|
ds, appCfgPtr, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
(*appCfgPtr).MDM.EnabledAndConfigured = true
|
|
(*appCfgPtr).MDM.WindowsEnabledAndConfigured = true
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
|
|
return []*fleet.LabelSpec{
|
|
{
|
|
Name: "A",
|
|
Description: "A global label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
|
Hosts: []string{"host2", "host3"},
|
|
},
|
|
{
|
|
Name: "B",
|
|
Description: "Another label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
|
Query: "SELECT 1 from osquery_info",
|
|
},
|
|
{
|
|
Name: "C",
|
|
Description: "Another nother label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
|
Query: "SELECT 1 from osquery_info",
|
|
},
|
|
}, nil
|
|
}
|
|
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
|
|
}
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) 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 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) {
|
|
t.Logf("got: %s", out)
|
|
require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
|
|
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) {
|
|
require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
|
|
assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
|
|
assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
|
|
assert.NotContains(t, out, "[!] gitops 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 := testing_utils.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.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
|
|
return len(tt.tokens), 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
|
|
}
|
|
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) 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 TestGitOpsWindowsMigration(t *testing.T) {
|
|
cases := []struct {
|
|
file string
|
|
wantErr string
|
|
}{
|
|
// booleans are Windows MDM enabled and Windows migration enabled
|
|
{"testdata/gitops/global_config_windows_migration_true_true.yml", ""},
|
|
{"testdata/gitops/global_config_windows_migration_false_true.yml", "Windows MDM is not enabled"},
|
|
{"testdata/gitops/global_config_windows_migration_true_false.yml", ""},
|
|
{"testdata/gitops/global_config_windows_migration_false_false.yml", ""},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
|
testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
_, err := RunAppNoChecks([]string{"gitops", "-f", c.file})
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitOpsGlobalWebhooksDisable(t *testing.T) {
|
|
_, appConfig, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
webhook := &(*appConfig).WebhookSettings
|
|
webhook.ActivitiesWebhook.Enable = true
|
|
webhook.FailingPoliciesWebhook.Enable = true
|
|
webhook.HostStatusWebhook.Enable = true
|
|
webhook.VulnerabilitiesWebhook.Enable = true
|
|
|
|
// Run config with no webooks settings
|
|
_, err := RunAppNoChecks([]string{"gitops", "-f", "testdata/gitops/global_config_windows_migration_true_true.yml"})
|
|
require.NoError(t, err)
|
|
|
|
webhook = &(*appConfig).WebhookSettings
|
|
require.False(t, webhook.ActivitiesWebhook.Enable)
|
|
require.False(t, webhook.FailingPoliciesWebhook.Enable)
|
|
require.False(t, webhook.HostStatusWebhook.Enable)
|
|
require.False(t, webhook.VulnerabilitiesWebhook.Enable)
|
|
}
|
|
|
|
func TestGitOpsTeamWebhooks(t *testing.T) {
|
|
teamName := "TestTeamWebhooks"
|
|
|
|
ds, _, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
// Create a new team.
|
|
_, err := ds.NewTeam(context.Background(), &fleet.Team{Name: teamName, Config: fleet.TeamConfig{WebhookSettings: fleet.TeamWebhookSettings{
|
|
FailingPoliciesWebhook: fleet.FailingPoliciesWebhookSettings{Enable: true, DestinationURL: "http://saybye.by"},
|
|
HostStatusWebhook: &fleet.HostStatusWebhookSettings{Enable: true},
|
|
}}})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, *savedTeams[teamName])
|
|
|
|
// Do a GitOps run with no webhook settings.
|
|
t.Setenv("TEST_TEAM_NAME", teamName)
|
|
_, err = RunAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_config_webhook.yml"})
|
|
require.NoError(t, err)
|
|
|
|
team, err := ds.TeamByName(context.Background(), teamName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, team)
|
|
require.NotNil(t, team.Config.WebhookSettings)
|
|
|
|
// Check that the team's failing policy webhook settings are disabled and cleared, since the GitOps
|
|
// config doesn't include them.
|
|
require.False(t, team.Config.WebhookSettings.FailingPoliciesWebhook.Enable)
|
|
require.Equal(t, "", team.Config.WebhookSettings.FailingPoliciesWebhook.DestinationURL)
|
|
// Check that the team's host status webhook settings are enabled and set to the new values.
|
|
require.True(t, team.Config.WebhookSettings.HostStatusWebhook.Enable)
|
|
require.Equal(t, "http://coolwebhook.biz", team.Config.WebhookSettings.HostStatusWebhook.DestinationURL)
|
|
}
|
|
|
|
func TestGitOpsFeatures(t *testing.T) {
|
|
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
appConfig := fleet.AppConfig{
|
|
Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
AdditionalQueries: ptr.RawMessage(json.RawMessage(`{"query_a": "SELECT 1", "query_b": "SELECT 2"}`)),
|
|
DetailQueryOverrides: map[string]*string{
|
|
"detail_query_a": ptr.String("SELECT a"),
|
|
"detail_query_b": nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
globalFileUpdatedFeatures, err := os.CreateTemp(t.TempDir(), "*.yml")
|
|
require.NoError(t, err)
|
|
_, err = globalFileUpdatedFeatures.WriteString(fmt.Sprintf(
|
|
`
|
|
controls:
|
|
queries:
|
|
policies:
|
|
agent_options:
|
|
org_settings:
|
|
features:
|
|
enable_host_users: false
|
|
enable_software_inventory: false
|
|
additional_queries:
|
|
query_a: "SELECT 1"
|
|
detail_query_overrides:
|
|
detail_query_a: "SELECT it_works"
|
|
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)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &appConfig, nil
|
|
}
|
|
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
|
appConfig = *config
|
|
return nil
|
|
}
|
|
|
|
// Do a GitOps run with updated feature settings.
|
|
_, err = RunAppNoChecks([]string{"gitops", "-f", globalFileUpdatedFeatures.Name()})
|
|
require.NoError(t, err)
|
|
require.False(t, appConfig.Features.EnableHostUsers)
|
|
require.False(t, appConfig.Features.EnableSoftwareInventory)
|
|
|
|
// Parse the additional queries into a map.
|
|
var additionalQueries map[string]string
|
|
err = json.Unmarshal(*appConfig.Features.AdditionalQueries, &additionalQueries)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(additionalQueries))
|
|
require.Equal(t, "SELECT 1", additionalQueries["query_a"])
|
|
require.Equal(t, 1, len(appConfig.Features.DetailQueryOverrides))
|
|
require.Equal(t, "SELECT it_works", *appConfig.Features.DetailQueryOverrides["detail_query_a"])
|
|
|
|
// Do a GitOps run with no feature settings.
|
|
_, err = RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
|
|
require.NoError(t, err)
|
|
|
|
require.False(t, appConfig.Features.EnableHostUsers)
|
|
require.True(t, appConfig.Features.EnableSoftwareInventory)
|
|
require.Nil(t, appConfig.Features.AdditionalQueries)
|
|
require.Nil(t, appConfig.Features.DetailQueryOverrides)
|
|
}
|
|
|
|
func TestGitOpsSSOSettings(t *testing.T) {
|
|
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
appConfig := fleet.AppConfig{
|
|
SSOSettings: &fleet.SSOSettings{
|
|
SSOProviderSettings: fleet.SSOProviderSettings{
|
|
EntityID: "some-entity-id",
|
|
IssuerURI: "https://example.com/saml",
|
|
Metadata: "some-metadata",
|
|
IDPName: "some-idp-name",
|
|
},
|
|
IDPImageURL: "https://example.com/logo.png",
|
|
EnableSSO: true,
|
|
EnableSSOIdPLogin: true,
|
|
EnableJITProvisioning: true,
|
|
EnableJITRoleSync: true,
|
|
},
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &appConfig, nil
|
|
}
|
|
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
|
appConfig = *config
|
|
return nil
|
|
}
|
|
|
|
// Do a GitOps run with no sso settings.
|
|
_, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
|
|
require.NoError(t, err)
|
|
|
|
require.Nil(t, appConfig.SSOSettings)
|
|
}
|
|
|
|
func TestGitOpsSSOServerURL(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
globalFile, err := os.CreateTemp(tmpDir, "*.yml")
|
|
require.NoError(t, err)
|
|
_, err = globalFile.WriteString(`
|
|
controls:
|
|
queries:
|
|
policies:
|
|
agent_options:
|
|
org_settings:
|
|
server_settings:
|
|
server_url: ` + fleetServerURL + `
|
|
org_info:
|
|
org_name: ` + orgName + `
|
|
sso_settings:
|
|
entity_id: "test-entity"
|
|
idp_name: "Test IdP"
|
|
metadata: "<xml>test-metadata</xml>"
|
|
enable_sso: true
|
|
sso_server_url: "https://sso.example.com"
|
|
secrets:
|
|
- secret: test-secret
|
|
`)
|
|
require.NoError(t, err)
|
|
globalFile.Close()
|
|
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
appConfig := fleet.AppConfig{}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &appConfig, nil
|
|
}
|
|
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
|
appConfig = *config
|
|
return nil
|
|
}
|
|
|
|
// Run GitOps with SSO settings including sso_url
|
|
_, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name()})
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, appConfig.SSOSettings)
|
|
require.Equal(t, "https://sso.example.com", appConfig.SSOSettings.SSOServerURL)
|
|
require.Equal(t, "test-entity", appConfig.SSOSettings.EntityID)
|
|
require.True(t, appConfig.SSOSettings.EnableSSO)
|
|
}
|
|
|
|
func TestGitOpsSMTPSettings(t *testing.T) {
|
|
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
appConfig := fleet.AppConfig{
|
|
SMTPSettings: &fleet.SMTPSettings{
|
|
SMTPEnabled: true,
|
|
SMTPConfigured: true,
|
|
SMTPSenderAddress: "http://example.com",
|
|
SMTPServer: "server.example.com",
|
|
SMTPPort: 587,
|
|
SMTPAuthenticationType: "smoooth",
|
|
SMTPUserName: "uzer",
|
|
SMTPPassword: "pazzword",
|
|
SMTPEnableTLS: true,
|
|
SMTPAuthenticationMethod: "crunchy",
|
|
SMTPDomain: "smtp.example.com",
|
|
SMTPVerifySSLCerts: true,
|
|
SMTPEnableStartTLS: true,
|
|
},
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &appConfig, nil
|
|
}
|
|
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
|
appConfig = *config
|
|
return nil
|
|
}
|
|
|
|
// Do a GitOps run with no smtp settings.
|
|
_, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
|
|
require.NoError(t, err)
|
|
|
|
// Currently we do NOT clear the SMTP settings if they are not in the config,
|
|
// because the smtp_settings key is not documented in the GitOps config.
|
|
// TODO - update this test if we change this behavior.
|
|
require.Equal(t, &fleet.SMTPSettings{
|
|
SMTPEnabled: true,
|
|
SMTPConfigured: true,
|
|
SMTPSenderAddress: "http://example.com",
|
|
SMTPServer: "server.example.com",
|
|
SMTPPort: 587,
|
|
SMTPAuthenticationType: "smoooth",
|
|
SMTPUserName: "uzer",
|
|
SMTPPassword: "********",
|
|
SMTPEnableTLS: true,
|
|
SMTPAuthenticationMethod: "crunchy",
|
|
SMTPDomain: "smtp.example.com",
|
|
SMTPVerifySSLCerts: true,
|
|
SMTPEnableStartTLS: true,
|
|
}, appConfig.SMTPSettings)
|
|
}
|
|
|
|
func TestGitOpsMDMAuthSettings(t *testing.T) {
|
|
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
appConfig := fleet.AppConfig{
|
|
MDM: fleet.MDM{
|
|
EndUserAuthentication: fleet.MDMEndUserAuthentication{
|
|
SSOProviderSettings: fleet.SSOProviderSettings{
|
|
EntityID: "some-entity-id",
|
|
IssuerURI: "https://example.com/saml",
|
|
Metadata: "some-metadata",
|
|
IDPName: "some-idp-name",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &appConfig, nil
|
|
}
|
|
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
|
appConfig = *config
|
|
return nil
|
|
}
|
|
|
|
// Do a GitOps run with no mdm end user auth settings.
|
|
_, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, appConfig.MDM.EndUserAuthentication)
|
|
require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.EntityID)
|
|
require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.IssuerURI)
|
|
require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.Metadata)
|
|
require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.MetadataURL)
|
|
require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.IDPName)
|
|
}
|
|
|
|
func TestGitOpsTeamConditionalAccess(t *testing.T) {
|
|
teamName := "TestTeamConditionalAccess"
|
|
|
|
ds, _, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
|
|
return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
|
|
}
|
|
|
|
// Create integration with conditional access enabled.
|
|
_, err := ds.NewTeam(context.Background(), &fleet.Team{Name: teamName, Config: fleet.TeamConfig{
|
|
Integrations: fleet.TeamIntegrations{
|
|
ConditionalAccessEnabled: optjson.SetBool(true),
|
|
},
|
|
}})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, *savedTeams[teamName])
|
|
|
|
// Do a GitOps run with conditional access not set.
|
|
t.Setenv("TEST_TEAM_NAME", teamName)
|
|
_, err = RunAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_config_webhook.yml"})
|
|
require.NoError(t, err)
|
|
|
|
team, err := ds.TeamByName(context.Background(), teamName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, team)
|
|
require.True(t, team.Config.Integrations.ConditionalAccessEnabled.Set)
|
|
require.False(t, team.Config.Integrations.ConditionalAccessEnabled.Value)
|
|
}
|
|
|
|
func TestGitOpsNoTeamConditionalAccess(t *testing.T) {
|
|
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
|
|
return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
|
|
}
|
|
|
|
appConfig := fleet.AppConfig{
|
|
Integrations: fleet.Integrations{
|
|
ConditionalAccessEnabled: optjson.SetBool(true),
|
|
},
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &appConfig, nil
|
|
}
|
|
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
|
appConfig = *config
|
|
return nil
|
|
}
|
|
|
|
// Do a GitOps run with conditional access not set.
|
|
_, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
|
|
require.NoError(t, err)
|
|
require.True(t, appConfig.Integrations.ConditionalAccessEnabled.Set)
|
|
require.False(t, appConfig.Integrations.ConditionalAccessEnabled.Value)
|
|
}
|
|
|
|
func TestGitOpsEULASetting(t *testing.T) {
|
|
createGlobalGitOpsConfig := 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)
|
|
}
|
|
|
|
// Create a temporary PDF file
|
|
pdfContent := []byte("%PDF-1\npdf-test")
|
|
tmpPDF, err := os.CreateTemp(t.TempDir(), "*.pdf")
|
|
require.NoError(t, err)
|
|
// Write a minimal valid PDF header so the file is recognized as a PDF.
|
|
_, err = tmpPDF.Write(pdfContent)
|
|
require.NoError(t, err)
|
|
pdfPath, err := filepath.Abs(tmpPDF.Name())
|
|
require.NoError(t, err)
|
|
|
|
// Create an invalid temp PDF file
|
|
tmpInvalidPDF, err := os.CreateTemp(t.TempDir(), "*.txt")
|
|
require.NoError(t, err)
|
|
_, err = tmpInvalidPDF.Write([]byte("not-a-pdf"))
|
|
require.NoError(t, err)
|
|
invalidPDFPath, err := filepath.Abs(tmpInvalidPDF.Name())
|
|
require.NoError(t, err)
|
|
|
|
cases := []struct {
|
|
name string
|
|
cfg string
|
|
mockSetup func(t *testing.T, ds *mock.Store, gitopsDir string)
|
|
dryRunAssertion func(t *testing.T, ds *mock.Store, out string, err error)
|
|
realRunAssertion func(t *testing.T, ds *mock.Store, out string, err error)
|
|
}{
|
|
{
|
|
name: "valid pdf file (no existing EULA uploaded)",
|
|
cfg: createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, pdfPath)),
|
|
mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return nil, ¬FoundError{} // No existing EULA
|
|
}
|
|
},
|
|
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] would've applied EULA")
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] applied EULA")
|
|
assert.True(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
},
|
|
{
|
|
name: "relative path to working dir to pdf file (no existing EULA uploaded)",
|
|
cfg: createGlobalGitOpsConfig(`end_user_license_agreement: "./testdata/gitops/tiny_eula.pdf"`),
|
|
mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return nil, ¬FoundError{} // No existing EULA
|
|
}
|
|
},
|
|
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "error uploading EULA: reading eula file:")
|
|
assert.ErrorContains(t, err, "no such file or directory")
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "error uploading EULA: reading eula file:")
|
|
assert.ErrorContains(t, err, "no such file or directory")
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
},
|
|
{
|
|
name: "relative path to yaml file to pdf file (no existing EULA uploaded)",
|
|
cfg: createGlobalGitOpsConfig(`end_user_license_agreement: "./lib/eula.pdf"`),
|
|
mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
|
|
err := os.Mkdir(filepath.Join(dir, "lib"), 0o755)
|
|
require.NoError(t, err)
|
|
tmpPDF, err := os.Create(filepath.Join(dir, "lib", "eula.pdf"))
|
|
require.NoError(t, err)
|
|
_, err = tmpPDF.Write(pdfContent)
|
|
require.NoError(t, err)
|
|
err = tmpPDF.Close()
|
|
require.NoError(t, err)
|
|
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return nil, ¬FoundError{} // No existing EULA
|
|
}
|
|
},
|
|
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] would've applied EULA")
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] applied EULA")
|
|
assert.True(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
},
|
|
{
|
|
name: "valid new pdf file (different EULA already uploaded)",
|
|
cfg: createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, pdfPath)),
|
|
mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return &fleet.MDMEULA{
|
|
Name: pdfPath,
|
|
Token: "test-token",
|
|
}, nil
|
|
}
|
|
},
|
|
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] would've applied EULA")
|
|
assert.False(t, ds.MDMDeleteEULAFuncInvoked)
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] applied EULA")
|
|
assert.True(t, ds.MDMDeleteEULAFuncInvoked) // deleted old EULA
|
|
assert.True(t, ds.MDMInsertEULAFuncInvoked) // new EULA was updated
|
|
},
|
|
},
|
|
{
|
|
name: "no EULA specified (no existing EULA uploaded)",
|
|
cfg: createGlobalGitOpsConfig(""),
|
|
mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return nil, ¬FoundError{} // No existing EULA
|
|
}
|
|
},
|
|
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] would've applied EULA")
|
|
assert.False(t, ds.MDMDeleteEULAFuncInvoked)
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] applied EULA")
|
|
assert.False(t, ds.MDMDeleteEULAFuncInvoked) // no EULA to delete
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked) // no EULA to upload
|
|
},
|
|
},
|
|
{
|
|
name: "deleting existing EULA",
|
|
cfg: createGlobalGitOpsConfig(""),
|
|
mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return &fleet.MDMEULA{
|
|
Name: pdfPath,
|
|
Token: "test-token",
|
|
}, nil
|
|
}
|
|
},
|
|
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] would've applied EULA")
|
|
assert.False(t, ds.MDMDeleteEULAFuncInvoked)
|
|
},
|
|
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] applied EULA")
|
|
assert.True(t, ds.MDMDeleteEULAFuncInvoked) // deleted EULA
|
|
},
|
|
},
|
|
{
|
|
name: "not a PDF file",
|
|
cfg: createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, invalidPDFPath)),
|
|
mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return nil, ¬FoundError{} // No existing EULA
|
|
}
|
|
},
|
|
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.ErrorContains(t, err, "invalid file type")
|
|
},
|
|
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.ErrorContains(t, err, "invalid file type")
|
|
},
|
|
},
|
|
{
|
|
name: "uploading the same EULA again",
|
|
cfg: createGlobalGitOpsConfig(""),
|
|
mockSetup: func(t *testing.T, ds *mock.Store, dir string) {
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
hash := sha256.Sum256(pdfContent) // Simulate same EULA
|
|
return &fleet.MDMEULA{
|
|
Name: pdfPath,
|
|
Token: "test-token",
|
|
Sha256: hash[:], // Simulate same EULA
|
|
}, nil
|
|
}
|
|
},
|
|
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] would've applied EULA")
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
|
},
|
|
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, out, "[+] applied EULA")
|
|
assert.False(t, ds.MDMInsertEULAFuncInvoked) // No new EULA uploaded
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
// these mocks are used for all tests
|
|
ds.MDMInsertEULAFunc = func(ctx context.Context, eula *fleet.MDMEULA) error {
|
|
return nil
|
|
}
|
|
ds.MDMDeleteEULAFunc = func(ctx context.Context, token string) error {
|
|
return nil
|
|
}
|
|
|
|
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString(tt.cfg)
|
|
require.NoError(t, err)
|
|
|
|
// these mocks are defined in the individual test cases
|
|
tt.mockSetup(t, ds, filepath.Dir(tmpFile.Name()))
|
|
|
|
// Dry run
|
|
out, err := RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
|
|
tt.dryRunAssertion(t, ds, out.String(), err)
|
|
if t.Failed() {
|
|
t.FailNow()
|
|
}
|
|
|
|
// Real run
|
|
out, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
|
|
tt.realRunAssertion(t, ds, out.String(), err)
|
|
})
|
|
}
|
|
}
|