mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
**Related issue:** Resolves #36288 Refactored Android certificate CRUD to use ListOptions
978 lines
37 KiB
Go
978 lines
37 KiB
Go
package gitops
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl"
|
|
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
teamName = "Team Test"
|
|
)
|
|
|
|
func TestGitOpsTeamSoftwareInstallers(t *testing.T) {
|
|
testing_utils.StartSoftwareInstallerServer(t)
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
|
|
cases := []struct {
|
|
file string
|
|
wantErr string
|
|
}{
|
|
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
|
|
{"testdata/gitops/team_software_installer_install_script_secret.yml", "environment variable \"FLEET_SECRET_NAME\" not set"},
|
|
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
|
|
// commenting out, results in the process getting killed on CI and on some machines
|
|
// {"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 3 GB"},
|
|
{"testdata/gitops/team_software_installer_valid.yml", ""},
|
|
{"testdata/gitops/team_software_installer_subdir.yml", ""},
|
|
{"testdata/gitops/subdir/team_software_installer_valid.yml", ""},
|
|
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
|
|
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
|
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries_apply.yml", "should have only one query."},
|
|
{"testdata/gitops/team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/team_software_installer_install_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/team_software_installer_uninstall_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/team_software_installer_post_install_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/team_software_installer_no_url.yml", "at least one of hash_sha256 or url is required for each software package"},
|
|
{"testdata/gitops/team_software_installer_no_url_multi.yml", "multi_missing_url.yml, list item #1"},
|
|
{
|
|
"testdata/gitops/team_software_installer_invalid_self_service_value.yml",
|
|
"Couldn't edit \"../../fleetctl/testdata/gitops/team_software_installer_invalid_self_service_value.yml\" at \"software.packages.self_service\", expected type bool but got string",
|
|
},
|
|
{
|
|
"testdata/gitops/team_software_installer_invalid_both_include_exclude.yml",
|
|
`only one of "labels_exclude_any" or "labels_include_any" can be specified`,
|
|
},
|
|
{"testdata/gitops/team_software_installer_valid_include.yml", ""},
|
|
{"testdata/gitops/team_software_installer_valid_exclude.yml", ""},
|
|
{
|
|
"testdata/gitops/team_software_installer_invalid_unknown_label.yml",
|
|
"Please create the missing labels, or update your settings to not refer to these labels.",
|
|
},
|
|
// display_name tests
|
|
{"testdata/gitops/team_software_installer_with_display_name.yml", ""},
|
|
{"testdata/gitops/team_software_installer_display_name_too_long.yml", "display_name is too long (max 255 characters)"},
|
|
{"testdata/gitops/team_software_app_store_display_name_too_long.yml", "display_name is too long (max 255 characters)"},
|
|
// team tests for setup experience software/script
|
|
{"testdata/gitops/team_setup_software_valid.yml", ""},
|
|
{"testdata/gitops/team_setup_software_on_package.yml", ""},
|
|
{"testdata/gitops/team_setup_software_defined_in_conflicting_places.yml", " Setup experience may only be specified directly on software or within macos_setup, but not both."},
|
|
{"testdata/gitops/team_setup_software_defined_in_conflicting_places_vpp.yml", " Setup experience may only be specified directly on software or within macos_setup, but not both."},
|
|
{"testdata/gitops/team_setup_software_invalid_script.yml", "no_such_script.sh: no such file"},
|
|
{"testdata/gitops/team_setup_software_invalid_software_package.yml", "no_such_software.yml\" does not exist for that team"},
|
|
{"testdata/gitops/team_setup_software_invalid_vpp_app.yml", "\"no_such_app\" does not exist for that team"},
|
|
{"testdata/gitops/team_software_installer_valid_ipa.yml", ""},
|
|
{"testdata/gitops/team_software_installer_subdir_ipa.yml", ""},
|
|
}
|
|
for _, c := range cases {
|
|
c.file = filepath.Join("../../fleetctl", c.file)
|
|
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
tokExpire := time.Now().Add(time.Hour)
|
|
token, err := test.CreateVPPTokenEncoded(tokExpire, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: tokExpire,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}, nil
|
|
}
|
|
|
|
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",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
labelToIDs := map[string]uint{
|
|
fleet.BuiltinLabelMacOS14Plus: 1,
|
|
"a": 2,
|
|
"b": 3,
|
|
}
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
|
// for this test, recognize labels a and b (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.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.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", c.file})
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {
|
|
testing_utils.StartSoftwareInstallerServer(t)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
t.Setenv("QUERY_VAR", "IT_WORKS")
|
|
|
|
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
|
if len(installers) != 0 && installers[0].PreInstallQuery != "select IT_WORKS" {
|
|
return fmt.Errorf("Missing env var, got %s", installers[0].PreInstallQuery)
|
|
}
|
|
return nil
|
|
}
|
|
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
|
return nil
|
|
}
|
|
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
|
return nil, 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.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
_, err := fleetctl.RunAppNoChecks([]string{"gitops", "-f", "../../fleetctl/testdata/gitops/team_software_installer_valid_env_query.yml"})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestGitOpsNoTeamVPPPolicies(t *testing.T) {
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
|
|
cases := []struct {
|
|
noTeamFile string
|
|
wantErr string
|
|
vppApps []fleet.VPPAppResponse
|
|
}{
|
|
{
|
|
noTeamFile: "testdata/gitops/subdir/no_team_vpp_policies_valid.yml",
|
|
vppApps: []fleet.VPPAppResponse{
|
|
{ // for more test coverage
|
|
Platform: fleet.MacOSPlatform,
|
|
},
|
|
{ // for more test coverage
|
|
TitleID: ptr.Uint(122),
|
|
Platform: fleet.MacOSPlatform,
|
|
},
|
|
{
|
|
TeamID: ptr.Uint(0),
|
|
TitleID: ptr.Uint(123),
|
|
AppStoreID: "1",
|
|
Platform: fleet.IOSPlatform,
|
|
},
|
|
{
|
|
TeamID: ptr.Uint(0),
|
|
TitleID: ptr.Uint(124),
|
|
AppStoreID: "1",
|
|
Platform: fleet.MacOSPlatform,
|
|
},
|
|
{
|
|
TeamID: ptr.Uint(0),
|
|
TitleID: ptr.Uint(125),
|
|
AppStoreID: "1",
|
|
Platform: fleet.IPadOSPlatform,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
c.noTeamFile = filepath.Join("../../fleetctl", c.noTeamFile)
|
|
t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
tokExpire := time.Now().Add(time.Hour)
|
|
token, err := test.CreateVPPTokenEncoded(tokExpire, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
|
|
return nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return c.vppApps, nil
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: tokExpire,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}, nil
|
|
}
|
|
labelToIDs := map[string]uint{
|
|
fleet.BuiltinLabelMacOS14Plus: 1,
|
|
"a": 2,
|
|
"b": 3,
|
|
}
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
|
// for this test, recognize labels a and b (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.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
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
|
|
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
|
|
globalFile := "../../fleetctl/testdata/gitops/global_config_no_paths.yml"
|
|
dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml")
|
|
t.Cleanup(func() {
|
|
os.Remove(dstPath)
|
|
})
|
|
err = file.Copy(c.noTeamFile, dstPath, 0o755)
|
|
require.NoError(t, err)
|
|
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath})
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
|
|
testing_utils.StartSoftwareInstallerServer(t)
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
|
|
cases := []struct {
|
|
noTeamFile string
|
|
wantErr string
|
|
}{
|
|
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
|
|
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
|
|
// commenting out, results in the process getting killed on CI and on some machines
|
|
// {"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 3 GB"},
|
|
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
|
|
{"testdata/gitops/no_team_software_installer_subdir.yml", ""},
|
|
{"testdata/gitops/subdir/no_team_software_installer_valid.yml", ""},
|
|
{"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
|
{"testdata/gitops/no_team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/no_team_software_installer_install_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/no_team_software_installer_uninstall_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/no_team_software_installer_post_install_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/no_team_software_installer_no_url.yml", "at least one of hash_sha256 or url is required for each software package"},
|
|
{
|
|
"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml",
|
|
"Couldn't edit \"../../fleetctl/testdata/gitops/no-team.yml\" at \"software.packages.self_service\", expected type bool but got string",
|
|
},
|
|
{
|
|
"testdata/gitops/no_team_software_installer_invalid_both_include_exclude.yml",
|
|
`only one of "labels_exclude_any" or "labels_include_any" can be specified`,
|
|
},
|
|
{"testdata/gitops/no_team_software_installer_valid_include.yml", ""},
|
|
{"testdata/gitops/no_team_software_installer_valid_exclude.yml", ""},
|
|
{
|
|
"testdata/gitops/no_team_software_installer_invalid_unknown_label.yml",
|
|
"Please create the missing labels, or update your settings to not refer to these labels.",
|
|
},
|
|
// No team tests for setup experience software/script
|
|
{"testdata/gitops/no_team_setup_software_valid.yml", ""},
|
|
{"testdata/gitops/no_team_setup_software_invalid_script.yml", "no_such_script.sh: no such file"},
|
|
{"testdata/gitops/no_team_setup_software_invalid_software_package.yml", "no_such_software.yml\" does not exist for that team"},
|
|
{"testdata/gitops/no_team_setup_software_invalid_vpp_app.yml", "\"no_such_app\" does not exist for that team"},
|
|
{"testdata/gitops/no_team_software_installer_valid_ipa.yml", ""},
|
|
{"testdata/gitops/no_team_software_installer_subdir_ipa.yml", ""},
|
|
}
|
|
for _, c := range cases {
|
|
c.noTeamFile = filepath.Join("../../fleetctl", c.noTeamFile)
|
|
t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
tokExpire := time.Now().Add(time.Hour)
|
|
token, err := test.CreateVPPTokenEncoded(tokExpire, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
|
|
return nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: tokExpire,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}, nil
|
|
}
|
|
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",
|
|
},
|
|
}, nil
|
|
}
|
|
labelToIDs := map[string]uint{
|
|
fleet.BuiltinLabelMacOS14Plus: 1,
|
|
"a": 2,
|
|
"b": 3,
|
|
}
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
|
// for this test, recognize labels a and b (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.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
|
|
}
|
|
|
|
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
|
|
globalFile := "../../fleetctl/testdata/gitops/global_config_no_paths.yml"
|
|
if strings.HasPrefix(filepath.Base(c.noTeamFile), "no_team_setup_software") {
|
|
// the controls section is in the no-team test file, so use a global file without that section
|
|
globalFile = "../../fleetctl/testdata/gitops/global_config_no_paths_no_controls.yml"
|
|
}
|
|
dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml")
|
|
t.Cleanup(func() {
|
|
os.Remove(dstPath)
|
|
})
|
|
err = file.Copy(c.noTeamFile, dstPath, 0o755)
|
|
require.NoError(t, err)
|
|
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath})
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitOpsTeamVPPApps(t *testing.T) {
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
|
|
cases := []struct {
|
|
file string
|
|
wantErr string
|
|
tokenExpiration time.Time
|
|
expectedLabels map[string]uint
|
|
}{
|
|
{"testdata/gitops/team_vpp_valid_app.yml", "", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_valid_app_self_service.yml", "", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(-24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_valid_app.yml", "VPP token expired", time.Now().Add(-24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_invalid_app.yml", "app not available on vpp account", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{
|
|
"testdata/gitops/team_vpp_incorrect_type.yml", "Couldn't edit \"../../fleetctl/testdata/gitops/team_vpp_incorrect_type.yml\" at \"software.app_store_apps.app_store_id\", expected type string but got number",
|
|
time.Now().Add(24 * time.Hour),
|
|
map[string]uint{},
|
|
},
|
|
{"testdata/gitops/team_vpp_empty_adamid.yml", "software app store id required", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{
|
|
"testdata/gitops/team_vpp_valid_app_labels_exclude_any.yml", "", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_valid_app_labels_include_any.yml", "", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_invalid_app_labels_exclude_any.yml",
|
|
"Please create the missing labels, or update your settings to not refer to these labels.", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_invalid_app_labels_include_any.yml",
|
|
"Please create the missing labels, or update your settings to not refer to these labels.", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_invalid_app_labels_both.yml",
|
|
`only one of "labels_exclude_any" or "labels_include_any" can be specified for app store app`, time.Now().Add(24 * time.Hour),
|
|
map[string]uint{},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
c.file = filepath.Join("../../fleetctl", c.file)
|
|
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
token, err := test.CreateVPPTokenEncoded(c.tokenExpiration, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
|
|
return nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: c.tokenExpiration,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}, nil
|
|
}
|
|
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context) ([]*fleet.LabelSpec, error) {
|
|
return []*fleet.LabelSpec{
|
|
{
|
|
Name: "label 1",
|
|
Description: "A global label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
|
Hosts: []string{"host2", "host3"},
|
|
},
|
|
{
|
|
Name: "label 2",
|
|
Description: "Another label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
|
Query: "SELECT 1 from osquery_info",
|
|
},
|
|
}, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
|
|
found := make(map[string]uint)
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
|
for _, l := range labels {
|
|
if id, ok := c.expectedLabels[l]; ok {
|
|
found[l] = id
|
|
}
|
|
}
|
|
return found, nil
|
|
}
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", c.file})
|
|
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
if len(c.expectedLabels) > 0 {
|
|
require.True(t, ds.LabelIDsByNameFuncInvoked)
|
|
}
|
|
|
|
require.Equal(t, c.expectedLabels, found)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGitOpsTeamVPPAndApp tests the flow where a new team is created with VPP apps.
|
|
// GitOps must first create the team, then assign VPP token to it, and only then add VPP apps.
|
|
func TestGitOpsTeamVPPAndApp(t *testing.T) {
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
renewDate := time.Now().Add(24 * time.Hour)
|
|
token, err := test.CreateVPPTokenEncoded(renewDate, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// The following mocks are key to this test.
|
|
vppToken := &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: renewDate,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}
|
|
tokensByTeams := make(map[uint]*fleet.VPPTokenDB)
|
|
ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
|
|
for _, teamID := range teams {
|
|
tokensByTeams[teamID] = vppToken
|
|
}
|
|
return vppToken, nil
|
|
}
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{vppToken}, nil
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
if teamID == nil {
|
|
return vppToken, nil
|
|
}
|
|
token, ok := tokensByTeams[*teamID]
|
|
if !ok {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
return token, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
|
|
buf, err := fleetctl.RunAppNoChecks([]string{
|
|
"gitops", "-f", "../../fleetctl/testdata/gitops/global_config_vpp.yml", "-f",
|
|
"../../fleetctl/testdata/gitops/team_vpp_valid_app.yml",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, ds.UpdateVPPTokenTeamsFuncInvoked)
|
|
assert.True(t, ds.GetVPPTokenByTeamIDFuncInvoked)
|
|
assert.True(t, ds.SetTeamVPPAppsFuncInvoked)
|
|
assert.Contains(t, buf.String(), fmt.Sprintf(fleetctl.ReapplyingTeamForVPPAppsMsg, teamName))
|
|
}
|
|
|
|
func TestGitOpsVPP(t *testing.T) {
|
|
global := func(mdm string) string {
|
|
return fmt.Sprintf(`
|
|
controls:
|
|
queries:
|
|
policies:
|
|
agent_options:
|
|
software:
|
|
org_settings:
|
|
server_settings:
|
|
server_url: "https://foo.example.com"
|
|
org_info:
|
|
org_name: GitOps Test
|
|
secrets:
|
|
- secret: "global"
|
|
mdm:
|
|
%s
|
|
`, mdm)
|
|
}
|
|
|
|
team := func(name string) string {
|
|
return fmt.Sprintf(`
|
|
name: %s
|
|
team_settings:
|
|
secrets:
|
|
- secret: "%s-secret"
|
|
agent_options:
|
|
controls:
|
|
policies:
|
|
queries:
|
|
software:
|
|
`, name, name)
|
|
}
|
|
|
|
workstations := team("💻 Workstations")
|
|
iosTeam := team("📱🏢 Company-owned iPhones")
|
|
ipadTeam := team("🔳🏢 Company-owned iPads")
|
|
|
|
cases := []struct {
|
|
name string
|
|
cfgs []string
|
|
dryRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
|
|
realRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
|
|
}{
|
|
{
|
|
name: "new key all valid",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "💻 Workstations"
|
|
- "📱🏢 Company-owned iPhones"
|
|
- "🔳🏢 Company-owned iPads"`),
|
|
workstations,
|
|
iosTeam,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: []string{
|
|
"💻 Workstations",
|
|
"📱🏢 Company-owned iPhones",
|
|
"🔳🏢 Company-owned iPads",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "new key multiple elements",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Acme Inc.
|
|
teams:
|
|
- "💻 Workstations"
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "📱🏢 Company-owned iPhones"
|
|
- "🔳🏢 Company-owned iPads"`),
|
|
workstations,
|
|
iosTeam,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Acme Inc.",
|
|
Teams: []string{
|
|
"💻 Workstations",
|
|
},
|
|
},
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: []string{
|
|
"📱🏢 Company-owned iPhones",
|
|
"🔳🏢 Company-owned iPads",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "using an undefined team errors",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "💻 Workstations"
|
|
- "📱🏢 Company-owned iPhones"
|
|
- "🔳🏢 Company-owned iPads"`),
|
|
workstations,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.ErrorContains(t, err, "volume_purchasing_program team 📱🏢 Company-owned iPhones not found in team configs")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.ErrorContains(t, err, "volume_purchasing_program team 📱🏢 Company-owned iPhones not found in team configs")
|
|
},
|
|
},
|
|
{
|
|
name: "no team is supported",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "💻 Workstations"
|
|
- "📱🏢 Company-owned iPhones"
|
|
- "No team"`),
|
|
workstations,
|
|
iosTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: []string{
|
|
"💻 Workstations",
|
|
"📱🏢 Company-owned iPhones",
|
|
"No team",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "all teams is supported",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "All teams"`),
|
|
workstations,
|
|
iosTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: []string{
|
|
"All teams",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "not provided teams defaults to no team",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:`),
|
|
workstations,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: nil,
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "non existent location fails",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Does not exist
|
|
teams:`),
|
|
workstations,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.ErrorContains(t, err, "token with location Does not exist doesn't exist")
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.NotContains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.ErrorContains(t, err, "token with location Does not exist doesn't exist")
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.NotContains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ds, savedAppConfigPtr, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{{Location: "Fleet Device Management Inc."}, {Location: "Acme Inc."}}, nil
|
|
}
|
|
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
|
|
}
|
|
ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
|
|
return 1, 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.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
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 := fleetctl.RunAppNoChecks(append(args, "--dry-run"))
|
|
tt.dryRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
|
|
if t.Failed() {
|
|
t.FailNow()
|
|
}
|
|
|
|
// Real run
|
|
out, err = fleetctl.RunAppNoChecks(args)
|
|
tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
|
|
|
|
// Second real run, now that all the teams are saved
|
|
out, err = fleetctl.RunAppNoChecks(args)
|
|
tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
|
|
})
|
|
}
|
|
}
|