Add VPP install automation in GitOps (#25400)

For #23531.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated automated tests
- [x] A detailed QA plan exists on the associated ticket (if it isn't
there, work with the product group's QA engineer to add it)
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Ian Littman 2025-01-14 12:52:39 -06:00 committed by GitHub
parent d8897e0cca
commit 4f0a2e2af9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 760 additions and 205 deletions

View file

@ -92,9 +92,10 @@ func applyCommand() *cli.Command {
baseDir := filepath.Dir(flFilename)
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
teamsVPPApps := make(map[string][]fleet.VPPAppResponse)
teamsScripts := make(map[string][]fleet.ScriptResponse)
_, _, _, err = fleetClient.ApplyGroup(c.Context, false, specs, baseDir, logf, nil, opts, teamsSoftwareInstallers, teamsScripts)
_, _, _, _, err = fleetClient.ApplyGroup(c.Context, false, specs, baseDir, logf, nil, opts, teamsSoftwareInstallers, teamsVPPApps, teamsScripts)
if err != nil {
return err
}

View file

@ -117,6 +117,7 @@ func gitopsCommand() *cli.Command {
// we keep track of team software installers and scripts for correct policy application
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
teamsVPPApps := make(map[string][]fleet.VPPAppResponse)
teamsScripts := make(map[string][]fleet.ScriptResponse)
// We keep track of the secrets to check if duplicates exist during dry run
@ -227,7 +228,7 @@ func gitopsCommand() *cli.Command {
if err != nil {
return err
}
assumptions, err := fleetClient.DoGitOps(c.Context, config, flFilename, logf, flDryRun, teamDryRunAssumptions, appConfig, teamsSoftwareInstallers, teamsScripts)
assumptions, err := fleetClient.DoGitOps(c.Context, config, flFilename, logf, flDryRun, teamDryRunAssumptions, appConfig, teamsSoftwareInstallers, teamsVPPApps, teamsScripts)
if err != nil {
return err
}

View file

@ -1105,6 +1105,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
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"
@ -1930,6 +1933,9 @@ func TestGitOpsTeamSofwareInstallers(t *testing.T) {
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
}
@ -1990,6 +1996,106 @@ func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {
require.NoError(t, err)
}
func TestGitOpsNoTeamVPPPolicies(t *testing.T) {
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 {
t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) {
ds, _, _ := 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
}
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
globalFile := "./testdata/gitops/global_config_no_paths.yml"
dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml")
t.Cleanup(func() {
os.Remove(dstPath)
})
err = file.Copy(c.noTeamFile, dstPath, 0o755)
require.NoError(t, err)
_, err = runAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath})
if c.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, c.wantErr)
}
})
}
}
func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
startSoftwareInstallerServer(t)
startAndServeVPPServer(t)
@ -2035,6 +2141,9 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
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,
@ -2114,6 +2223,9 @@ func TestGitOpsTeamVPPApps(t *testing.T) {
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{

View file

@ -397,7 +397,8 @@ Use the stop and reset subcommands to manage the server and dependencies once st
// so pass in the current working directory.
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
teamsScripts := make(map[string][]fleet.ScriptResponse)
_, _, _, err = client.ApplyGroup(c.Context, false, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{}, teamsSoftwareInstallers, teamsScripts)
teamsVPPApps := make(map[string][]fleet.VPPAppResponse)
_, _, _, _, err = client.ApplyGroup(c.Context, false, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{}, teamsSoftwareInstallers, teamsVPPApps, teamsScripts)
if err != nil {
return err
}

View file

@ -0,0 +1,11 @@
name: No team
controls:
policies:
- name: Slack on macOS is installed
platform: darwin
query: SELECT 1 FROM apps WHERE name = 'Slack.app';
install_software:
app_store_id: "1"
software:
app_store_apps:
- app_store_id: "1"

View file

@ -39,9 +39,9 @@ func (svc *Service) getVPPToken(ctx context.Context, teamID *uint) (string, erro
return token.Token, nil
}
func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, payloads []fleet.VPPBatchPayload, dryRun bool) error {
func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, payloads []fleet.VPPBatchPayload, dryRun bool) ([]fleet.VPPAppResponse, error) {
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
return err
return nil, err
}
var teamID *uint
@ -50,15 +50,15 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
if err != nil {
// If this is a dry run, the team may not have been created yet
if dryRun && fleet.IsNotFound(err) {
return nil
return nil, nil
}
return err
return nil, err
}
teamID = &tm.ID
}
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil {
return ctxerr.Wrap(ctx, err, "validating authorization")
return nil, ctxerr.Wrap(ctx, err, "validating authorization")
}
// Adding VPP apps will add them to all available platforms per decision:
@ -88,7 +88,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
if dryRun {
// On dry runs return early because the VPP token might not exist yet
// and we don't want to apply the VPP apps.
return nil
return nil, nil
}
var vppAppTeams []fleet.VPPAppTeam
@ -96,7 +96,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
if len(payloads) > 0 {
token, err := svc.getVPPToken(ctx, teamID)
if err != nil {
return fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "could not retrieve vpp token"), http.StatusUnprocessableEntity)
return nil, fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "could not retrieve vpp token"), http.StatusUnprocessableEntity)
}
for _, payload := range payloadsWithPlatform {
@ -104,7 +104,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
payload.Platform = fleet.MacOSPlatform
}
if payload.Platform != fleet.IOSPlatform && payload.Platform != fleet.IPadOSPlatform && payload.Platform != fleet.MacOSPlatform {
return fleet.NewInvalidArgumentError("app_store_apps.platform",
return nil, fleet.NewInvalidArgumentError("app_store_apps.platform",
fmt.Sprintf("platform must be one of '%s', '%s', or '%s", fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform))
}
vppAppTeams = append(vppAppTeams, fleet.VPPAppTeam{
@ -121,7 +121,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
assets, err := vpp.GetAssets(token, nil)
if err != nil {
return ctxerr.Wrap(ctx, err, "unable to retrieve assets")
return nil, ctxerr.Wrap(ctx, err, "unable to retrieve assets")
}
assetMap := map[string]struct{}{}
@ -137,22 +137,22 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
if len(missingAssets) != 0 {
reqErr := ctxerr.Errorf(ctx, "requested app not available on vpp account: %s", strings.Join(missingAssets, ","))
return fleet.NewUserMessageError(reqErr, http.StatusUnprocessableEntity)
return nil, fleet.NewUserMessageError(reqErr, http.StatusUnprocessableEntity)
}
}
if len(vppAppTeams) > 0 {
apps, err := getVPPAppsMetadata(ctx, vppAppTeams)
if err != nil {
return ctxerr.Wrap(ctx, err, "refreshing VPP app metadata")
return nil, ctxerr.Wrap(ctx, err, "refreshing VPP app metadata")
}
if len(apps) == 0 {
return fleet.NewInvalidArgumentError("app_store_apps",
return nil, fleet.NewInvalidArgumentError("app_store_apps",
"no valid apps found matching the provided app store IDs and platforms")
}
if err := svc.ds.BatchInsertVPPApps(ctx, apps); err != nil {
return ctxerr.Wrap(ctx, err, "inserting vpp app metadata")
return nil, ctxerr.Wrap(ctx, err, "inserting vpp app metadata")
}
// Filter out the apps with invalid platforms
if len(apps) != len(vppAppTeams) {
@ -165,12 +165,16 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
}
if err := svc.ds.SetTeamVPPApps(ctx, teamID, vppAppTeams); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "no vpp token to set team vpp assets"), http.StatusUnprocessableEntity)
return nil, fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "no vpp token to set team vpp assets"), http.StatusUnprocessableEntity)
}
return ctxerr.Wrap(ctx, err, "set team vpp assets")
return nil, ctxerr.Wrap(ctx, err, "set team vpp assets")
}
return nil
if len(vppAppTeams) == 0 {
return []fleet.VPPAppResponse{}, nil
}
return svc.ds.GetVPPApps(ctx, teamID)
}
func (svc *Service) GetAppStoreApps(ctx context.Context, teamID *uint) ([]*fleet.VPPApp, error) {

View file

@ -73,6 +73,7 @@ type PolicyRunScript struct {
type PolicyInstallSoftware struct {
PackagePath string `json:"package_path"`
AppStoreID string `json:"app_store_id"`
}
type Query struct {
@ -564,7 +565,7 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
for _, item := range policies {
item := item
if item.Path == nil {
if err := parsePolicyInstallSoftware(baseDir, result.TeamName, &item, result.Software.Packages); err != nil {
if err := parsePolicyInstallSoftware(baseDir, result.TeamName, &item, result.Software.Packages, result.Software.AppStoreApps); err != nil {
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy install_software %q: %v", item.Name, err))
continue
}
@ -600,7 +601,7 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
multiError, fmt.Errorf("nested paths are not supported: %s in %s", *pp.Path, *item.Path),
)
} else {
if err := parsePolicyInstallSoftware(filepath.Dir(filePath), result.TeamName, pp, result.Software.Packages); err != nil {
if err := parsePolicyInstallSoftware(filepath.Dir(filePath), result.TeamName, pp, result.Software.Packages, result.Software.AppStoreApps); err != nil {
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy install_software %q: %v", pp.Name, err))
continue
}
@ -684,36 +685,56 @@ func parsePolicyRunScript(baseDir string, teamName *string, policy *Policy, scri
return nil
}
func parsePolicyInstallSoftware(baseDir string, teamName *string, policy *Policy, packages []*fleet.SoftwarePackageSpec) error {
func parsePolicyInstallSoftware(baseDir string, teamName *string, policy *Policy, packages []*fleet.SoftwarePackageSpec, appStoreApps []*fleet.TeamSpecAppStoreApp) error {
if policy.InstallSoftware == nil {
policy.SoftwareTitleID = ptr.Uint(0) // unset the installer
return nil
}
if policy.InstallSoftware != nil && policy.InstallSoftware.PackagePath != "" && teamName == nil {
if policy.InstallSoftware != nil && (policy.InstallSoftware.PackagePath != "" || policy.InstallSoftware.AppStoreID != "") && teamName == nil {
return errors.New("install_software can only be set on team policies")
}
if policy.InstallSoftware.PackagePath == "" {
return errors.New("empty package_path")
if policy.InstallSoftware.PackagePath == "" && policy.InstallSoftware.AppStoreID == "" {
return errors.New("install_software must include either a package path or app store app ID")
}
fileBytes, err := os.ReadFile(resolveApplyRelativePath(baseDir, policy.InstallSoftware.PackagePath))
if err != nil {
return fmt.Errorf("failed to read install_software.package_path file %q: %v", policy.InstallSoftware.PackagePath, err)
if policy.InstallSoftware.PackagePath != "" && policy.InstallSoftware.AppStoreID != "" {
return errors.New("install_software must have only one of package_path or app_store_id")
}
var policyInstallSoftwareSpec fleet.SoftwarePackageSpec
if err := yaml.Unmarshal(fileBytes, &policyInstallSoftwareSpec); err != nil {
return fmt.Errorf("failed to unmarshal install_software.package_path file %s: %v", policy.InstallSoftware.PackagePath, err)
if policy.InstallSoftware.PackagePath != "" {
fileBytes, err := os.ReadFile(resolveApplyRelativePath(baseDir, policy.InstallSoftware.PackagePath))
if err != nil {
return fmt.Errorf("failed to read install_software.package_path file %q: %v", policy.InstallSoftware.PackagePath, err)
}
var policyInstallSoftwareSpec fleet.SoftwarePackageSpec
if err := yaml.Unmarshal(fileBytes, &policyInstallSoftwareSpec); err != nil {
return fmt.Errorf("failed to unmarshal install_software.package_path file %s: %v", policy.InstallSoftware.PackagePath, err)
}
installerOnTeamFound := false
for _, pkg := range packages {
if pkg.URL == policyInstallSoftwareSpec.URL {
installerOnTeamFound = true
break
}
}
if !installerOnTeamFound {
return fmt.Errorf("install_software.package_path URL %s not found on team: %s", policyInstallSoftwareSpec.URL, policy.InstallSoftware.PackagePath)
}
policy.InstallSoftwareURL = policyInstallSoftwareSpec.URL
}
installerOnTeamFound := false
for _, pkg := range packages {
if pkg.URL == policyInstallSoftwareSpec.URL {
installerOnTeamFound = true
break
if policy.InstallSoftware.AppStoreID != "" {
appOnTeamFound := false
for _, app := range appStoreApps {
if app.AppStoreID == policy.InstallSoftware.AppStoreID {
appOnTeamFound = true
break
}
}
if !appOnTeamFound {
return fmt.Errorf("install_software.app_store_id %s not found on team %s", policy.InstallSoftware.AppStoreID, *teamName)
}
}
if !installerOnTeamFound {
return fmt.Errorf("install_software.package_path URL %s not found on team: %s", policyInstallSoftwareSpec.URL, policy.InstallSoftware.PackagePath)
}
policy.InstallSoftwareURL = policyInstallSoftwareSpec.URL
return nil
}

View file

@ -264,12 +264,16 @@ func TestValidGitOpsYaml(t *testing.T) {
require.False(t, gitops.Software.Packages[0].SelfService)
require.Equal(t, "https://ftp.mozilla.org/pub/firefox/releases/129.0.2/mac/en-US/Firefox%20129.0.2.pkg", gitops.Software.Packages[1].URL)
require.True(t, gitops.Software.Packages[1].SelfService)
require.Len(t, gitops.Software.AppStoreApps, 1)
require.Equal(t, gitops.Software.AppStoreApps[0].AppStoreID, "123456")
require.False(t, gitops.Software.AppStoreApps[0].SelfService)
}
// Check policies
expectedPoliciesCount := 5
if test.isTeam {
expectedPoliciesCount = 8
expectedPoliciesCount = 9
}
require.Len(t, gitops.Policies, expectedPoliciesCount)
assert.Equal(t, "😊 Failing policy", gitops.Policies[0].Name)
@ -283,14 +287,18 @@ func TestValidGitOpsYaml(t *testing.T) {
assert.NotNil(t, gitops.Policies[5].InstallSoftware)
assert.Equal(t, "./microsoft-teams.pkg.software.yml", gitops.Policies[5].InstallSoftware.PackagePath)
assert.Equal(t, "Script run policy", gitops.Policies[6].Name)
assert.NotNil(t, gitops.Policies[6].RunScript)
assert.Equal(t, "./lib/collect-fleetd-logs.sh", gitops.Policies[6].RunScript.Path)
assert.Equal(t, "Slack on macOS is installed", gitops.Policies[6].Name)
assert.NotNil(t, gitops.Policies[6].InstallSoftware)
assert.Equal(t, "123456", gitops.Policies[6].InstallSoftware.AppStoreID)
assert.Equal(t, "🔥 Failing policy with script", gitops.Policies[7].Name)
assert.Equal(t, "Script run policy", gitops.Policies[7].Name)
assert.NotNil(t, gitops.Policies[7].RunScript)
assert.Equal(t, "./lib/collect-fleetd-logs.sh", gitops.Policies[7].RunScript.Path)
assert.Equal(t, "🔥 Failing policy with script", gitops.Policies[8].Name)
assert.NotNil(t, gitops.Policies[8].RunScript)
// . or .. depending on whether with paths or without
assert.Contains(t, gitops.Policies[7].RunScript.Path, "./lib/collect-fleetd-logs.sh")
assert.Contains(t, gitops.Policies[8].RunScript.Path, "./lib/collect-fleetd-logs.sh")
}
},
)
@ -911,7 +919,19 @@ policies:
package_path:
`
_, err = gitOpsFromString(t, config)
assert.ErrorContains(t, err, "empty package_path")
assert.ErrorContains(t, err, "must include either a package path or app store app ID")
config = getTeamConfig([]string{"policies"})
config += `
policies:
- name: Some policy
query: SELECT 1;
install_software:
package_path: ./some_path.yml
app_store_id: "123456"
`
_, err = gitOpsFromString(t, config)
assert.ErrorContains(t, err, "must have only one of package_path or app_store_id")
// Software has a URL that's too big
tooBigURL := fmt.Sprintf("https://ftp.mozilla.org/%s", strings.Repeat("a", 4000-23))
@ -929,6 +949,18 @@ software:
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
assert.ErrorContains(t, err, fmt.Sprintf("software URL \"%s\" is too long, must be 4000 characters or less", tooBigURL))
// Policy references a VPP app not present on the team
config = getTeamConfig([]string{"policies"})
config += `
policies:
- name: Some policy
query: SELECT 1;
install_software:
app_store_id: "123456"
`
_, err = gitOpsFromString(t, config)
assert.ErrorContains(t, err, "not found on team")
// Policy references a software installer not present in the team.
config = getTeamConfig([]string{"policies"})
config += `

View file

@ -26,6 +26,11 @@ policies:
resolution: There is no resolution for this policy.
query: SELECT 1 FROM osquery_info WHERE start_time < 0;
- path: ./team_install_software.policies.yml
- name: Slack on macOS is installed
platform: darwin
query: SELECT 1 FROM apps WHERE name = 'Slack.app';
install_software:
app_store_id: "123456"
- name: Script run policy
platform: linux
description: This should run a script on failure
@ -34,6 +39,8 @@ policies:
path: ./lib/collect-fleetd-logs.sh
- path: ./policies/script-policy.yml
software:
app_store_apps:
- app_store_id: "123456"
packages:
- path: ./microsoft-teams.pkg.software.yml
- url: https://ftp.mozilla.org/pub/firefox/releases/129.0.2/mac/en-US/Firefox%20129.0.2.pkg

View file

@ -121,6 +121,11 @@ policies:
query: SELECT 1 FROM apps WHERE name = 'Microsoft Teams.app' AND version_compare(bundle_short_version, '24193.1707.3028.4282') >= 0;
install_software:
package_path: ./microsoft-teams.pkg.software.yml
- name: Slack on macOS is installed
platform: darwin
query: SELECT 1 FROM apps WHERE name = 'Slack.app';
install_software:
app_store_id: "123456"
- name: Script run policy
platform: linux
description: This should run a script on failure
@ -135,6 +140,8 @@ policies:
run_script:
path: ./lib/collect-fleetd-logs.sh
software:
app_store_apps:
- app_store_id: "123456"
packages:
- path: ./microsoft-teams.pkg.software.yml
- url: https://ftp.mozilla.org/pub/firefox/releases/129.0.2/mac/en-US/Firefox%20129.0.2.pkg

View file

@ -825,6 +825,8 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
teamNameToID := make(map[string]*uint, 1)
teamIDToPolicies := make(map[*uint][]*fleet.PolicySpec, 1)
softwareInstallerIDs := make(map[*uint]map[uint]*uint) // teamID -> titleID -> softwareInstallerID
vppAppsTeamsIDs := make(map[*uint]map[uint]*uint) // teamID -> titleID -> vppAppsTeamsID
vppTitleIDs := make(map[uint]struct{}) // set when a title is a VPP app rather than a software installer
// Get the team IDs
for _, spec := range specs {
@ -853,7 +855,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
teamIDToPolicies[teamID] = append(teamIDToPolicies[teamID], spec)
}
// Get software installer ids from software title ids.
// Get software installer ids + VPP apps teams IDs from software title IDs.
for _, spec := range specs {
if spec.SoftwareTitleID == nil || *spec.SoftwareTitleID == 0 {
continue
@ -861,20 +863,36 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
if spec.Team == "" {
return ctxerr.Wrap(ctx, errSoftwareTitleIDOnGlobalPolicy, "create policy from spec")
}
var softwareInstallerID uint
err := sqlx.GetContext(ctx, queryerContext, &softwareInstallerID,
`SELECT id FROM software_installers WHERE global_or_team_id = ? AND title_id = ?`,
teamNameToID[spec.Team], spec.SoftwareTitleID)
var ids struct {
SoftwareInstallerID *uint `db:"si_id"`
VPPAppsTeamsID *uint `db:"vat_id"`
}
err := sqlx.GetContext(ctx, queryerContext, &ids,
`SELECT id si_id, NULL vat_id FROM software_installers WHERE global_or_team_id = ? AND title_id = ?
UNION
SELECT NULL si_id, vat.id vat_id FROM vpp_apps_teams vat
JOIN vpp_apps va ON va.adam_id = vat.adam_id AND va.platform = vat.platform
WHERE global_or_team_id = ? AND title_id = ?`,
teamNameToID[spec.Team], spec.SoftwareTitleID, teamNameToID[spec.Team], spec.SoftwareTitleID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return ctxerr.Wrap(ctx, notFound("SoftwareInstaller").WithID(*spec.SoftwareTitleID), "get software installer id")
}
return ctxerr.Wrap(ctx, err, "get software installer id")
}
if len(softwareInstallerIDs[teamNameToID[spec.Team]]) == 0 {
softwareInstallerIDs[teamNameToID[spec.Team]] = make(map[uint]*uint)
if ids.SoftwareInstallerID != nil {
if len(softwareInstallerIDs[teamNameToID[spec.Team]]) == 0 {
softwareInstallerIDs[teamNameToID[spec.Team]] = make(map[uint]*uint)
}
softwareInstallerIDs[teamNameToID[spec.Team]][*spec.SoftwareTitleID] = ids.SoftwareInstallerID
}
if ids.VPPAppsTeamsID != nil {
if len(vppAppsTeamsIDs[teamNameToID[spec.Team]]) == 0 {
vppAppsTeamsIDs[teamNameToID[spec.Team]] = make(map[uint]*uint)
}
vppAppsTeamsIDs[teamNameToID[spec.Team]][*spec.SoftwareTitleID] = ids.VPPAppsTeamsID
vppTitleIDs[*spec.SoftwareTitleID] = struct{}{}
}
softwareInstallerIDs[teamNameToID[spec.Team]][*spec.SoftwareTitleID] = &softwareInstallerID
}
// Get the query and platforms of the current policies so that we can check if query or platform changed later, if needed
@ -883,6 +901,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
Query string `db:"query"`
Platforms string `db:"platforms"`
SoftwareInstallerID *uint `db:"software_installer_id"`
VPPAppsTeamsID *uint `db:"vpp_apps_teams_id"`
ScriptID *uint `db:"script_id"`
}
teamIDToPoliciesByName := make(map[*uint]map[string]policyLite, len(teamIDToPolicies))
@ -897,10 +916,10 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
var args []interface{}
var err error
if teamID == nil {
query, args, err = sqlx.In("SELECT name, query, platforms, software_installer_id, script_id FROM policies WHERE team_id IS NULL AND name IN (?)", policyNames)
query, args, err = sqlx.In("SELECT name, query, platforms, software_installer_id, vpp_apps_teams_id, script_id FROM policies WHERE team_id IS NULL AND name IN (?)", policyNames)
} else {
query, args, err = sqlx.In(
"SELECT name, query, platforms, software_installer_id, script_id FROM policies WHERE team_id = ? AND name IN (?)", *teamID, policyNames,
"SELECT name, query, platforms, software_installer_id, vpp_apps_teams_id, script_id FROM policies WHERE team_id = ? AND name IN (?)", *teamID, policyNames,
)
}
if err != nil {
@ -930,9 +949,10 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
critical,
calendar_events_enabled,
software_installer_id,
vpp_apps_teams_id,
script_id,
checksum
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, %s)
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, %s)
ON DUPLICATE KEY UPDATE
query = VALUES(query),
description = VALUES(description),
@ -942,15 +962,22 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
critical = VALUES(critical),
calendar_events_enabled = VALUES(calendar_events_enabled),
software_installer_id = VALUES(software_installer_id),
vpp_apps_teams_id = VALUES(vpp_apps_teams_id),
script_id = VALUES(script_id)
`, policiesChecksumComputedColumn(),
)
for teamID, teamPolicySpecs := range teamIDToPolicies {
for _, spec := range teamPolicySpecs {
var softwareInstallerID *uint
var vppAppsTeamsID *uint
if spec.SoftwareTitleID != nil {
softwareInstallerID = softwareInstallerIDs[teamNameToID[spec.Team]][*spec.SoftwareTitleID]
if _, ok := vppTitleIDs[*spec.SoftwareTitleID]; !ok {
softwareInstallerID = softwareInstallerIDs[teamNameToID[spec.Team]][*spec.SoftwareTitleID]
} else {
vppAppsTeamsID = vppAppsTeamsIDs[teamNameToID[spec.Team]][*spec.SoftwareTitleID]
}
}
scriptID := spec.ScriptID
if spec.ScriptID != nil && *spec.ScriptID == 0 {
scriptID = nil
@ -958,8 +985,9 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
res, err := tx.ExecContext(
ctx,
query, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, teamID, spec.Platform, spec.Critical,
spec.CalendarEventsEnabled, softwareInstallerID, scriptID,
query,
spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, teamID, spec.Platform, spec.Critical,
spec.CalendarEventsEnabled, softwareInstallerID, vppAppsTeamsID, scriptID,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
@ -973,7 +1001,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
shouldRemoveAllPolicyMemberships bool
removePolicyStats bool
)
// Figure out if the query, platform or software installer changed.
// Figure out if the query, platform, software installer, or VPP app changed.
var softwareInstallerID *uint
if spec.SoftwareTitleID != nil {
softwareInstallerID = softwareInstallerIDs[teamID][*spec.SoftwareTitleID]
@ -988,6 +1016,11 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
(prev.SoftwareInstallerID != nil && softwareInstallerID != nil && *prev.SoftwareInstallerID != *softwareInstallerID)):
shouldRemoveAllPolicyMemberships = true
removePolicyStats = true
case teamID != nil &&
((prev.VPPAppsTeamsID == nil && spec.SoftwareTitleID != nil) ||
(prev.VPPAppsTeamsID != nil && vppAppsTeamsID != nil && *prev.VPPAppsTeamsID != *vppAppsTeamsID)):
shouldRemoveAllPolicyMemberships = true
removePolicyStats = true
case teamID != nil &&
((prev.ScriptID == nil && spec.ScriptID != nil) ||
(prev.ScriptID != nil && spec.ScriptID != nil && *prev.ScriptID != *spec.ScriptID)):

View file

@ -4654,6 +4654,25 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.NotNil(t, installer5.TitleID)
test.CreateInsertGlobalVPPToken(t, ds)
// create VPP apps
va1, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp1", BundleIdentifier: "com.app.vpp1",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_1", Platform: fleet.MacOSPlatform}},
}, &team1.ID)
require.NoError(t, err)
va2, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp2", BundleIdentifier: "com.app.vpp2",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_2", Platform: fleet.MacOSPlatform}},
}, &team2.ID)
require.NoError(t, err)
va1NoTeam, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp1", BundleIdentifier: "com.app.vpp1",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_1", Platform: fleet.MacOSPlatform}},
}, nil)
require.NoError(t, err)
// Installers cannot be assigned to global policies.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
@ -4698,34 +4717,78 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
Platform: "linux",
SoftwareTitleID: installer3.TitleID,
},
{
Name: "VPP Team policy 1",
Query: "SELECT 1;",
Description: "Description 1",
Resolution: "Resolution 1",
Team: "team1",
Platform: "darwin",
SoftwareTitleID: &va1.TitleID,
},
{
Name: "VPP Team policy 2",
Query: "SELECT 2;",
Description: "Description 2",
Resolution: "Resolution 2",
Team: "team2",
Platform: "linux",
SoftwareTitleID: &va2.TitleID,
},
{
Name: "VPP No team policy 3",
Query: "SELECT 3;",
Description: "Description 3",
Resolution: "Resolution 3",
Team: "No team",
Platform: "linux",
SoftwareTitleID: &va1NoTeam.TitleID,
},
})
require.NoError(t, err)
team1Policies, _, err := ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.Len(t, team1Policies, 2)
require.NotNil(t, team1Policies[0].SoftwareInstallerID)
require.NotNil(t, team1Policies[1].VPPAppsTeamsID)
policy1Team1 := team1Policies[0]
require.Equal(t, installer1.InstallerID, *team1Policies[0].SoftwareInstallerID)
vppPolicy1Team1 := team1Policies[1]
va1Meta, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team1.ID, va1.TitleID)
require.NoError(t, err)
require.Equal(t, va1Meta.VPPAppsTeamsID, *vppPolicy1Team1.VPPAppsTeamsID)
team2Policies, _, err := ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team2Policies, 1)
require.Len(t, team2Policies, 2)
require.NotNil(t, team2Policies[0].SoftwareInstallerID)
require.Equal(t, installer2.InstallerID, *team2Policies[0].SoftwareInstallerID)
vppPolicy2Team2 := team2Policies[1]
va2Meta, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team2.ID, va2.TitleID)
require.NoError(t, err)
require.Equal(t, va2Meta.VPPAppsTeamsID, *vppPolicy2Team2.VPPAppsTeamsID)
noTeamPolicies, _, err := ds.ListTeamPolicies(ctx, fleet.PolicyNoTeamID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, noTeamPolicies, 1)
require.Len(t, noTeamPolicies, 2)
require.NotNil(t, noTeamPolicies[0].SoftwareInstallerID)
require.Equal(t, installer3.InstallerID, *noTeamPolicies[0].SoftwareInstallerID)
vppNoTeamPolicy := noTeamPolicies[1]
vNoTeamMeta, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, ptr.Uint(0), va1NoTeam.TitleID)
require.NoError(t, err)
require.Equal(t, vNoTeamMeta.VPPAppsTeamsID, *vppNoTeamPolicy.VPPAppsTeamsID)
// Record policy execution on policy1Team1.
err = ds.RecordPolicyQueryExecutions(ctx, host1Team1, map[uint]*bool{
policy1Team1.ID: ptr.Bool(false),
policy1Team1.ID: ptr.Bool(false),
vppPolicy1Team1.ID: ptr.Bool(false),
}, time.Now(), false)
require.NoError(t, err)
err = ds.UpdateHostPolicyCounts(ctx)
require.NoError(t, err)
// Unset software installer from "Team policy 1".
// Unset software installer from "Team policy 1" and the VPP policy.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "Team policy 1",
@ -4736,14 +4799,25 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
Platform: "darwin",
SoftwareTitleID: nil,
},
{
Name: "VPP Team policy 1",
Query: "SELECT 1;",
Description: "Description 1",
Resolution: "Resolution 1",
Team: "team1",
Platform: "darwin",
SoftwareTitleID: nil,
},
})
require.NoError(t, err)
team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.Len(t, team1Policies, 2)
require.Nil(t, team1Policies[0].SoftwareInstallerID)
require.Nil(t, team1Policies[1].VPPAppsTeamsID)
// Should not clear results because we've cleared not changed/set-new installer.
require.Equal(t, uint(1), team1Policies[0].FailingHostCount)
require.Equal(t, uint(1), team1Policies[1].FailingHostCount)
// Set "Team policy 1" to a software installer on team2.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
@ -4761,6 +4835,21 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
var notFoundErr *notFoundError
require.ErrorAs(t, err, &notFoundErr)
// Set "Team policy 1" to a VPP app on team2.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "Team policy 1",
Query: "SELECT 1;",
Description: "Description 1",
Resolution: "Resolution 1",
Team: "team1",
Platform: "darwin",
SoftwareTitleID: &va2.TitleID,
},
})
require.Error(t, err)
require.ErrorAs(t, err, &notFoundErr)
// Set "No team policy 3" to a software installer on team2.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
@ -4776,6 +4865,21 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.Error(t, err)
require.ErrorAs(t, err, &notFoundErr)
// Set "No Team policy 3" to a VPP app on team2.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "No team policy 3",
Query: "SELECT 3;",
Description: "Description 3",
Resolution: "Resolution 3",
Team: "No team",
Platform: "darwin",
SoftwareTitleID: &va2.TitleID,
},
})
require.Error(t, err)
require.ErrorAs(t, err, &notFoundErr)
// Set "Team policy 1" to a software title that doesn't exist.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
@ -4821,10 +4925,11 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.NoError(t, err)
team2Policies, _, err = ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team2Policies, 1)
require.Len(t, team2Policies, 2)
require.Nil(t, team2Policies[0].SoftwareInstallerID)
require.Equal(t, va2Meta.VPPAppsTeamsID, *team2Policies[1].VPPAppsTeamsID) // stays set since Apply doesn't delete
// Apply team policies associated to two installers (again, with two installers with the same title).
// Apply team policies associated to two installers (again, with two installers with the same title), and same with VPP apps
tfr4, err := fleet.NewTempFileReader(strings.NewReader("hello3"), t.TempDir)
require.NoError(t, err)
installer4ID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
@ -4845,6 +4950,12 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
installer4, err := ds.GetSoftwareInstallerMetadataByID(ctx, installer4ID)
require.NoError(t, err)
require.NotNil(t, installer2.TitleID)
va4Team2, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp4", BundleIdentifier: "com.app.vpp4",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_4", Platform: fleet.MacOSPlatform}},
}, &team2.ID)
require.NoError(t, err)
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "Team policy 1",
@ -4864,13 +4975,35 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
Platform: "linux",
SoftwareTitleID: installer4.TitleID,
},
{
Name: "VPP Team policy 1",
Query: "SELECT 1;",
Description: "Description 1",
Resolution: "Resolution 1",
Team: "team1",
Platform: "darwin",
SoftwareTitleID: &va1.TitleID,
},
{
Name: "VPP Team policy 2",
Query: "SELECT 2;",
Description: "Description 2",
Resolution: "Resolution 2",
Team: "team2",
Platform: "linux",
SoftwareTitleID: &va4Team2.TitleID,
},
})
require.NoError(t, err)
team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.Len(t, team1Policies, 2)
require.NotNil(t, team1Policies[0].SoftwareInstallerID)
require.Equal(t, installer1.InstallerID, *team1Policies[0].SoftwareInstallerID)
require.NotNil(t, team1Policies[1].VPPAppsTeamsID)
require.NoError(t, err)
require.Equal(t, va1Meta.VPPAppsTeamsID, *team1Policies[1].VPPAppsTeamsID)
// Should clear results because we've are setting an installer.
require.Equal(t, uint(0), team1Policies[0].FailingHostCount)
countBiggerThanZero := true
@ -4884,13 +5017,18 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.False(t, countBiggerThanZero)
team2Policies, _, err = ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team2Policies, 1)
require.Len(t, team2Policies, 2)
require.NotNil(t, team2Policies[0].SoftwareInstallerID)
require.Equal(t, installer4.InstallerID, *team2Policies[0].SoftwareInstallerID)
require.NotNil(t, team2Policies[1].VPPAppsTeamsID)
va4Team2Meta, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team2.ID, va4Team2.TitleID)
require.NoError(t, err)
require.Equal(t, va4Team2Meta.VPPAppsTeamsID, *team2Policies[1].VPPAppsTeamsID)
// Record policy execution on policy1Team1 to test that setting the same installer won't clear results.
// Record policy execution on policy1Team1 + VPP equivalent to test that setting the same installer won't clear results.
err = ds.RecordPolicyQueryExecutions(ctx, host1Team1, map[uint]*bool{
policy1Team1.ID: ptr.Bool(false),
policy1Team1.ID: ptr.Bool(false),
vppPolicy1Team1.ID: ptr.Bool(false),
}, time.Now(), false)
require.NoError(t, err)
err = ds.UpdateHostPolicyCounts(ctx)
@ -4905,11 +5043,20 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
Platform: "darwin",
SoftwareTitleID: installer1.TitleID,
},
{
Name: "VPP Team policy 1",
Query: "SELECT 1;",
Description: "Description 1",
Resolution: "Resolution 1",
Team: "team1",
Platform: "darwin",
SoftwareTitleID: &va1.TitleID,
},
})
require.NoError(t, err)
team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.Len(t, team1Policies, 2)
require.Equal(t, uint(1), team1Policies[0].FailingHostCount)
countBiggerThanZero = false
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@ -4920,6 +5067,22 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
)
})
require.True(t, countBiggerThanZero)
require.Equal(t, uint(1), team1Policies[1].FailingHostCount)
countBiggerThanZero = false
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&countBiggerThanZero,
`SELECT COUNT(*) > 0 FROM policy_membership WHERE policy_id = ?`,
team1Policies[1].ID,
)
})
require.True(t, countBiggerThanZero)
va4Team1, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp4", BundleIdentifier: "com.app.vpp4",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_4", Platform: fleet.MacOSPlatform}},
}, &team1.ID)
require.NoError(t, err)
// Now change the installer, should clear results.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
@ -4932,11 +5095,20 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
Platform: "darwin",
SoftwareTitleID: installer5.TitleID,
},
{
Name: "VPP Team policy 1",
Query: "SELECT 1;",
Description: "Description 1",
Resolution: "Resolution 1",
Team: "team1",
Platform: "darwin",
SoftwareTitleID: &va4Team1.TitleID,
},
})
require.NoError(t, err)
team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.Len(t, team1Policies, 2)
require.Equal(t, uint(0), team1Policies[0].FailingHostCount)
countBiggerThanZero = true
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@ -4947,6 +5119,16 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
)
})
require.False(t, countBiggerThanZero)
require.Equal(t, uint(0), team1Policies[1].FailingHostCount)
countBiggerThanZero = true
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&countBiggerThanZero,
`SELECT COUNT(*) > 0 FROM policy_membership WHERE policy_id = ?`,
team1Policies[1].ID,
)
})
require.False(t, countBiggerThanZero)
}
func testTeamPoliciesNoTeam(t *testing.T, ds *Datastore) {

View file

@ -270,6 +270,25 @@ func (ds *Datastore) InsertVPPAppWithTeam(ctx context.Context, app *fleet.VPPApp
return app, nil
}
func (ds *Datastore) GetVPPApps(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
var tmID uint
if teamID != nil {
tmID = *teamID
}
var results []fleet.VPPAppResponse
// intentionally using writer as this is called right after batch-setting VPP apps
if err := sqlx.SelectContext(ctx, ds.writer(ctx), &results, `
SELECT vat.team_id, va.title_id, vat.adam_id app_store_id, vat.platform
FROM vpp_apps_teams vat
JOIN vpp_apps va ON va.adam_id = vat.adam_id AND va.platform = vat.platform
WHERE global_or_team_id = ?`, tmID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get VPP apps")
}
return results, nil
}
func (ds *Datastore) GetAssignedVPPApps(ctx context.Context, teamID *uint) (map[fleet.VPPAppID]fleet.VPPAppTeam, error) {
stmt := `
SELECT
@ -358,17 +377,14 @@ ON DUPLICATE KEY UPDATE
}
func removeVPPAppTeams(ctx context.Context, tx sqlx.ExtContext, appID fleet.VPPAppID, teamID *uint) error {
stmt := `
DELETE FROM
vpp_apps_teams
WHERE
adam_id = ?
AND
team_id = ?
AND
platform = ?
`
_, err := tx.ExecContext(ctx, stmt, appID.AdamID, teamID, appID.Platform)
_, err := tx.ExecContext(ctx, `UPDATE policies p
JOIN vpp_apps_teams vat ON vat.id = p.vpp_apps_teams_id AND vat.adam_id = ? AND vat.team_id = ? AND vat.platform = ?
SET vpp_apps_teams_id = NULL`, appID.AdamID, teamID, appID.Platform)
if err != nil {
return ctxerr.Wrap(ctx, err, "unsetting vpp app policy associations from team")
}
_, err = tx.ExecContext(ctx, `DELETE FROM vpp_apps_teams WHERE adam_id = ? AND team_id = ? AND platform = ?`, appID.AdamID, teamID, appID.Platform)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting vpp app from team")
}

View file

@ -601,6 +601,26 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
// create policies using two of the apps
app1Meta, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team.ID, app1.TitleID)
require.NoError(t, err)
app2Meta, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team.ID, app2.TitleID)
require.NoError(t, err)
policy1, err := ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{
Name: "Policy 1",
Query: "SELECT 1;",
Platform: "darwin",
VPPAppsTeamsID: &app1Meta.VPPAppsTeamsID,
})
require.NoError(t, err)
policy2, err := ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{
Name: "Policy 2",
Query: "SELECT 1;",
Platform: "darwin",
VPPAppsTeamsID: &app2Meta.VPPAppsTeamsID,
})
require.NoError(t, err)
assigned, err = ds.GetAssignedVPPApps(ctx, &team.ID)
require.NoError(t, err)
require.Len(t, assigned, 2)
@ -617,6 +637,10 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
policy1, err = ds.Policy(ctx, policy1.ID)
require.NoError(t, err)
require.NotNil(t, policy1.VPPAppsTeamsID)
assigned, err = ds.GetAssignedVPPApps(ctx, &team.ID)
require.NoError(t, err)
require.Len(t, assigned, 3)
@ -634,6 +658,10 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
policy1, err = ds.Policy(ctx, policy1.ID)
require.NoError(t, err)
require.Equal(t, app1Meta.VPPAppsTeamsID, *policy1.VPPAppsTeamsID)
assigned, err = ds.GetAssignedVPPApps(ctx, &team.ID)
require.NoError(t, err)
require.Len(t, assigned, 3)
@ -666,6 +694,14 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
policy1, err = ds.Policy(ctx, policy1.ID)
require.NoError(t, err)
require.Nil(t, policy1.VPPAppsTeamsID)
policy2, err = ds.Policy(ctx, policy2.ID)
require.NoError(t, err)
require.Equal(t, app2Meta.VPPAppsTeamsID, *policy2.VPPAppsTeamsID)
// Remove all apps
err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{})
require.NoError(t, err)
@ -673,6 +709,14 @@ func testSetTeamVPPApps(t *testing.T, ds *Datastore) {
assigned, err = ds.GetAssignedVPPApps(ctx, &team.ID)
require.NoError(t, err)
require.Len(t, assigned, 0)
policy1, err = ds.Policy(ctx, policy1.ID)
require.NoError(t, err)
require.Nil(t, policy1.VPPAppsTeamsID)
policy2, err = ds.Policy(ctx, policy2.ID)
require.NoError(t, err)
require.Nil(t, policy2.VPPAppsTeamsID)
}
func testGetVPPAppByTeamAndTitleID(t *testing.T, ds *Datastore) {

View file

@ -1817,6 +1817,7 @@ type Datastore interface {
BatchInsertVPPApps(ctx context.Context, apps []*VPPApp) error
GetAssignedVPPApps(ctx context.Context, teamID *uint) (map[VPPAppID]VPPAppTeam, error)
GetVPPApps(ctx context.Context, teamID *uint) ([]VPPAppResponse, error)
SetTeamVPPApps(ctx context.Context, teamID *uint, appIDs []VPPAppTeam) error
InsertVPPAppWithTeam(ctx context.Context, app *VPPApp, teamID *uint) (*VPPApp, error)

View file

@ -770,7 +770,7 @@ type Service interface {
GetVPPTokens(ctx context.Context) ([]*VPPTokenDB, error)
DeleteVPPToken(ctx context.Context, tokenID uint) error
BatchAssociateVPPApps(ctx context.Context, teamName string, payloads []VPPBatchPayload, dryRun bool) error
BatchAssociateVPPApps(ctx context.Context, teamName string, payloads []VPPBatchPayload, dryRun bool) ([]VPPAppResponse, error)
// GetHostDEPAssignment retrieves the host DEP assignment for the specified host.
GetHostDEPAssignment(ctx context.Context, host *Host) (*HostDEPAssignment, error)

View file

@ -158,6 +158,19 @@ type SoftwarePackageResponse struct {
URL string `json:"url" db:"url"`
}
// VPPAppResponse is the response type used when applying app store apps by batch.
type VPPAppResponse struct {
// TeamID is the ID of the team.
// A value of nil means it is scoped to hosts that are assigned to "No team".
TeamID *uint `json:"team_id" db:"team_id"`
// TitleID is the id of the software title associated with the software installer.
TitleID *uint `json:"title_id" db:"title_id"`
// AppStoreID is the ADAM ID for this app (set when uploading via batch/gitops).
AppStoreID string `json:"app_store_id" db:"app_store_id"`
// Platform is the platform this title ID corresponds to
Platform AppleDevicePlatform `json:"platform" db:"platform"`
}
// AuthzType implements authz.AuthzTyper.
func (s *SoftwareInstaller) AuthzType() string {
return "installable_entity"

View file

@ -1143,6 +1143,8 @@ type BatchInsertVPPAppsFunc func(ctx context.Context, apps []*fleet.VPPApp) erro
type GetAssignedVPPAppsFunc func(ctx context.Context, teamID *uint) (map[fleet.VPPAppID]fleet.VPPAppTeam, error)
type GetVPPAppsFunc func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error)
type SetTeamVPPAppsFunc func(ctx context.Context, teamID *uint, appIDs []fleet.VPPAppTeam) error
type InsertVPPAppWithTeamFunc func(ctx context.Context, app *fleet.VPPApp, teamID *uint) (*fleet.VPPApp, error)
@ -2887,6 +2889,9 @@ type DataStore struct {
GetAssignedVPPAppsFunc GetAssignedVPPAppsFunc
GetAssignedVPPAppsFuncInvoked bool
GetVPPAppsFunc GetVPPAppsFunc
GetVPPAppsFuncInvoked bool
SetTeamVPPAppsFunc SetTeamVPPAppsFunc
SetTeamVPPAppsFuncInvoked bool
@ -6907,6 +6912,13 @@ func (s *DataStore) GetAssignedVPPApps(ctx context.Context, teamID *uint) (map[f
return s.GetAssignedVPPAppsFunc(ctx, teamID)
}
func (s *DataStore) GetVPPApps(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
s.mu.Lock()
s.GetVPPAppsFuncInvoked = true
s.mu.Unlock()
return s.GetVPPAppsFunc(ctx, teamID)
}
func (s *DataStore) SetTeamVPPApps(ctx context.Context, teamID *uint, appIDs []fleet.VPPAppTeam) error {
s.mu.Lock()
s.SetTeamVPPAppsFuncInvoked = true

View file

@ -42,37 +42,37 @@ type MDMBootstrapPackageStore struct {
mu sync.Mutex
}
func (fs *MDMBootstrapPackageStore) Get(ctx context.Context, packageID string) (io.ReadCloser, int64, error) {
fs.mu.Lock()
fs.GetFuncInvoked = true
fs.mu.Unlock()
return fs.GetFunc(ctx, packageID)
func (s *MDMBootstrapPackageStore) Get(ctx context.Context, packageID string) (io.ReadCloser, int64, error) {
s.mu.Lock()
s.GetFuncInvoked = true
s.mu.Unlock()
return s.GetFunc(ctx, packageID)
}
func (fs *MDMBootstrapPackageStore) Put(ctx context.Context, packageID string, content io.ReadSeeker) error {
fs.mu.Lock()
fs.PutFuncInvoked = true
fs.mu.Unlock()
return fs.PutFunc(ctx, packageID, content)
func (s *MDMBootstrapPackageStore) Put(ctx context.Context, packageID string, content io.ReadSeeker) error {
s.mu.Lock()
s.PutFuncInvoked = true
s.mu.Unlock()
return s.PutFunc(ctx, packageID, content)
}
func (fs *MDMBootstrapPackageStore) Exists(ctx context.Context, packageID string) (bool, error) {
fs.mu.Lock()
fs.ExistsFuncInvoked = true
fs.mu.Unlock()
return fs.ExistsFunc(ctx, packageID)
func (s *MDMBootstrapPackageStore) Exists(ctx context.Context, packageID string) (bool, error) {
s.mu.Lock()
s.ExistsFuncInvoked = true
s.mu.Unlock()
return s.ExistsFunc(ctx, packageID)
}
func (fs *MDMBootstrapPackageStore) Cleanup(ctx context.Context, usedPackageIDs []string, removeCreatedBefore time.Time) (int, error) {
fs.mu.Lock()
fs.CleanupFuncInvoked = true
fs.mu.Unlock()
return fs.CleanupFunc(ctx, usedPackageIDs, removeCreatedBefore)
func (s *MDMBootstrapPackageStore) Cleanup(ctx context.Context, usedPackageIDs []string, removeCreatedBefore time.Time) (int, error) {
s.mu.Lock()
s.CleanupFuncInvoked = true
s.mu.Unlock()
return s.CleanupFunc(ctx, usedPackageIDs, removeCreatedBefore)
}
func (fs *MDMBootstrapPackageStore) Sign(ctx context.Context, fileID string) (string, error) {
fs.mu.Lock()
fs.SignFuncInvoked = true
fs.mu.Unlock()
return fs.SignFunc(ctx, fileID)
func (s *MDMBootstrapPackageStore) Sign(ctx context.Context, fileID string) (string, error) {
s.mu.Lock()
s.SignFuncInvoked = true
s.mu.Unlock()
return s.SignFunc(ctx, fileID)
}

View file

@ -423,8 +423,9 @@ func (c *Client) ApplyGroup(
appconfig *fleet.EnrichedAppConfig,
opts fleet.ApplyClientSpecOptions,
teamsSoftwareInstallers map[string][]fleet.SoftwarePackageResponse,
teamsVPPApps map[string][]fleet.VPPAppResponse,
teamsScripts map[string][]fleet.ScriptResponse,
) (map[string]uint, map[string][]fleet.SoftwarePackageResponse, map[string][]fleet.ScriptResponse, error) {
) (map[string]uint, map[string][]fleet.SoftwarePackageResponse, map[string][]fleet.VPPAppResponse, map[string][]fleet.ScriptResponse, error) {
logfn := func(format string, args ...interface{}) {
if logf != nil {
logf(format, args...)
@ -437,7 +438,7 @@ func (c *Client) ApplyGroup(
logfn("[!] ignoring queries, dry run mode only supported for 'config' and 'team' specs\n")
} else {
if err := c.ApplyQueries(specs.Queries); err != nil {
return nil, nil, nil, fmt.Errorf("applying queries: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying queries: %w", err)
}
logfn("[+] applied %d queries\n", len(specs.Queries))
}
@ -448,7 +449,7 @@ func (c *Client) ApplyGroup(
logfn("[!] ignoring labels, dry run mode only supported for 'config' and 'team' specs\n")
} else {
if err := c.ApplyLabels(specs.Labels); err != nil {
return nil, nil, nil, fmt.Errorf("applying labels: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying labels: %w", err)
}
logfn("[+] applied %d labels\n", len(specs.Labels))
}
@ -459,7 +460,7 @@ func (c *Client) ApplyGroup(
logfn("[!] ignoring packs, dry run mode only supported for 'config' and 'team' specs\n")
} else {
if err := c.ApplyPacks(specs.Packs); err != nil {
return nil, nil, nil, fmt.Errorf("applying packs: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying packs: %w", err)
}
logfn("[+] applied %d packs\n", len(specs.Packs))
}
@ -479,7 +480,7 @@ func (c *Client) ApplyGroup(
if (windowsCustomSettings != nil && macosCustomSettings != nil) || len(windowsCustomSettings)+len(macosCustomSettings) > 0 {
fileContents, err := getProfilesContents(baseDir, macosCustomSettings, windowsCustomSettings, opts.ExpandEnvConfigProfiles)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
// Figure out if MDM should be enabled.
assumeEnabled := false
@ -493,30 +494,30 @@ func (c *Client) ApplyGroup(
}
}
if err := c.ApplyNoTeamProfiles(fileContents, opts.ApplySpecOptions, assumeEnabled); err != nil {
return nil, nil, nil, fmt.Errorf("applying custom settings: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying custom settings: %w", err)
}
}
if macosSetup := extractAppCfgMacOSSetup(specs.AppConfig); macosSetup != nil {
if macosSetup.BootstrapPackage.Value != "" {
pkg, err := c.ValidateBootstrapPackageFromURL(macosSetup.BootstrapPackage.Value)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
}
if !opts.DryRun {
if err := c.EnsureBootstrapPackage(pkg, uint(0)); err != nil {
return nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
}
}
}
if macosSetup.MacOSSetupAssistant.Value != "" {
content, err := c.validateMacOSSetupAssistant(resolveApplyRelativePath(baseDir, macosSetup.MacOSSetupAssistant.Value))
if err != nil {
return nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
}
if !opts.DryRun {
if err := c.uploadMacOSSetupAssistant(content, nil, macosSetup.MacOSSetupAssistant.Value); err != nil {
return nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
}
}
}
@ -526,7 +527,7 @@ func (c *Client) ApplyGroup(
for i, f := range scripts {
b, err := os.ReadFile(f)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying no-team scripts: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying no-team scripts: %w", err)
}
scriptPayloads[i] = fleet.ScriptPayload{
ScriptContents: b,
@ -535,14 +536,14 @@ func (c *Client) ApplyGroup(
}
noTeamScripts, err := c.ApplyNoTeamScripts(scriptPayloads, opts.ApplySpecOptions)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying no-team scripts: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying no-team scripts: %w", err)
}
teamsScripts["No team"] = noTeamScripts
}
rules, err := extractAppCfgYaraRules(specs.AppConfig)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying yara rules: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying yara rules: %w", err)
}
if rules != nil {
rulePayloads := make([]fleet.YaraRule, len(rules))
@ -550,7 +551,7 @@ func (c *Client) ApplyGroup(
path := resolveApplyRelativePath(baseDir, f.Path)
b, err := os.ReadFile(path)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying yara rules: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying yara rules: %w", err)
}
rulePayloads[i] = fleet.YaraRule{
Contents: string(b),
@ -561,7 +562,7 @@ func (c *Client) ApplyGroup(
}
if err := c.ApplyAppConfig(specs.AppConfig, opts.ApplySpecOptions); err != nil {
return nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
}
if opts.DryRun {
logfn("[+] would've applied fleet config\n")
@ -572,7 +573,7 @@ func (c *Client) ApplyGroup(
if specs.EnrollSecret != nil {
if err := c.ApplyEnrollSecretSpec(specs.EnrollSecret, opts.ApplySpecOptions); err != nil {
return nil, nil, nil, fmt.Errorf("applying enroll secrets: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying enroll secrets: %w", err)
}
if opts.DryRun {
logfn("[+] would've applied enroll secrets\n")
@ -591,7 +592,8 @@ func (c *Client) ApplyGroup(
for k, profileSpecs := range tmMDMSettings {
fileContents, err := getProfilesContents(baseDir, profileSpecs.macos, profileSpecs.windows, opts.ExpandEnvConfigProfiles)
if err != nil {
return nil, nil, nil, fmt.Errorf("Team %s: %w", k, err) // TODO: consider adding team name to improve error messages generally for other parts of the config because multiple team configs can be processed at once
// TODO: consider adding team name to improve error messages generally for other parts of the config because multiple team configs can be processed at once
return nil, nil, nil, nil, fmt.Errorf("Team %s: %w", k, err)
}
tmFileContents[k] = fileContents
}
@ -612,28 +614,28 @@ func (c *Client) ApplyGroup(
if setup.BootstrapPackage.Value != "" {
bp, err := c.ValidateBootstrapPackageFromURL(setup.BootstrapPackage.Value)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying teams: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying teams: %w", err)
}
tmBootstrapPackages[k] = bp
}
if setup.MacOSSetupAssistant.Value != "" {
b, err := c.validateMacOSSetupAssistant(resolveApplyRelativePath(baseDir, setup.MacOSSetupAssistant.Value))
if err != nil {
return nil, nil, nil, fmt.Errorf("applying teams: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying teams: %w", err)
}
tmMacSetupAssistants[k] = b
}
if setup.Script.Value != "" {
b, err := c.validateMacOSSetupScript(resolveApplyRelativePath(baseDir, setup.Script.Value))
if err != nil {
return nil, nil, nil, fmt.Errorf("applying teams: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying teams: %w", err)
}
tmMacSetupScript[k] = fileContent{Filename: filepath.Base(setup.Script.Value), Content: b}
}
if viaGitOps {
m, err := extractTeamOrNoTeamMacOSSetupSoftware(baseDir, setup.Software.Value)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
tmSoftwareMacOSSetup[k] = m
tmMacSetupSoftware[k] = setup.Software.Value
@ -647,7 +649,7 @@ func (c *Client) ApplyGroup(
for i, f := range paths {
b, err := os.ReadFile(f)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying fleet config: %w", err)
}
scriptPayloads[i] = fleet.ScriptPayload{
ScriptContents: b,
@ -664,7 +666,7 @@ func (c *Client) ApplyGroup(
installDuringSetupKeys := tmSoftwareMacOSSetup[tmName]
softwarePayloads, err := buildSoftwarePackagesPayload(software, installDuringSetupKeys)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying software installers for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("applying software installers for team %q: %w", tmName, err)
}
tmSoftwarePackagesPayloads[tmName] = softwarePayloads
for _, swSpec := range software {
@ -708,7 +710,7 @@ func (c *Client) ApplyGroup(
// packages or vpp apps.
for tmName, setupSw := range tmMacSetupSoftware {
if err := validateTeamOrNoTeamMacOSSetupSoftware(tmName, setupSw, tmSoftwarePackageByPath[tmName], tmSoftwareAppsByAppID[tmName]); err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
}
@ -722,7 +724,7 @@ func (c *Client) ApplyGroup(
// In dry-run, the team names returned are the old team names (when team name is modified via gitops)
teamIDsByName, err = c.ApplyTeams(specs.Teams, teamOpts)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying teams: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying teams: %w", err)
}
// When using GitOps, the team name could change, so we need to check for that
@ -749,7 +751,7 @@ func (c *Client) ApplyGroup(
} else {
logfn("[+] applying MDM profiles for team %s\n", tmName)
if err := c.ApplyTeamProfiles(currentTeamName, profs, teamOpts); err != nil {
return nil, nil, nil, fmt.Errorf("applying custom settings for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("applying custom settings for team %q: %w", tmName, err)
}
}
}
@ -758,7 +760,7 @@ func (c *Client) ApplyGroup(
for tmName, tmID := range teamIDsByName {
if bp, ok := tmBootstrapPackages[tmName]; ok {
if err := c.EnsureBootstrapPackage(bp, tmID); err != nil {
return nil, nil, nil, fmt.Errorf("uploading bootstrap package for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("uploading bootstrap package for team %q: %w", tmName, err)
}
}
if b, ok := tmMacSetupAssistants[tmName]; ok {
@ -770,11 +772,11 @@ func (c *Client) ApplyGroup(
// to render a more helpful error message.
parts := strings.Split(err.Error(), ".")
if len(parts) < 2 {
return nil, nil, nil, fmt.Errorf("unexpected error while uploading macOS setup assistant for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("unexpected error while uploading macOS setup assistant for team %q: %w", tmName, err)
}
return nil, nil, nil, fmt.Errorf("Couldn't edit macos_setup_assistant. Response from Apple: %s. Learn more at %s", strings.Trim(parts[1], " "), "https://fleetdm.com/learn-more-about/dep-profile")
return nil, nil, nil, nil, fmt.Errorf("Couldn't edit macos_setup_assistant. Response from Apple: %s. Learn more at %s", strings.Trim(parts[1], " "), "https://fleetdm.com/learn-more-about/dep-profile")
}
return nil, nil, nil, fmt.Errorf("uploading macOS setup assistant for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("uploading macOS setup assistant for team %q: %w", tmName, err)
}
}
}
@ -783,11 +785,11 @@ func (c *Client) ApplyGroup(
for tmName, tmID := range teamIDsByName {
if fc, ok := tmMacSetupScript[tmName]; ok {
if err := c.uploadMacOSSetupScript(fc.Filename, fc.Content, &tmID); err != nil {
return nil, nil, nil, fmt.Errorf("uploading setup experience script for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("uploading setup experience script for team %q: %w", tmName, err)
}
} else {
if err := c.deleteMacOSSetupScript(&tmID); err != nil {
return nil, nil, nil, fmt.Errorf("deleting setup experience script for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("deleting setup experience script for team %q: %w", tmName, err)
}
}
}
@ -798,7 +800,7 @@ func (c *Client) ApplyGroup(
currentTeamName := getTeamName(tmName)
scriptResponses, err := c.ApplyTeamScripts(currentTeamName, scripts, opts.ApplySpecOptions)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying scripts for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("applying scripts for team %q: %w", tmName, err)
}
teamsScripts[tmName] = scriptResponses
}
@ -810,7 +812,7 @@ func (c *Client) ApplyGroup(
logfn("[+] applying %d software packages for team %s\n", len(software), tmName)
installers, err := c.ApplyTeamSoftwareInstallers(currentTeamName, software, opts.ApplySpecOptions)
if err != nil {
return nil, nil, nil, fmt.Errorf("applying software installers for team %q: %w", tmName, err)
return nil, nil, nil, nil, fmt.Errorf("applying software installers for team %q: %w", tmName, err)
}
teamsSoftwareInstallers[tmName] = installers
}
@ -820,9 +822,11 @@ func (c *Client) ApplyGroup(
// For non-dry run, currentTeamName and tmName are the same
currentTeamName := getTeamName(tmName)
logfn("[+] applying %d app store apps for team %s\n", len(apps), tmName)
if err := c.ApplyTeamAppStoreAppsAssociation(currentTeamName, apps, opts.ApplySpecOptions); err != nil {
return nil, nil, nil, fmt.Errorf("applying app store apps for team: %q: %w", tmName, err)
appsResponse, err := c.ApplyTeamAppStoreAppsAssociation(currentTeamName, apps, opts.ApplySpecOptions)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("applying app store apps for team: %q: %w", tmName, err)
}
teamsVPPApps[tmName] = appsResponse
}
}
if opts.DryRun {
@ -836,7 +840,7 @@ func (c *Client) ApplyGroup(
if len(specs.Policies) > 0 {
// Policy names must be unique, return error if duplicate policy names are found
if policyName := fleet.FirstDuplicatePolicySpecName(specs.Policies); policyName != "" {
return nil, nil, nil, fmt.Errorf(
return nil, nil, nil, nil, fmt.Errorf(
"applying policies: policy names must be unique. Please correct policy %q and try again.", policyName,
)
}
@ -850,7 +854,7 @@ func (c *Client) ApplyGroup(
}
}
if err := c.ApplyPolicies(specs.Policies); err != nil {
return nil, nil, nil, fmt.Errorf("applying policies: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying policies: %w", err)
}
logfn("[+] applied %d policies\n", len(specs.Policies))
}
@ -861,13 +865,13 @@ func (c *Client) ApplyGroup(
logfn("[!] ignoring user roles, dry run mode only supported for 'config' and 'team' specs\n")
} else {
if err := c.ApplyUsersRoleSecretSpec(specs.UsersRoles); err != nil {
return nil, nil, nil, fmt.Errorf("applying user roles: %w", err)
return nil, nil, nil, nil, fmt.Errorf("applying user roles: %w", err)
}
logfn("[+] applied user roles\n")
}
}
return teamIDsByName, teamsSoftwareInstallers, teamsScripts, nil
return teamIDsByName, teamsSoftwareInstallers, teamsVPPApps, teamsScripts, nil
}
func extractTeamOrNoTeamMacOSSetupSoftware(baseDir string, software []*fleet.MacOSSetupSoftware) (map[fleet.MacOSSetupSoftware]struct{}, error) {
@ -1447,6 +1451,7 @@ func (c *Client) DoGitOps(
appConfig *fleet.EnrichedAppConfig,
// pass-by-ref to build lists
teamsSoftwareInstallers map[string][]fleet.SoftwarePackageResponse,
teamsVPPApps map[string][]fleet.VPPAppResponse,
teamsScripts map[string][]fleet.ScriptResponse,
) (*fleet.TeamSpecsDryRunAssumptions, error) {
baseDir := filepath.Dir(fullFilename)
@ -1710,18 +1715,20 @@ func (c *Client) DoGitOps(
}
// Apply org settings, scripts, enroll secrets, team entities (software, scripts, etc.), and controls.
teamIDsByName, teamsSoftwareInstallers, teamsScripts, err := c.ApplyGroup(ctx, true, &group, baseDir, logf, appConfig, fleet.ApplyClientSpecOptions{
teamIDsByName, teamsSoftwareInstallers, teamsVPPApps, teamsScripts, err := c.ApplyGroup(ctx, true, &group, baseDir, logf, appConfig, fleet.ApplyClientSpecOptions{
ApplySpecOptions: fleet.ApplySpecOptions{
DryRun: dryRun,
},
ExpandEnvConfigProfiles: true,
}, teamsSoftwareInstallers, teamsScripts)
}, teamsSoftwareInstallers, teamsVPPApps, teamsScripts)
if err != nil {
return nil, err
}
var teamSoftwareInstallers []fleet.SoftwarePackageResponse
var teamVPPApps []fleet.VPPAppResponse
var teamScripts []fleet.ScriptResponse
if config.TeamName != nil {
if !config.IsNoTeam() {
if len(teamIDsByName) != 1 {
@ -1739,18 +1746,20 @@ func (c *Client) DoGitOps(
config.TeamID = &teamID
}
teamSoftwareInstallers = teamsSoftwareInstallers[*config.TeamName]
teamVPPApps = teamsVPPApps[*config.TeamName]
teamScripts = teamsScripts[*config.TeamName]
} else {
noTeamSoftwareInstallers, err := c.doGitOpsNoTeamSoftware(config, baseDir, appConfig, logFn, dryRun)
noTeamSoftwareInstallers, noTeamVPPApps, err := c.doGitOpsNoTeamSoftware(config, baseDir, appConfig, logFn, dryRun)
if err != nil {
return nil, err
}
teamSoftwareInstallers = noTeamSoftwareInstallers
teamVPPApps = noTeamVPPApps
teamScripts = teamsScripts["No team"]
}
}
err = c.doGitOpsPolicies(config, teamSoftwareInstallers, teamScripts, logFn, dryRun)
err = c.doGitOpsPolicies(config, teamSoftwareInstallers, teamVPPApps, teamScripts, logFn, dryRun)
if err != nil {
return nil, err
}
@ -1773,9 +1782,9 @@ func (c *Client) doGitOpsNoTeamSoftware(
appconfig *fleet.EnrichedAppConfig,
logFn func(format string, args ...interface{}),
dryRun bool,
) ([]fleet.SoftwarePackageResponse, error) {
) ([]fleet.SoftwarePackageResponse, []fleet.VPPAppResponse, error) {
if !config.IsNoTeam() || appconfig == nil || !appconfig.License.IsPremium() {
return nil, nil
return nil, nil, nil
}
// marshaling dance to get the macos_setup data - config.Controls.MacOSSetup
@ -1785,11 +1794,11 @@ func (c *Client) doGitOpsNoTeamSoftware(
// the untyped map.
b, err := json.Marshal(config.Controls.MacOSSetup)
if err != nil {
return nil, fmt.Errorf("applying software installers: json-encode controls.macos_setup: %w", err)
return nil, nil, fmt.Errorf("applying software installers: json-encode controls.macos_setup: %w", err)
}
var macOSSetup fleet.MacOSSetup
if err := json.Unmarshal(b, &macOSSetup); err != nil {
return nil, fmt.Errorf("applying software installers: json-decode controls.macos_setup: %w", err)
return nil, nil, fmt.Errorf("applying software installers: json-decode controls.macos_setup: %w", err)
}
// load the no-team macos_setup.script if any
@ -1797,14 +1806,14 @@ func (c *Client) doGitOpsNoTeamSoftware(
if macOSSetup.Script.Value != "" {
b, err := c.validateMacOSSetupScript(resolveApplyRelativePath(baseDir, macOSSetup.Script.Value))
if err != nil {
return nil, fmt.Errorf("applying no team macos_setup.script: %w", err)
return nil, nil, fmt.Errorf("applying no team macos_setup.script: %w", err)
}
macosSetupScript = &fileContent{Filename: filepath.Base(macOSSetup.Script.Value), Content: b}
}
noTeamSoftwareMacOSSetup, err := extractTeamOrNoTeamMacOSSetupSoftware(baseDir, macOSSetup.Software.Value)
if err != nil {
return nil, err
return nil, nil, err
}
var softwareInstallers []fleet.SoftwarePackageResponse
@ -1838,30 +1847,31 @@ func (c *Client) doGitOpsNoTeamSoftware(
}
if err := validateTeamOrNoTeamMacOSSetupSoftware(*config.TeamName, macOSSetup.Software.Value, packagesByPath, appsByAppID); err != nil {
return nil, err
return nil, nil, err
}
swPkgPayload, err := buildSoftwarePackagesPayload(packages, noTeamSoftwareMacOSSetup)
if err != nil {
return nil, fmt.Errorf("applying software installers: %w", err)
return nil, nil, fmt.Errorf("applying software installers: %w", err)
}
if macosSetupScript != nil {
logFn("[+] applying macos setup experience script for 'No team'\n")
if err := c.uploadMacOSSetupScript(macosSetupScript.Filename, macosSetupScript.Content, nil); err != nil {
return nil, fmt.Errorf("uploading setup experience script for No team: %w", err)
return nil, nil, fmt.Errorf("uploading setup experience script for No team: %w", err)
}
} else if err := c.deleteMacOSSetupScript(nil); err != nil {
return nil, fmt.Errorf("deleting setup experience script for No team: %w", err)
return nil, nil, fmt.Errorf("deleting setup experience script for No team: %w", err)
}
logFn("[+] applying %d software packages for 'No team'\n", len(swPkgPayload))
softwareInstallers, err = c.ApplyNoTeamSoftwareInstallers(swPkgPayload, fleet.ApplySpecOptions{DryRun: dryRun})
if err != nil {
return nil, fmt.Errorf("applying software installers: %w", err)
return nil, nil, fmt.Errorf("applying software installers: %w", err)
}
logFn("[+] applying %d app store apps for 'No team'\n", len(appsPayload))
if err := c.ApplyNoTeamAppStoreAppsAssociation(appsPayload, fleet.ApplySpecOptions{DryRun: dryRun}); err != nil {
return nil, fmt.Errorf("applying app store apps: %w", err)
vppApps, err := c.ApplyNoTeamAppStoreAppsAssociation(appsPayload, fleet.ApplySpecOptions{DryRun: dryRun})
if err != nil {
return nil, nil, fmt.Errorf("applying app store apps: %w", err)
}
if dryRun {
@ -1869,10 +1879,10 @@ func (c *Client) doGitOpsNoTeamSoftware(
} else {
logFn("[+] applied 'No Team' software packages\n")
}
return softwareInstallers, nil
return softwareInstallers, vppApps, nil
}
func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []fleet.SoftwarePackageResponse, teamScripts []fleet.ScriptResponse, logFn func(format string, args ...interface{}), dryRun bool) error {
func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []fleet.SoftwarePackageResponse, teamVPPApps []fleet.VPPAppResponse, teamScripts []fleet.ScriptResponse, logFn func(format string, args ...interface{}), dryRun bool) error {
var teamID *uint // Global policies (nil)
switch {
case config.TeamID != nil: // Team policies
@ -1882,7 +1892,8 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []
}
if teamID != nil {
// Get software titles of packages for the team.
softwareTitleURLs := make(map[string]uint)
softwareTitleIDsByInstallerURL := make(map[string]uint)
softwareTitleIDsByAppStoreAppID := make(map[string]uint)
for _, softwareInstaller := range teamSoftwareInstallers {
if softwareInstaller.TitleID == nil {
// Should not happen, but to not panic we just log a warning.
@ -1894,23 +1905,53 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []
logFn("[!] software installer without url: team_id=%d, title_id=%d\n", *teamID, *softwareInstaller.TitleID)
continue
}
softwareTitleURLs[softwareInstaller.URL] = *softwareInstaller.TitleID
softwareTitleIDsByInstallerURL[softwareInstaller.URL] = *softwareInstaller.TitleID
}
for _, vppApp := range teamVPPApps {
if vppApp.Platform != fleet.MacOSPlatform {
continue // ignore iPad/iPhone VPP apps as they aren't relevant for policies
}
if vppApp.TitleID == nil {
// Should not happen, but to not panic we just log a warning.
logFn("[!] VPP app without title id: team_id=%d, app_store_id=%s\n", *teamID, vppApp.AppStoreID)
continue
}
if vppApp.AppStoreID == "" {
// Should not happen because we previously applied apps via gitops, but to not panic we just log a warning.
logFn("[!] VPP app without app ID: team_id=%d, title_id=%d\n", *teamID, *vppApp.TitleID)
continue
}
softwareTitleIDsByAppStoreAppID[vppApp.AppStoreID] = *vppApp.TitleID
}
for i := range config.Policies {
config.Policies[i].SoftwareTitleID = ptr.Uint(0) // 0 unsets the installer
if config.Policies[i].InstallSoftware == nil {
continue
}
softwareTitleID, ok := softwareTitleURLs[config.Policies[i].InstallSoftwareURL]
if !ok {
// Should not happen because software packages are uploaded first.
if !dryRun {
logFn("[!] software URL without software title id: %s\n", config.Policies[i].InstallSoftwareURL)
if config.Policies[i].InstallSoftwareURL != "" {
softwareTitleID, ok := softwareTitleIDsByInstallerURL[config.Policies[i].InstallSoftwareURL]
if !ok {
// Should not happen because software packages are uploaded first.
if !dryRun {
logFn("[!] software URL without software title ID: %s\n", config.Policies[i].InstallSoftwareURL)
}
continue
}
continue
config.Policies[i].SoftwareTitleID = &softwareTitleID
}
if config.Policies[i].InstallSoftware.AppStoreID != "" {
softwareTitleID, ok := softwareTitleIDsByAppStoreAppID[config.Policies[i].InstallSoftware.AppStoreID]
if !ok {
// Should not happen because app store apps are uploaded first.
if !dryRun {
logFn("[!] software app store app ID without software title ID: %s\n", config.Policies[i].InstallSoftware.AppStoreID)
}
continue
}
config.Policies[i].SoftwareTitleID = &softwareTitleID
}
config.Policies[i].SoftwareTitleID = &softwareTitleID
}
// Get scripts for the team.

View file

@ -105,24 +105,29 @@ func (c *Client) ApplyTeamSoftwareInstallers(tmName string, softwareInstallers [
return c.applySoftwareInstallers(softwareInstallers, query, opts.DryRun)
}
func (c *Client) ApplyTeamAppStoreAppsAssociation(tmName string, vppBatchPayload []fleet.VPPBatchPayload, opts fleet.ApplySpecOptions) error {
func (c *Client) ApplyTeamAppStoreAppsAssociation(tmName string, vppBatchPayload []fleet.VPPBatchPayload, opts fleet.ApplySpecOptions) ([]fleet.VPPAppResponse, error) {
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return err
return nil, err
}
query.Add("team_name", tmName)
return c.applyAppStoreAppsAssociation(vppBatchPayload, query)
}
func (c *Client) ApplyNoTeamAppStoreAppsAssociation(vppBatchPayload []fleet.VPPBatchPayload, opts fleet.ApplySpecOptions) error {
func (c *Client) ApplyNoTeamAppStoreAppsAssociation(vppBatchPayload []fleet.VPPBatchPayload, opts fleet.ApplySpecOptions) ([]fleet.VPPAppResponse, error) {
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return err
return nil, err
}
return c.applyAppStoreAppsAssociation(vppBatchPayload, query)
}
func (c *Client) applyAppStoreAppsAssociation(vppBatchPayload []fleet.VPPBatchPayload, query url.Values) error {
func (c *Client) applyAppStoreAppsAssociation(vppBatchPayload []fleet.VPPBatchPayload, query url.Values) ([]fleet.VPPAppResponse, error) {
verb, path := "POST", "/api/latest/fleet/software/app_store_apps/batch"
return c.authenticatedRequestWithQuery(map[string]interface{}{"app_store_apps": vppBatchPayload}, verb, path, nil, query.Encode())
var appsResponse batchAssociateAppStoreAppsResponse
err := c.authenticatedRequestWithQuery(map[string]interface{}{"app_store_apps": vppBatchPayload}, verb, path, &appsResponse, query.Encode())
if err != nil {
return nil, err
}
return appsResponse.Apps, nil
}

View file

@ -778,7 +778,7 @@ func TestGitOpsErrors(t *testing.T) {
err = json.Unmarshal([]byte(tt.rawJSON), &config.OrgSettings)
require.NoError(t, err)
config.OrgSettings["secrets"] = []*fleet.EnrollSecret{}
_, err = client.DoGitOps(ctx, config, "/filename", nil, false, nil, nil, nil, nil)
_, err = client.DoGitOps(ctx, config, "/filename", nil, false, nil, nil, nil, nil, nil)
assert.ErrorContains(t, err, tt.wantErr)
})
}

View file

@ -10423,8 +10423,11 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
})
require.NoError(t, err)
var batchAssociateResponse batchAssociateAppStoreAppsResponse
// No vpp token set, but request is empty so it succeeds (clears VPP apps for the team).
s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusNoContent, "team_name", tmGood.Name)
s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
require.Len(t, batchAssociateResponse.Apps, 0)
// No vpp token set, try association
// FIXME
@ -10443,7 +10446,8 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)
// Remove all vpp associations from team with no members
s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusNoContent, "team_name", tmGood.Name)
s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
require.Len(t, batchAssociateResponse.Apps, 0)
// host with valid serial number
hValid, err := s.ds.NewHost(context.Background(), &fleet.Host{
@ -10468,7 +10472,8 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
require.Len(t, assoc, 0)
// Remove all vpp associations from team with no members
s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusNoContent, "team_name", tmGood.Name)
s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
require.Len(t, batchAssociateResponse.Apps, 0)
// Incorrect type check
incorrectTypes := struct {
@ -10498,7 +10503,10 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
require.Len(t, assoc, 0)
// Associating an app we own
s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}}}, http.StatusNoContent, "team_name", tmGood.Name)
s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID},
}}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
require.Len(t, batchAssociateResponse.Apps, 1)
assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
require.NoError(t, err)
@ -10519,15 +10527,16 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
require.Len(t, assoc, 1)
// Associating two apps we own
s.Do("POST",
s.DoJSON("POST",
batchURL,
batchAssociateAppStoreAppsRequest{
Apps: []fleet.VPPBatchPayload{
{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID},
{AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: true},
},
}, http.StatusNoContent, "team_name", tmGood.Name,
}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name,
)
require.Len(t, batchAssociateResponse.Apps, 4)
assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
require.NoError(t, err)
require.Len(t, assoc, 4)
@ -10544,15 +10553,16 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
// Reverse self-service associations
// Associating two apps we own
s.Do("POST",
s.DoJSON("POST",
batchURL,
batchAssociateAppStoreAppsRequest{
Apps: []fleet.VPPBatchPayload{
{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, SelfService: true},
{AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: false},
},
}, http.StatusNoContent, "team_name", tmGood.Name,
}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name,
)
require.Len(t, batchAssociateResponse.Apps, 4)
assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
require.NoError(t, err)
require.Len(t, assoc, 4)
@ -10575,10 +10585,11 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
}, assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform}])
// Associate an app with a team with no team members
s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}}}, http.StatusNoContent, "team_name", tmEmpty.Name)
s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}}}, http.StatusOK, &batchAssociateResponse, "team_name", tmEmpty.Name)
// Remove all vpp associations
s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusNoContent, "team_name", tmGood.Name)
s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
require.Len(t, batchAssociateResponse.Apps, 0)
assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
require.NoError(t, err)

View file

@ -767,25 +767,25 @@ func (b *batchAssociateAppStoreAppsRequest) DecodeBody(ctx context.Context, r io
}
type batchAssociateAppStoreAppsResponse struct {
Err error `json:"error,omitempty"`
Apps []fleet.VPPAppResponse `json:"app_store_apps"`
Err error `json:"error,omitempty"`
}
func (r batchAssociateAppStoreAppsResponse) error() error { return r.Err }
func (r batchAssociateAppStoreAppsResponse) Status() int { return http.StatusNoContent }
func batchAssociateAppStoreAppsEndpoint(ctx context.Context, request any, svc fleet.Service) (errorer, error) {
req := request.(*batchAssociateAppStoreAppsRequest)
if err := svc.BatchAssociateVPPApps(ctx, req.TeamName, req.Apps, req.DryRun); err != nil {
apps, err := svc.BatchAssociateVPPApps(ctx, req.TeamName, req.Apps, req.DryRun)
if err != nil {
return batchAssociateAppStoreAppsResponse{Err: err}, nil
}
return batchAssociateAppStoreAppsResponse{}, nil
return batchAssociateAppStoreAppsResponse{Apps: apps}, nil
}
func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, payloads []fleet.VPPBatchPayload, dryRun bool) error {
func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, payloads []fleet.VPPBatchPayload, dryRun bool) ([]fleet.VPPAppResponse, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
return fleet.ErrMissingLicense
return nil, fleet.ErrMissingLicense
}