diff --git a/changes/41324-support-labels-include-all-for-installers b/changes/41324-support-labels-include-all-for-installers new file mode 100644 index 0000000000..9d8b7b50d5 --- /dev/null +++ b/changes/41324-support-labels-include-all-for-installers @@ -0,0 +1 @@ +- Added support for `labels_include_all` conditional scoping for software installers and apps. diff --git a/cmd/fleetctl/fleetctl/generate_gitops.go b/cmd/fleetctl/fleetctl/generate_gitops.go index 7f47584f16..b45f73a005 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops.go +++ b/cmd/fleetctl/fleetctl/generate_gitops.go @@ -1831,6 +1831,10 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint, labels = softwareTitle.SoftwarePackage.LabelsExcludeAny labelKey = "labels_exclude_any" } + if len(softwareTitle.SoftwarePackage.LabelsIncludeAll) > 0 { + labels = softwareTitle.SoftwarePackage.LabelsIncludeAll + labelKey = "labels_include_all" + } if _, exists := setupSoftwareBySoftwareTitle[softwareTitle.ID]; exists { softwareSpec["setup_experience"] = true } @@ -1845,6 +1849,10 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint, labels = softwareTitle.AppStoreApp.LabelsExcludeAny labelKey = "labels_exclude_any" } + if len(softwareTitle.AppStoreApp.LabelsIncludeAll) > 0 { + labels = softwareTitle.AppStoreApp.LabelsIncludeAll + labelKey = "labels_include_all" + } if _, exists := setupSoftwareByPlatformAndAppID[platformAndAppID]; exists { softwareSpec["setup_experience"] = true } diff --git a/cmd/fleetctl/fleetctl/generate_gitops_test.go b/cmd/fleetctl/fleetctl/generate_gitops_test.go index 9f686bffaa..0e31570c6c 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops_test.go +++ b/cmd/fleetctl/fleetctl/generate_gitops_test.go @@ -451,9 +451,15 @@ func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTi return &fleet.SoftwareTitle{ ID: 6, AppStoreApp: &fleet.VPPAppStoreApp{ - VPPAppID: fleet.VPPAppID{AdamID: "com.example.setup-experience-software", Platform: fleet.AndroidPlatform}, - LabelsExcludeAny: []fleet.SoftwareScopeLabel{}, - SelfService: true, + VPPAppID: fleet.VPPAppID{AdamID: "com.example.setup-experience-software", Platform: fleet.AndroidPlatform}, + LabelsIncludeAll: []fleet.SoftwareScopeLabel{ + { + LabelName: "Label C", + }, { + LabelName: "Label D", + }, + }, + SelfService: true, }, IconUrl: ptr.String("/api/icon3.png"), }, nil @@ -466,6 +472,13 @@ func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTi AppStoreApp: &fleet.VPPAppStoreApp{ VPPAppID: fleet.VPPAppID{AdamID: "com.example.ios-auto-update", Platform: fleet.IOSPlatform}, SelfService: false, + LabelsIncludeAll: []fleet.SoftwareScopeLabel{ + { + LabelName: "Label C", + }, { + LabelName: "Label D", + }, + }, }, IconUrl: ptr.String("/api/icon4.png"), SoftwareAutoUpdateConfig: fleet.SoftwareAutoUpdateConfig{ @@ -502,6 +515,14 @@ func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTi ID: 9, Name: "My Windows FMA", SoftwarePackage: &fleet.SoftwareInstaller{ + LabelsIncludeAll: []fleet.SoftwareScopeLabel{ + { + LabelName: "Label A", + }, + { + LabelName: "Label B", + }, + }, InstallScript: "install", UninstallScript: "uninstall", SelfService: true, diff --git a/cmd/fleetctl/fleetctl/gitops.go b/cmd/fleetctl/fleetctl/gitops.go index e15c79f968..94ae537e1e 100644 --- a/cmd/fleetctl/fleetctl/gitops.go +++ b/cmd/fleetctl/fleetctl/gitops.go @@ -763,10 +763,16 @@ func getLabelUsage(config *spec.GitOps) (map[string][]LabelUsage, error) { } if len(softwarePackage.LabelsExcludeAny) > 0 { if len(labels) > 0 { - return nil, fmt.Errorf("Software package '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_exclude_any`.", softwarePackage.URL) + return nil, fmt.Errorf("Software package '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", softwarePackage.URL) } labels = softwarePackage.LabelsExcludeAny } + if len(softwarePackage.LabelsIncludeAll) > 0 { + if len(labels) > 0 { + return nil, fmt.Errorf("Software package '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", softwarePackage.URL) + } + labels = softwarePackage.LabelsIncludeAll + } updateLabelUsage(labels, softwarePackage.URL, "Software Package", result) } @@ -778,10 +784,16 @@ func getLabelUsage(config *spec.GitOps) (map[string][]LabelUsage, error) { } if len(vppApp.LabelsExcludeAny) > 0 { if len(labels) > 0 { - return nil, fmt.Errorf("App Store App '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_exclude_any`.", vppApp.AppStoreID) + return nil, fmt.Errorf("App Store App '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", vppApp.AppStoreID) } labels = vppApp.LabelsExcludeAny } + if len(vppApp.LabelsIncludeAll) > 0 { + if len(labels) > 0 { + return nil, fmt.Errorf("App Store App '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", vppApp.AppStoreID) + } + labels = vppApp.LabelsIncludeAll + } updateLabelUsage(labels, vppApp.AppStoreID, "App Store App", result) } @@ -792,10 +804,16 @@ func getLabelUsage(config *spec.GitOps) (map[string][]LabelUsage, error) { } if len(maintainedApp.LabelsExcludeAny) > 0 { if len(labels) > 0 { - return nil, fmt.Errorf("Fleet Maintained App '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_exclude_any`.", maintainedApp.Slug) + return nil, fmt.Errorf("Fleet Maintained App '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", maintainedApp.Slug) } labels = maintainedApp.LabelsExcludeAny } + if len(maintainedApp.LabelsIncludeAll) > 0 { + if len(labels) > 0 { + return nil, fmt.Errorf("Fleet Maintained App '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", maintainedApp.Slug) + } + labels = maintainedApp.LabelsIncludeAll + } updateLabelUsage(labels, maintainedApp.Slug, "Fleet Maintained App", result) } diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml index 72376119d0..6a2c1a1904 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml @@ -30,6 +30,9 @@ fleet_maintained_apps: path: ../lib/some-team/queries/my-fma-darwin-preinstallquery.yml self_service: true - slug: fma2/windows + labels_include_all: + - Label A + - Label B self_service: true app_store_apps: - app_store_id: com.example.team-software @@ -42,10 +45,16 @@ app_store_apps: self_service: true platform: darwin - app_store_id: com.example.setup-experience-software + labels_include_all: + - Label C + - Label D platform: android self_service: true setup_experience: true - app_store_id: com.example.ios-auto-update + labels_include_all: + - Label C + - Label D auto_update_enabled: true auto_update_window_start: "01:00" auto_update_window_end: "03:00" diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml index 0da799c2fa..49b0a9278e 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml @@ -113,6 +113,9 @@ software: - app_store_id: com.example.setup-experience-software icon: path: "../lib/team-a-👍/icons/my-setup-experience-app-android-icon.png" + labels_include_all: + - Label C + - Label D platform: android self_service: true setup_experience: true @@ -122,6 +125,9 @@ software: auto_update_window_start: "01:00" icon: path: "../lib/team-a-👍/icons/my-ios-auto-update-app-ios-icon.png" + labels_include_all: + - Label C + - Label D platform: ios fleet_maintained_apps: - categories: @@ -140,6 +146,9 @@ software: slug: fma1/darwin - icon: path: "../lib/team-a-👍/icons/my-windows-fma-windows-icon.png" + labels_include_all: + - Label A + - Label B self_service: true slug: fma2/windows packages: diff --git a/cmd/fleetctl/fleetctl/testdata/gitops/no_team_software_installer_valid_include_all.yml b/cmd/fleetctl/fleetctl/testdata/gitops/no_team_software_installer_valid_include_all.yml new file mode 100644 index 0000000000..95a39e8ae2 --- /dev/null +++ b/cmd/fleetctl/fleetctl/testdata/gitops/no_team_software_installer_valid_include_all.yml @@ -0,0 +1,19 @@ +name: No team +controls: +policies: +software: + packages: + - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb + install_script: + path: lib/install_ruby.sh + pre_install_query: + path: lib/query_ruby.yml + post_install_script: + path: lib/post_install_ruby.sh + uninstall_script: + path: lib/uninstall_ruby.sh + labels_include_all: + - a + - b + - url: ${SOFTWARE_INSTALLER_URL}/other.deb + self_service: true diff --git a/cmd/fleetctl/fleetctl/testdata/gitops/team_software_installer_valid_include_all.yml b/cmd/fleetctl/fleetctl/testdata/gitops/team_software_installer_valid_include_all.yml new file mode 100644 index 0000000000..5ffe5af234 --- /dev/null +++ b/cmd/fleetctl/fleetctl/testdata/gitops/team_software_installer_valid_include_all.yml @@ -0,0 +1,28 @@ +name: "${TEST_TEAM_NAME}" +team_settings: + secrets: + - secret: "ABC" + features: + enable_host_users: true + enable_software_inventory: true + host_expiry_settings: + host_expiry_enabled: true + host_expiry_window: 30 +agent_options: +controls: +policies: +queries: +software: + packages: + - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb + install_script: + path: lib/install_ruby.sh + pre_install_query: + path: lib/query_ruby_apply.yml + post_install_script: + path: lib/post_install_ruby.sh + labels_include_all: + - a + - b + - url: ${SOFTWARE_INSTALLER_URL}/other.deb + self_service: true diff --git a/cmd/fleetctl/fleetctl/testdata/gitops/team_vpp_valid_app_labels_include_all.yml b/cmd/fleetctl/fleetctl/testdata/gitops/team_vpp_valid_app_labels_include_all.yml new file mode 100644 index 0000000000..5486e9db6f --- /dev/null +++ b/cmd/fleetctl/fleetctl/testdata/gitops/team_vpp_valid_app_labels_include_all.yml @@ -0,0 +1,20 @@ +name: "${TEST_TEAM_NAME}" +team_settings: + secrets: + - secret: "ABC" + features: + enable_host_users: true + enable_software_inventory: true + host_expiry_settings: + host_expiry_enabled: true + host_expiry_window: 30 +agent_options: +controls: +policies: +queries: +software: + app_store_apps: + - app_store_id: "1" + labels_include_all: + - "label 1" + - "label 2" diff --git a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go index db515c4b1d..deb40eb803 100644 --- a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go +++ b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go @@ -329,6 +329,10 @@ team_settings: withLabelsExcludeAny = ` labels_exclude_any: - Label1 +` + withLabelsIncludeAll = ` + labels_include_all: + - Label1 ` ) @@ -392,6 +396,34 @@ team_settings: require.Len(t, meta.LabelsExcludeAny, 1) require.Equal(t, "Label1", meta.LabelsExcludeAny[0].LabelName) + // switch both to labels_include_all + err = os.WriteFile(noTeamFilePath, fmt.Appendf(nil, noTeamTemplate, withLabelsIncludeAll), 0o644) + require.NoError(t, err) + err = os.WriteFile(teamFile.Name(), fmt.Appendf(nil, teamTemplate, withLabelsIncludeAll, teamName), 0o644) + require.NoError(t, err) + + // Apply configs + s.assertDryRunOutputWithDeprecation(t, fleetctl.RunAppForTest(t, + []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), "--dry-run"}), true) + s.assertRealRunOutputWithDeprecation(t, fleetctl.RunAppForTest(t, + []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name()}), true) + + // the installer is now scoped by labels_include_all for no team + meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, noTeamTitleID, false) + require.NoError(t, err) + require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsExcludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, "Label1", meta.LabelsIncludeAll[0].LabelName) + + // the installer is now scoped by labels_include_all for team + meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) + require.NoError(t, err) + require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsExcludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, "Label1", meta.LabelsIncludeAll[0].LabelName) + // remove the label conditions err = os.WriteFile(noTeamFilePath, fmt.Appendf(nil, noTeamTemplate, emptyLabelsIncludeAny), 0o644) require.NoError(t, err) @@ -411,6 +443,7 @@ team_settings: require.Equal(t, noTeamTitleID, *meta.TitleID) require.Len(t, meta.LabelsExcludeAny, 0) require.Len(t, meta.LabelsIncludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) require.NoError(t, err) @@ -418,6 +451,7 @@ team_settings: require.Equal(t, teamTitleID, *meta.TitleID) require.Len(t, meta.LabelsExcludeAny, 0) require.Len(t, meta.LabelsIncludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) } func (s *enterpriseIntegrationGitopsTestSuite) TestNoTeamWebhookSettingsDeprecated() { @@ -944,6 +978,7 @@ team_settings: require.True(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsIncludeAll) } require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources) require.ElementsMatch(t, []string{"ios", "ipados"}, platforms) @@ -1000,6 +1035,7 @@ team_settings: require.NoError(t, err) require.False(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) + require.Empty(t, meta.LabelsIncludeAll) require.Len(t, meta.LabelsIncludeAny, 1) require.Equal(t, lbl.ID, meta.LabelsIncludeAny[0].LabelID) require.Empty(t, meta.InstallScript) // install script should be ignored for ipa apps @@ -1039,6 +1075,7 @@ team_settings: require.False(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsIncludeAll) } require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources) require.ElementsMatch(t, []string{"ios", "ipados"}, platforms) diff --git a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go index 49b9803667..cfa7774e48 100644 --- a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go +++ b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go @@ -916,6 +916,10 @@ software: ` emptyLabelsIncludeAny = ` labels_include_any: +` + withLabelsIncludeAll = ` + labels_include_all: + - Label1 ` teamTemplate = ` controls: @@ -996,6 +1000,34 @@ settings: require.Len(t, meta.LabelsExcludeAny, 1) require.Equal(t, "Label1", meta.LabelsExcludeAny[0].LabelName) + // switch both to labels_include_all + err = os.WriteFile(noTeamFilePath, fmt.Appendf(nil, noTeamTemplate, withLabelsIncludeAll), 0o644) + require.NoError(t, err) + err = os.WriteFile(teamFile.Name(), fmt.Appendf(nil, teamTemplate, withLabelsIncludeAll, teamName), 0o644) + require.NoError(t, err) + + // Apply configs + s.assertDryRunOutput(t, fleetctl.RunAppForTest(t, + []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), "--dry-run"})) + s.assertRealRunOutput(t, fleetctl.RunAppForTest(t, + []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name()})) + + // the installer is now scoped by labels_include_all for no team + meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, noTeamTitleID, false) + require.NoError(t, err) + require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsExcludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, "Label1", meta.LabelsIncludeAll[0].LabelName) + + // the installer is now scoped by labels_include_all for team + meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) + require.NoError(t, err) + require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsExcludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, "Label1", meta.LabelsIncludeAll[0].LabelName) + // remove the label conditions err = os.WriteFile(noTeamFilePath, []byte(fmt.Sprintf(noTeamTemplate, emptyLabelsIncludeAny)), 0o644) require.NoError(t, err) @@ -1015,6 +1047,7 @@ settings: require.Equal(t, noTeamTitleID, *meta.TitleID) require.Len(t, meta.LabelsExcludeAny, 0) require.Len(t, meta.LabelsIncludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) require.NoError(t, err) @@ -1022,6 +1055,7 @@ settings: require.Equal(t, teamTitleID, *meta.TitleID) require.Len(t, meta.LabelsExcludeAny, 0) require.Len(t, meta.LabelsIncludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) } func (s *enterpriseIntegrationGitopsTestSuite) TestDeletingNoTeamYAML() { @@ -2191,6 +2225,7 @@ settings: require.True(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsIncludeAll) } require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources) require.ElementsMatch(t, []string{"ios", "ipados"}, platforms) @@ -2247,6 +2282,7 @@ settings: require.NoError(t, err) require.False(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) + require.Empty(t, meta.LabelsIncludeAll) require.Len(t, meta.LabelsIncludeAny, 1) require.Equal(t, lbl.ID, meta.LabelsIncludeAny[0].LabelID) require.Empty(t, meta.InstallScript) // install script should be ignored for ipa apps @@ -2286,6 +2322,7 @@ settings: require.False(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsIncludeAll) } require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources) require.ElementsMatch(t, []string{"ios", "ipados"}, platforms) @@ -3999,3 +4036,204 @@ name: %s require.NoError(t, err) require.Len(t, titles, 0) } + +// TestFMALabelsIncludeAll tests that labels_include_all is correctly applied and +// cleared for Fleet Maintained Apps via gitops, for both no-team and a specific team. +func (s *enterpriseIntegrationGitopsTestSuite) TestFMALabelsIncludeAll() { + t := s.T() + ctx := context.Background() + + user := s.createGitOpsUser(t) + fleetctlConfig := s.createFleetctlConfig(t, user) + + lbl, err := s.DS.NewLabel(ctx, &fleet.Label{Name: "Label1" + t.Name(), Query: "SELECT 1"}) + require.NoError(t, err) + require.NotZero(t, lbl.ID) + + slug := fmt.Sprintf("foo%s/darwin", t.Name()) + + const ( + globalTemplate = ` +agent_options: +controls: +org_settings: + server_settings: + server_url: $FLEET_URL + org_info: + org_name: Fleet + secrets: +policies: +reports: +` + noTeamTemplate = `name: No team +controls: +policies: +software: + fleet_maintained_apps: + - slug: %s +%s +` + teamTemplate = ` +controls: +software: + fleet_maintained_apps: + - slug: %s +%s +reports: +policies: +agent_options: +name: %s +settings: + secrets: [{"secret":"enroll_secret"}] +` + ) + const noLabels = "" + + withLabelsIncludeAll := fmt.Sprintf(` + labels_include_all: + - %s +`, lbl.Name) + + globalFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = globalFile.WriteString(globalTemplate) + require.NoError(t, err) + err = globalFile.Close() + require.NoError(t, err) + + noTeamFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = fmt.Fprintf(noTeamFile, noTeamTemplate, slug, withLabelsIncludeAll) + require.NoError(t, err) + err = noTeamFile.Close() + require.NoError(t, err) + noTeamFilePath := filepath.Join(filepath.Dir(noTeamFile.Name()), "no-team.yml") + err = os.Rename(noTeamFile.Name(), noTeamFilePath) + require.NoError(t, err) + + teamName := uuid.NewString() + teamFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = fmt.Fprintf(teamFile, teamTemplate, slug, withLabelsIncludeAll, teamName) + require.NoError(t, err) + err = teamFile.Close() + require.NoError(t, err) + + // Set the required environment variables + t.Setenv("FLEET_URL", s.Server.URL) + testing_utils.StartSoftwareInstallerServer(t) + + // Mock server to serve FMA installer bytes + installerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("foo")) + })) + defer installerServer.Close() + + // Mock server to serve the FMA manifest + manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + versions := []*ma.FMAManifestApp{ + { + Version: "1.0", + Queries: ma.FMAQueries{ + Exists: "SELECT 1 FROM osquery_info;", + }, + InstallerURL: installerServer.URL + "/foo.pkg", + InstallScriptRef: "fooscript", + UninstallScriptRef: "fooscript", + SHA256: "no_check", + }, + } + manifest := ma.FMAManifestFile{ + Versions: versions, + Refs: map[string]string{"fooscript": "echo hello"}, + } + err := json.NewEncoder(w).Encode(manifest) + require.NoError(t, err) + })) + defer manifestServer.Close() + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL, t) + + // Insert the FMA record so gitops can resolve the slug + mysql.ExecAdhocSQL(t, s.DS, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, + `INSERT INTO fleet_maintained_apps (name, slug, platform, unique_identifier) + VALUES (?, ?, 'darwin', ?)`, "foo"+t.Name(), slug, `com.example.foo`+t.Name()) + return err + }) + + // Apply configs — dry-run first, then real run + s.assertDryRunOutput(t, fleetctl.RunAppForTest(t, []string{ + "gitops", "--config", fleetctlConfig.Name(), + "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), + "--dry-run", + })) + s.assertRealRunOutput(t, fleetctl.RunAppForTest(t, []string{ + "gitops", "--config", fleetctlConfig.Name(), + "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), + })) + + // Retrieve the team so we have its ID + team, err := s.DS.TeamByName(ctx, teamName) + require.NoError(t, err) + + // Locate the FMA installer for no-team and assert labels_include_all is set + noTeamTitles, _, _, err := s.DS.ListSoftwareTitles(ctx, + fleet.SoftwareTitleListOptions{AvailableForInstall: true, TeamID: ptr.Uint(0)}, + fleet.TeamFilter{User: test.UserAdmin}) + require.NoError(t, err) + require.Len(t, noTeamTitles, 1) + noTeamTitleID := noTeamTitles[0].ID + + noTeamMeta, err := s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, noTeamTitleID, false) + require.NoError(t, err) + require.Empty(t, noTeamMeta.LabelsIncludeAny) + require.Empty(t, noTeamMeta.LabelsExcludeAny) + require.Len(t, noTeamMeta.LabelsIncludeAll, 1) + require.Equal(t, lbl.Name, noTeamMeta.LabelsIncludeAll[0].LabelName) + + // Locate the FMA installer for the team and assert labels_include_all is set + teamTitles, _, _, err := s.DS.ListSoftwareTitles(ctx, + fleet.SoftwareTitleListOptions{TeamID: &team.ID}, + fleet.TeamFilter{User: test.UserAdmin}) + require.NoError(t, err) + require.Len(t, teamTitles, 1) + teamTitleID := teamTitles[0].ID + + teamMeta, err := s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) + require.NoError(t, err) + require.Empty(t, teamMeta.LabelsIncludeAny) + require.Empty(t, teamMeta.LabelsExcludeAny) + require.Len(t, teamMeta.LabelsIncludeAll, 1) + require.Equal(t, lbl.Name, teamMeta.LabelsIncludeAll[0].LabelName) + + // Now re-apply without labels_include_all and confirm they are cleared + err = os.WriteFile(noTeamFilePath, fmt.Appendf(nil, noTeamTemplate, slug, noLabels), 0o644) + require.NoError(t, err) + err = os.WriteFile(teamFile.Name(), fmt.Appendf(nil, teamTemplate, slug, noLabels, teamName), 0o644) + require.NoError(t, err) + + s.assertDryRunOutput(t, fleetctl.RunAppForTest(t, []string{ + "gitops", "--config", fleetctlConfig.Name(), + "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), + "--dry-run", + })) + s.assertRealRunOutput(t, fleetctl.RunAppForTest(t, []string{ + "gitops", "--config", fleetctlConfig.Name(), + "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), + })) + + // Labels should now be empty for no-team + noTeamMeta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, noTeamTitleID, false) + require.NoError(t, err) + require.Empty(t, noTeamMeta.LabelsIncludeAny) + require.Empty(t, noTeamMeta.LabelsExcludeAny) + require.Empty(t, noTeamMeta.LabelsIncludeAll) + + // Labels should now be empty for the team + teamMeta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) + require.NoError(t, err) + require.Empty(t, teamMeta.LabelsIncludeAny) + require.Empty(t, teamMeta.LabelsExcludeAny) + require.Empty(t, teamMeta.LabelsIncludeAll) +} diff --git a/cmd/fleetctl/integrationtest/gitops/software_test.go b/cmd/fleetctl/integrationtest/gitops/software_test.go index 00fd71c0ca..87f53ac568 100644 --- a/cmd/fleetctl/integrationtest/gitops/software_test.go +++ b/cmd/fleetctl/integrationtest/gitops/software_test.go @@ -54,10 +54,11 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) { }, { "testdata/gitops/team_software_installer_invalid_both_include_exclude.yml", - `only one of "labels_exclude_any" or "labels_include_any" can be specified`, + `only one of "labels_include_all", "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_valid_include_all.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.", @@ -360,10 +361,11 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) { }, { "testdata/gitops/no_team_software_installer_invalid_both_include_exclude.yml", - `only one of "labels_exclude_any" or "labels_include_any" can be specified`, + `only one of "labels_include_all", "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_valid_include_all.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.", @@ -505,6 +507,10 @@ func TestGitOpsTeamVPPApps(t *testing.T) { "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_valid_app_labels_include_all.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), @@ -517,7 +523,7 @@ func TestGitOpsTeamVPPApps(t *testing.T) { }, { "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), + `only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified for app store app`, time.Now().Add(24 * time.Hour), map[string]uint{}, }, } diff --git a/ee/server/service/in_house_apps.go b/ee/server/service/in_house_apps.go index 610b8ff021..d6a7f90c6c 100644 --- a/ee/server/service/in_house_apps.go +++ b/ee/server/service/in_house_apps.go @@ -35,7 +35,7 @@ func (svc *Service) updateInHouseAppInstaller(ctx context.Context, payload *flee payload.InstallerID = existingInstaller.InstallerID - _, validatedLabels, err := ValidateSoftwareLabelsForUpdate(ctx, svc, existingInstaller, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + _, validatedLabels, err := ValidateSoftwareLabelsForUpdate(ctx, svc, existingInstaller, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating software labels for update") } @@ -127,13 +127,14 @@ func (svc *Service) updateInHouseAppInstaller(ctx context.Context, payload *flee // now that the payload has been updated with any patches, we can set the // final fields of the activity - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels( - existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromSoftwareScopeLabels( + existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny, existingInstaller.LabelsIncludeAll) if payload.ValidatedLabels != nil { - actLabelsIncl, actLabelsExcl = activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll = activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) } - activity.LabelsIncludeAny = actLabelsIncl - activity.LabelsExcludeAny = actLabelsExcl + activity.LabelsIncludeAny = actLabelsInclAny + activity.LabelsExcludeAny = actLabelsExclAny + activity.LabelsIncludeAll = actLabelsInclAll if err := svc.NewActivity(ctx, vc.User, activity); err != nil { return nil, ctxerr.Wrap(ctx, err, "creating activity for edited in house app") } diff --git a/ee/server/service/maintained_apps.go b/ee/server/service/maintained_apps.go index f879cd910c..6668b965e0 100644 --- a/ee/server/service/maintained_apps.go +++ b/ee/server/service/maintained_apps.go @@ -26,7 +26,7 @@ func (svc *Service) AddFleetMaintainedApp( appID uint, installScript, preInstallQuery, postInstallScript, uninstallScript string, selfService bool, automaticInstall bool, - labelsIncludeAny, labelsExcludeAny []string, + labelsIncludeAny, labelsExcludeAny, labelsIncludeAll []string, ) (titleID uint, err error) { if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil { return 0, err @@ -38,7 +38,7 @@ func (svc *Service) AddFleetMaintainedApp( } // validate labels before we do anything else - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, labelsIncludeAny, labelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, labelsIncludeAny, labelsExcludeAny, labelsIncludeAll) if err != nil { return 0, ctxerr.Wrap(ctx, err, "validating software labels") } @@ -205,7 +205,7 @@ func (svc *Service) AddFleetMaintainedApp( teamName = &t.Name } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeAddedSoftware{ SoftwareTitle: payload.Title, SoftwarePackage: payload.Filename, @@ -213,8 +213,9 @@ func (svc *Service) AddFleetMaintainedApp( TeamID: payload.TeamID, SelfService: payload.SelfService, SoftwareTitleID: titleID, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, }); err != nil { return 0, ctxerr.Wrap(ctx, err, "creating activity for added software") } diff --git a/ee/server/service/maintained_apps_test.go b/ee/server/service/maintained_apps_test.go index 71f4e9b02c..e05625b58d 100644 --- a/ee/server/service/maintained_apps_test.go +++ b/ee/server/service/maintained_apps_test.go @@ -337,7 +337,7 @@ func TestAddFleetMaintainedApp(t *testing.T) { authCtx := authz_ctx.AuthorizationContext{} ctx := authz_ctx.NewContext(context.Background(), &authCtx) ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) - _, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil) + _, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil, nil) require.ErrorContains(t, err, "forced error to short-circuit storage and activity creation") require.True(t, ds.MatchOrCreateSoftwareInstallerFuncInvoked) @@ -417,7 +417,7 @@ func TestExtractMaintainedAppVersionWhenLatest(t *testing.T) { authCtx := authz_ctx.AuthorizationContext{} ctx := authz_ctx.NewContext(context.Background(), &authCtx) ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) - _, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil) + _, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil, nil) require.ErrorContains(t, err, "forced error to short-circuit storage and activity creation") require.True(t, ds.MatchOrCreateSoftwareInstallerFuncInvoked) diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 8a17556069..377a030103 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -54,7 +54,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. } // validate labels before we do anything else - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, payload.TeamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, payload.TeamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating software labels") } @@ -148,7 +148,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. teamName = &t.Name } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeAddedSoftware{ SoftwareTitle: payload.Title, SoftwarePackage: payload.Filename, @@ -156,8 +156,9 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. TeamID: payload.TeamID, SelfService: payload.SelfService, SoftwareTitleID: titleID, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, }); err != nil { return nil, ctxerr.Wrap(ctx, err, "creating activity for added software") } @@ -195,24 +196,35 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. return addedInstaller, nil } -func ValidateSoftwareLabels(ctx context.Context, svc fleet.Service, teamID *uint, labelsIncludeAny, labelsExcludeAny []string) (*fleet.LabelIdentsWithScope, error) { +func ValidateSoftwareLabels(ctx context.Context, svc fleet.Service, teamID *uint, labelsIncludeAny, labelsExcludeAny, labelsIncludeAll []string) (*fleet.LabelIdentsWithScope, error) { if authctx, ok := authz_ctx.FromContext(ctx); !ok { return nil, fleet.NewAuthRequiredError("validate software labels: missing authorization context") } else if !authctx.Checked() { return nil, fleet.NewAuthRequiredError("validate software labels: method requires previous authorization") } + var count int + for _, set := range [][]string{labelsIncludeAny, labelsExcludeAny, labelsIncludeAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { + return nil, &fleet.BadRequestError{Message: `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included.`} + } + var names []string var scope fleet.LabelScope switch { - case len(labelsIncludeAny) > 0 && len(labelsExcludeAny) > 0: - return nil, &fleet.BadRequestError{Message: `Only one of "labels_include_any" or "labels_exclude_any" can be included.`} case len(labelsIncludeAny) > 0: names = labelsIncludeAny scope = fleet.LabelScopeIncludeAny case len(labelsExcludeAny) > 0: names = labelsExcludeAny scope = fleet.LabelScopeExcludeAny + case len(labelsIncludeAll) > 0: + names = labelsIncludeAll + scope = fleet.LabelScopeIncludeAll } if len(names) == 0 { @@ -404,7 +416,7 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet. dirty["SelfService"] = true } - shouldUpdateLabels, validatedLabels, err := ValidateSoftwareLabelsForUpdate(ctx, svc, existingInstaller, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + shouldUpdateLabels, validatedLabels, err := ValidateSoftwareLabelsForUpdate(ctx, svc, existingInstaller, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating software labels for update") } @@ -664,13 +676,14 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet. // now that the payload has been updated with any patches, we can set the // final fields of the activity - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels( - existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromSoftwareScopeLabels( + existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny, existingInstaller.LabelsIncludeAll) if payload.ValidatedLabels != nil { - actLabelsIncl, actLabelsExcl = activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll = activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) } - activity.LabelsIncludeAny = actLabelsIncl - activity.LabelsExcludeAny = actLabelsExcl + activity.LabelsIncludeAny = actLabelsInclAny + activity.LabelsExcludeAny = actLabelsExclAny + activity.LabelsIncludeAll = actLabelsInclAll if payload.SelfService != nil { activity.SelfService = *payload.SelfService } @@ -712,7 +725,7 @@ func (svc *Service) validateEmbeddedSecretsOnScript(ctx context.Context, scriptN return argErr } -func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, existingInstaller *fleet.SoftwareInstaller, includeAny, excludeAny []string) (shouldUpdate bool, validatedLabels *fleet.LabelIdentsWithScope, err error) { +func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, existingInstaller *fleet.SoftwareInstaller, includeAny, excludeAny, includeAll []string) (shouldUpdate bool, validatedLabels *fleet.LabelIdentsWithScope, err error) { if authctx, ok := authz_ctx.FromContext(ctx); !ok { return false, nil, fleet.NewAuthRequiredError("batch validate labels: missing authorization context") } else if !authctx.Checked() { @@ -723,16 +736,12 @@ func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, exi return false, nil, errors.New("existing installer must be provided") } - if len(existingInstaller.LabelsIncludeAny) > 0 && len(existingInstaller.LabelsExcludeAny) > 0 { - return false, nil, errors.New("existing installer must have only one label scope") - } - - if includeAny == nil && excludeAny == nil { + if includeAny == nil && excludeAny == nil && includeAll == nil { // nothing to do return false, nil, nil } - incoming, err := ValidateSoftwareLabels(ctx, svc, existingInstaller.TeamID, includeAny, excludeAny) + incoming, err := ValidateSoftwareLabels(ctx, svc, existingInstaller.TeamID, includeAny, excludeAny, includeAll) if err != nil { return false, nil, err } @@ -746,6 +755,9 @@ func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, exi case len(existingInstaller.LabelsExcludeAny) > 0: prevScope = fleet.LabelScopeExcludeAny prevLabels = existingInstaller.LabelsExcludeAny + case len(existingInstaller.LabelsIncludeAll) > 0: + prevScope = fleet.LabelScopeIncludeAll + prevLabels = existingInstaller.LabelsIncludeAll } prevByName := make(map[string]fleet.LabelIdent, len(prevLabels)) @@ -855,7 +867,7 @@ func (svc *Service) deleteVPPApp(ctx context.Context, teamID *uint, meta *fleet. teamName = &t.Name } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny, meta.LabelsIncludeAll) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityDeletedAppStoreApp{ AppStoreID: meta.AdamID, @@ -863,8 +875,9 @@ func (svc *Service) deleteVPPApp(ctx context.Context, teamID *uint, meta *fleet. TeamName: teamName, TeamID: teamID, Platform: meta.Platform, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, SoftwareIconURL: meta.IconURL, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for deleted VPP app") @@ -928,15 +941,16 @@ func (svc *Service) deleteSoftwareInstaller(ctx context.Context, meta *fleet.Sof teamName = &t.Name } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny, meta.LabelsIncludeAll) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeDeletedSoftware{ SoftwareTitle: meta.SoftwareTitle, SoftwarePackage: meta.Name, TeamName: teamName, TeamID: meta.TeamID, SelfService: meta.SelfService, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, SoftwareIconURL: meta.IconUrl, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for deleted software") @@ -2030,7 +2044,7 @@ func (svc *Service) BatchSetSoftwareInstallers( } } if !dryRun { - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return "", err } @@ -2288,6 +2302,7 @@ func (svc *Service) softwareBatchUpload( InstallDuringSetup: p.InstallDuringSetup, LabelsIncludeAny: p.LabelsIncludeAny, LabelsExcludeAny: p.LabelsExcludeAny, + LabelsIncludeAll: p.LabelsIncludeAll, ValidatedLabels: p.ValidatedLabels, Categories: p.Categories, DisplayName: p.DisplayName, @@ -3074,12 +3089,11 @@ func UninstallSoftwareMigration( return nil } -func activitySoftwareLabelsFromValidatedLabels(validatedLabels *fleet.LabelIdentsWithScope) (include, exclude []fleet.ActivitySoftwareLabel) { +func activitySoftwareLabelsFromValidatedLabels(validatedLabels *fleet.LabelIdentsWithScope) (includeAny, excludeAny, includeAll []fleet.ActivitySoftwareLabel) { if validatedLabels == nil || len(validatedLabels.ByName) == 0 { - return nil, nil + return nil, nil, nil } - excludeAny := validatedLabels.LabelScope == fleet.LabelScopeExcludeAny labels := make([]fleet.ActivitySoftwareLabel, 0, len(validatedLabels.ByName)) for _, lbl := range validatedLabels.ByName { labels = append(labels, fleet.ActivitySoftwareLabel{ @@ -3087,26 +3101,35 @@ func activitySoftwareLabelsFromValidatedLabels(validatedLabels *fleet.LabelIdent Name: lbl.LabelName, }) } - if excludeAny { - exclude = labels - } else { - include = labels + switch validatedLabels.LabelScope { + case fleet.LabelScopeIncludeAny: + includeAny = labels + case fleet.LabelScopeExcludeAny: + excludeAny = labels + case fleet.LabelScopeIncludeAll: + includeAll = labels } - return include, exclude + return includeAny, excludeAny, includeAll } -func activitySoftwareLabelsFromSoftwareScopeLabels(includeScopeLabels, excludeScopeLabels []fleet.SoftwareScopeLabel) (include, exclude []fleet.ActivitySoftwareLabel) { - for _, label := range includeScopeLabels { - include = append(include, fleet.ActivitySoftwareLabel{ +func activitySoftwareLabelsFromSoftwareScopeLabels(includeAnyScopeLabels, excludeAnyScopeLabels, includeAllScopeLabels []fleet.SoftwareScopeLabel) (includeAny, excludeAny, includeAll []fleet.ActivitySoftwareLabel) { + for _, label := range includeAnyScopeLabels { + includeAny = append(includeAny, fleet.ActivitySoftwareLabel{ ID: label.LabelID, Name: label.LabelName, }) } - for _, label := range excludeScopeLabels { - exclude = append(exclude, fleet.ActivitySoftwareLabel{ + for _, label := range excludeAnyScopeLabels { + excludeAny = append(excludeAny, fleet.ActivitySoftwareLabel{ ID: label.LabelID, Name: label.LabelName, }) } - return include, exclude + for _, label := range includeAllScopeLabels { + includeAll = append(includeAll, fleet.ActivitySoftwareLabel{ + ID: label.LabelID, + Name: label.LabelName, + }) + } + return includeAny, excludeAny, includeAll } diff --git a/ee/server/service/software_title_icons.go b/ee/server/service/software_title_icons.go index ab887499b3..54adb7a533 100644 --- a/ee/server/service/software_title_icons.go +++ b/ee/server/service/software_title_icons.go @@ -211,6 +211,7 @@ func generateEditActivityForSoftwareTitleIcon(ctx context.Context, svc *Service, SoftwareIconURL: &iconUrl, LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny, LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny, + LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for software title icon") } @@ -228,6 +229,7 @@ func generateEditActivityForSoftwareTitleIcon(ctx context.Context, svc *Service, SoftwareIconURL: &iconUrl, LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny, LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny, + LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll, SoftwareTitleID: activityDetailsForSoftwareTitleIcon.SoftwareTitleID, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for software title icon") @@ -246,6 +248,7 @@ func generateEditActivityForSoftwareTitleIcon(ctx context.Context, svc *Service, SoftwareIconURL: &iconUrl, LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny, LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny, + LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll, SoftwareTitleID: activityDetailsForSoftwareTitleIcon.SoftwareTitleID, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for software title icon") diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index a003ab1f0d..08ad7e148d 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -99,6 +99,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, Platform: fleet.MacOSPlatform, LabelsExcludeAny: payload.LabelsExcludeAny, LabelsIncludeAny: payload.LabelsIncludeAny, + LabelsIncludeAll: payload.LabelsIncludeAll, Categories: payload.Categories, DisplayName: payload.DisplayName, AutoUpdateEnabled: payload.AutoUpdateEnabled, @@ -112,6 +113,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, Platform: fleet.IOSPlatform, LabelsExcludeAny: payload.LabelsExcludeAny, LabelsIncludeAny: payload.LabelsIncludeAny, + LabelsIncludeAll: payload.LabelsIncludeAll, Categories: payload.Categories, DisplayName: payload.DisplayName, AutoUpdateEnabled: payload.AutoUpdateEnabled, @@ -125,6 +127,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, Platform: fleet.IPadOSPlatform, LabelsExcludeAny: payload.LabelsExcludeAny, LabelsIncludeAny: payload.LabelsIncludeAny, + LabelsIncludeAll: payload.LabelsIncludeAll, Categories: payload.Categories, DisplayName: payload.DisplayName, AutoUpdateEnabled: payload.AutoUpdateEnabled, @@ -141,6 +144,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, Platform: payload.Platform, LabelsExcludeAny: payload.LabelsExcludeAny, LabelsIncludeAny: payload.LabelsIncludeAny, + LabelsIncludeAll: payload.LabelsIncludeAll, Categories: payload.Categories, DisplayName: payload.DisplayName, Configuration: payload.Configuration, @@ -184,7 +188,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, } } - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating software labels for batch adding vpp app") } @@ -578,7 +582,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee fmt.Sprintf("platform must be one of '%s', '%s', '%s', or '%s'", fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform, fleet.AndroidPlatform)) } - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, appID.LabelsIncludeAny, appID.LabelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, appID.LabelsIncludeAny, appID.LabelsExcludeAny, appID.LabelsIncludeAll) if err != nil { return 0, ctxerr.Wrap(ctx, err, "validating software labels for adding vpp app") } @@ -757,7 +761,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee } } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(addedApp.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromValidatedLabels(addedApp.ValidatedLabels) act := fleet.ActivityAddedAppStoreApp{ AppStoreID: app.AdamID, @@ -767,8 +771,9 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee SoftwareTitleId: addedApp.TitleID, TeamID: teamID, SelfService: app.SelfService, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, Configuration: app.Configuration, } @@ -912,9 +917,9 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID } var validatedLabels *fleet.LabelIdentsWithScope - if payload.LabelsExcludeAny != nil || payload.LabelsIncludeAny != nil { + if payload.LabelsExcludeAny != nil || payload.LabelsIncludeAny != nil || payload.LabelsIncludeAll != nil { var err error - validatedLabels, err = ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + validatedLabels, err = ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: validating software labels") } @@ -995,6 +1000,13 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID for _, l := range meta.LabelsIncludeAny { existingLabels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID} } + + case len(meta.LabelsIncludeAll) > 0: + existingLabels.LabelScope = fleet.LabelScopeIncludeAll + existingLabels.ByName = make(map[string]fleet.LabelIdent, len(meta.LabelsIncludeAll)) + for _, l := range meta.LabelsIncludeAll { + existingLabels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID} + } } var labelsChanged bool if validatedLabels != nil { @@ -1066,7 +1078,7 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID } } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(validatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromValidatedLabels(validatedLabels) displayNameVal := ptr.ValOrZero(payload.DisplayName) @@ -1078,8 +1090,9 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID SoftwareTitle: meta.Name, AppStoreID: meta.AdamID, Platform: meta.Platform, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, SoftwareIconURL: meta.IconURL, SoftwareDisplayName: displayNameVal, Configuration: appToWrite.Configuration, diff --git a/ee/server/service/vpp_test.go b/ee/server/service/vpp_test.go index 74ef2cfa0c..b81184ccda 100644 --- a/ee/server/service/vpp_test.go +++ b/ee/server/service/vpp_test.go @@ -32,6 +32,7 @@ func TestBatchAssociateVPPApps(t *testing.T) { AppStoreID: "my-fake-app", LabelsExcludeAny: []string{}, LabelsIncludeAny: []string{}, + LabelsIncludeAll: []string{}, Categories: []string{}, Platform: fleet.MacOSPlatform, }, @@ -44,6 +45,7 @@ func TestBatchAssociateVPPApps(t *testing.T) { AppStoreID: "my-fake-app", LabelsExcludeAny: []string{}, LabelsIncludeAny: []string{}, + LabelsIncludeAll: []string{}, Categories: []string{}, Platform: fleet.MacOSPlatform, }, @@ -70,6 +72,7 @@ func TestBatchAssociateVPPApps(t *testing.T) { AppStoreID: pkg, LabelsExcludeAny: []string{}, LabelsIncludeAny: []string{}, + LabelsIncludeAll: []string{}, Categories: []string{}, Platform: fleet.AndroidPlatform, }, @@ -82,6 +85,7 @@ func TestBatchAssociateVPPApps(t *testing.T) { AppStoreID: pkg, LabelsExcludeAny: []string{}, LabelsIncludeAny: []string{}, + LabelsIncludeAll: []string{}, Categories: []string{}, Platform: fleet.AndroidPlatform, }, diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go index 0c2d2dd97d..b23ba1d4b8 100644 --- a/pkg/spec/gitops.go +++ b/pkg/spec/gitops.go @@ -251,6 +251,7 @@ func (spec SoftwarePackage) HydrateToPackageLevel(packageLevel fleet.SoftwarePac packageLevel.Categories = spec.Categories packageLevel.LabelsIncludeAny = spec.LabelsIncludeAny packageLevel.LabelsExcludeAny = spec.LabelsExcludeAny + packageLevel.LabelsIncludeAll = spec.LabelsIncludeAll packageLevel.InstallDuringSetup = spec.InstallDuringSetup packageLevel.SelfService = spec.SelfService @@ -1605,8 +1606,14 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin continue } - if len(item.LabelsExcludeAny) > 0 && len(item.LabelsIncludeAny) > 0 { - multiError = multierror.Append(multiError, fmt.Errorf(`only one of "labels_exclude_any" or "labels_include_any" can be specified for app store app %q`, item.AppStoreID)) + var count int + for _, set := range [][]string{item.LabelsExcludeAny, item.LabelsIncludeAny, item.LabelsIncludeAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { + multiError = multierror.Append(multiError, fmt.Errorf(`only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified for app store app %q`, item.AppStoreID)) continue } @@ -1626,8 +1633,14 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin continue } - if len(maintainedAppSpec.LabelsExcludeAny) > 0 && len(maintainedAppSpec.LabelsIncludeAny) > 0 { - multiError = multierror.Append(multiError, fmt.Errorf(`only one of "labels_exclude_any" or "labels_include_any" can be specified for fleet maintained app %q`, maintainedAppSpec.Slug)) + var count int + for _, set := range [][]string{maintainedAppSpec.LabelsExcludeAny, maintainedAppSpec.LabelsIncludeAny, maintainedAppSpec.LabelsIncludeAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { + multiError = multierror.Append(multiError, fmt.Errorf(`only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified for fleet maintained app %q`, maintainedAppSpec.Slug)) continue } @@ -1752,10 +1765,18 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin continue } } - if len(softwarePackageSpec.LabelsExcludeAny) > 0 && len(softwarePackageSpec.LabelsIncludeAny) > 0 { - multiError = multierror.Append(multiError, fmt.Errorf(`only one of "labels_exclude_any" or "labels_include_any" can be specified for software URL %q`, softwarePackageSpec.URL)) + + var count int + for _, set := range [][]string{softwarePackageSpec.LabelsExcludeAny, softwarePackageSpec.LabelsIncludeAny, softwarePackageSpec.LabelsIncludeAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { + multiError = multierror.Append(multiError, fmt.Errorf(`only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified for software URL %q`, softwarePackageSpec.URL)) continue } + if softwarePackageSpec.SHA256 != "" && !validSHA256Value.MatchString(softwarePackageSpec.SHA256) { multiError = multierror.Append(multiError, fmt.Errorf("hash_sha256 value %q must be a valid lower-case hex-encoded (64-character) SHA-256 hash value", softwarePackageSpec.SHA256)) continue diff --git a/pkg/spec/gitops_test.go b/pkg/spec/gitops_test.go index 1fbe81b735..72778685df 100644 --- a/pkg/spec/gitops_test.go +++ b/pkg/spec/gitops_test.go @@ -211,10 +211,12 @@ func TestValidGitOpsYaml(t *testing.T) { assert.Contains(t, pkg.LabelsIncludeAny, "a") assert.Contains(t, pkg.Categories, "Communication") assert.Empty(t, pkg.LabelsExcludeAny) + assert.Empty(t, pkg.LabelsIncludeAll) } else { assert.Empty(t, pkg.UninstallScript.Path) assert.Contains(t, pkg.LabelsExcludeAny, "a") assert.Empty(t, pkg.LabelsIncludeAny) + assert.Empty(t, pkg.LabelsIncludeAll) } } require.Len(t, gitops.Software.FleetMaintainedApps, 2) @@ -2417,7 +2419,7 @@ agent_options: org_settings: server_settings: org_info: - secrets: + secrets: controls: apple_settings: configuration_profiles: @@ -2445,7 +2447,7 @@ agent_options: org_settings: server_settings: org_info: - secrets: + secrets: controls: apple_settings: configuration_profiles: @@ -2474,7 +2476,7 @@ agent_options: org_settings: server_settings: org_info: - secrets: + secrets: controls: windows_settings: configuration_profiles: @@ -2503,7 +2505,7 @@ agent_options: org_settings: server_settings: org_info: - secrets: + secrets: controls: android_settings: configuration_profiles: @@ -2532,10 +2534,10 @@ agent_options: org_settings: server_settings: org_info: - secrets: + secrets: controls: setup_experience: - macos_setup: + macos_setup: ` yamlPath := filepath.Join(dir, "gitops.yml") require.NoError(t, os.WriteFile(yamlPath, []byte(config), 0o644)) diff --git a/server/datastore/mysql/in_house_apps.go b/server/datastore/mysql/in_house_apps.go index 77e178fc1b..4342f6e09c 100644 --- a/server/datastore/mysql/in_house_apps.go +++ b/server/datastore/mysql/in_house_apps.go @@ -211,20 +211,32 @@ WHERE if err != nil { return nil, ctxerr.Wrap(ctx, err, "get in house app labels") } - var exclAny, inclAny []fleet.SoftwareScopeLabel + var exclAny, inclAny, inclAll []fleet.SoftwareScopeLabel for _, l := range labels { - if l.Exclude { + switch { + case l.Exclude && !l.RequireAll: exclAny = append(exclAny, l) - } else { + case !l.Exclude && l.RequireAll: + inclAll = append(inclAll, l) + case !l.Exclude && !l.RequireAll: inclAny = append(inclAny, l) + default: + ds.logger.WarnContext(ctx, "in house app has an unsupported label scope", "installer_id", dest.InstallerID, "invalid_label", fmt.Sprintf("%#v", l)) } } - if len(inclAny) > 0 && len(exclAny) > 0 { - ds.logger.WarnContext(ctx, "in house app has both include and exclude labels", "installer_id", dest.InstallerID, "include", fmt.Sprintf("%v", inclAny), "exclude", fmt.Sprintf("%v", exclAny)) + var count int + for _, set := range [][]fleet.SoftwareScopeLabel{exclAny, inclAny, inclAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { + ds.logger.WarnContext(ctx, "in house app has more than one scope of labels", "installer_id", dest.InstallerID, "include_any", fmt.Sprintf("%v", inclAny), "exclude_any", fmt.Sprintf("%v", exclAny), "include_all", fmt.Sprintf("%v", inclAll)) } dest.LabelsExcludeAny = exclAny dest.LabelsIncludeAny = inclAny + dest.LabelsIncludeAll = inclAll categoryMap, err := ds.GetCategoriesForSoftwareTitles(ctx, []uint{titleID}, teamID) if err != nil { @@ -961,18 +973,21 @@ INSERT INTO in_house_app_labels ( in_house_app_id, label_id, - exclude + exclude, + require_all ) VALUES %s ON DUPLICATE KEY UPDATE - exclude = VALUES(exclude) + exclude = VALUES(exclude), + require_all = VALUES(require_all) ` const loadExistingInHouseLabels = ` SELECT label_id, - exclude + exclude, + require_all FROM in_house_app_labels WHERE @@ -1322,13 +1337,15 @@ WHERE } excludeLabels := installer.ValidatedLabels.LabelScope == fleet.LabelScopeExcludeAny + requireAllLabels := installer.ValidatedLabels.LabelScope == fleet.LabelScopeIncludeAll if len(existing) > 0 && !existing[0].IsMetadataModified { // load the remaining labels for that installer, so that we can detect // if any label changed (if the counts differ, then labels did change, - // otherwise if the exclude bool changed, the target did change). + // otherwise if the exclude/require all bool changed, the target did change). var existingLabels []struct { - LabelID uint `db:"label_id"` - Exclude bool `db:"exclude"` + LabelID uint `db:"label_id"` + Exclude bool `db:"exclude"` + RequireAll bool `db:"require_all"` } if err := sqlx.SelectContext(ctx, tx, &existingLabels, loadExistingInHouseLabels, installerID); err != nil { return ctxerr.Wrapf(ctx, err, "load existing labels for in-house with name %q", installer.Filename) @@ -1337,8 +1354,8 @@ WHERE if len(existingLabels) != len(labelIDs) { existing[0].IsMetadataModified = true } - if len(existingLabels) > 0 && existingLabels[0].Exclude != excludeLabels { - // same labels are provided, but the include <-> exclude changed + if len(existingLabels) > 0 && (existingLabels[0].Exclude != excludeLabels || existingLabels[0].RequireAll != requireAllLabels) { + // same labels are provided, but the include <-> exclude or require all changed existing[0].IsMetadataModified = true } } @@ -1346,9 +1363,9 @@ WHERE // upsert the new labels now that obsolete ones have been deleted var upsertLabelArgs []any for _, lblID := range labelIDs { - upsertLabelArgs = append(upsertLabelArgs, installerID, lblID, excludeLabels) + upsertLabelArgs = append(upsertLabelArgs, installerID, lblID, excludeLabels, requireAllLabels) } - upsertLabelValues := strings.TrimSuffix(strings.Repeat("(?,?,?),", len(installer.ValidatedLabels.ByName)), ",") + upsertLabelValues := strings.TrimSuffix(strings.Repeat("(?,?,?,?),", len(installer.ValidatedLabels.ByName)), ",") _, err = tx.ExecContext(ctx, fmt.Sprintf(upsertInHouseLabels, upsertLabelValues), upsertLabelArgs...) if err != nil { diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index 9c74b23ebb..42b26d7f57 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -3479,14 +3479,16 @@ func filterSoftwareInstallersByLabel( FROM software_installers INNER JOIN software_installer_labels - ON software_installer_labels.software_installer_id = software_installers.id AND software_installer_labels.exclude = 0 + ON software_installer_labels.software_installer_id = software_installers.id + AND software_installer_labels.exclude = 0 + AND software_installer_labels.require_all = 0 LEFT JOIN label_membership ON label_membership.label_id = software_installer_labels.label_id AND label_membership.host_id = :host_id GROUP BY software_installers.id HAVING - COUNT(*) > 0 AND COUNT(label_membership.label_id) > 0 + count_installer_labels > 0 AND count_host_labels > 0 ), exclude_any AS ( SELECT @@ -3505,7 +3507,9 @@ func filterSoftwareInstallersByLabel( FROM software_installers INNER JOIN software_installer_labels - ON software_installer_labels.software_installer_id = software_installers.id AND software_installer_labels.exclude = 1 + ON software_installer_labels.software_installer_id = software_installers.id + AND software_installer_labels.exclude = 1 + AND software_installer_labels.require_all = 0 INNER JOIN labels ON labels.id = software_installer_labels.label_id LEFT JOIN label_membership @@ -3514,17 +3518,30 @@ func filterSoftwareInstallersByLabel( GROUP BY software_installers.id HAVING - COUNT(*) > 0 - AND COUNT(*) = SUM( - CASE - WHEN labels.created_at IS NOT NULL AND ( - labels.label_membership_type = 1 OR - (labels.label_membership_type = 0 AND :host_label_updated_at >= labels.created_at) - ) THEN 1 - ELSE 0 - END - ) - AND COUNT(label_membership.label_id) = 0 + count_installer_labels > 0 + AND count_installer_labels = count_host_updated_after_labels + AND count_host_labels = 0 + ), + include_all AS ( + SELECT + software_installers.id AS installer_id, + COUNT(*) AS count_installer_labels, + COUNT(label_membership.label_id) AS count_host_labels, + 0 AS count_host_updated_after_labels + FROM + software_installers + INNER JOIN software_installer_labels + ON software_installer_labels.software_installer_id = software_installers.id + AND software_installer_labels.exclude = 0 + AND software_installer_labels.require_all = 1 + LEFT JOIN label_membership + ON label_membership.label_id = software_installer_labels.label_id + AND label_membership.host_id = :host_id + GROUP BY + software_installers.id + HAVING + count_installer_labels > 0 + AND count_host_labels = count_installer_labels ) SELECT software_installers.id AS id, @@ -3537,6 +3554,8 @@ func filterSoftwareInstallersByLabel( ON include_any.installer_id = software_installers.id LEFT JOIN exclude_any ON exclude_any.installer_id = software_installers.id + LEFT JOIN include_all + ON include_all.installer_id = software_installers.id WHERE software_installers.global_or_team_id = :global_or_team_id AND software_installers.id IN (:software_installer_ids) @@ -3544,6 +3563,7 @@ func filterSoftwareInstallersByLabel( no_labels.installer_id IS NOT NULL OR include_any.installer_id IS NOT NULL OR exclude_any.installer_id IS NOT NULL + OR include_all.installer_id IS NOT NULL ) ` labelSqlFilter, args, err := sqlx.Named(labelSqlFilter, map[string]any{ @@ -3633,7 +3653,9 @@ func filterVPPAppsByLabel( FROM vpp_apps_teams INNER JOIN vpp_app_team_labels - ON vpp_app_team_labels.vpp_app_team_id = vpp_apps_teams.id AND vpp_app_team_labels.exclude = 0 + ON vpp_app_team_labels.vpp_app_team_id = vpp_apps_teams.id + AND vpp_app_team_labels.exclude = 0 + AND vpp_app_team_labels.require_all = 0 LEFT JOIN label_membership ON label_membership.label_id = vpp_app_team_labels.label_id AND label_membership.host_id = :host_id @@ -3657,7 +3679,9 @@ func filterVPPAppsByLabel( FROM vpp_apps_teams INNER JOIN vpp_app_team_labels - ON vpp_app_team_labels.vpp_app_team_id = vpp_apps_teams.id AND vpp_app_team_labels.exclude = 1 + ON vpp_app_team_labels.vpp_app_team_id = vpp_apps_teams.id + AND vpp_app_team_labels.exclude = 1 + AND vpp_app_team_labels.require_all = 0 INNER JOIN labels ON labels.id = vpp_app_team_labels.label_id LEFT OUTER JOIN label_membership @@ -3668,6 +3692,26 @@ func filterVPPAppsByLabel( count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0 + ), + include_all AS ( + SELECT + vpp_apps_teams.id AS team_id, + COUNT(vpp_app_team_labels.label_id) AS count_installer_labels, + COUNT(label_membership.label_id) AS count_host_labels, + 0 as count_host_updated_after_labels + FROM + vpp_apps_teams + INNER JOIN vpp_app_team_labels + ON vpp_app_team_labels.vpp_app_team_id = vpp_apps_teams.id + AND vpp_app_team_labels.exclude = 0 + AND vpp_app_team_labels.require_all = 1 + LEFT JOIN label_membership + ON label_membership.label_id = vpp_app_team_labels.label_id + AND label_membership.host_id = :host_id + GROUP BY + vpp_apps_teams.id + HAVING + count_installer_labels > 0 AND count_host_labels = count_installer_labels ) SELECT vpp_apps.adam_id AS adam_id, @@ -3675,19 +3719,24 @@ func filterVPPAppsByLabel( FROM vpp_apps INNER JOIN - vpp_apps_teams ON vpp_apps.adam_id = vpp_apps_teams.adam_id AND vpp_apps.platform = vpp_apps_teams.platform AND vpp_apps_teams.global_or_team_id = :global_or_team_id + vpp_apps_teams ON vpp_apps.adam_id = vpp_apps_teams.adam_id + AND vpp_apps.platform = vpp_apps_teams.platform + AND vpp_apps_teams.global_or_team_id = :global_or_team_id LEFT JOIN no_labels ON no_labels.team_id = vpp_apps_teams.id LEFT JOIN include_any ON include_any.team_id = vpp_apps_teams.id LEFT JOIN exclude_any ON exclude_any.team_id = vpp_apps_teams.id + LEFT JOIN include_all + ON include_all.team_id = vpp_apps_teams.id WHERE vpp_apps.adam_id IN (:vpp_app_adam_ids) AND ( no_labels.team_id IS NOT NULL OR include_any.team_id IS NOT NULL OR exclude_any.team_id IS NOT NULL + OR include_all.team_id IS NOT NULL ) ` @@ -3786,8 +3835,10 @@ func filterInHouseAppsByLabel( 0 as count_host_updated_after_labels FROM in_house_apps iha - INNER JOIN in_house_app_labels ihl ON - ihl.in_house_app_id = iha.id AND ihl.exclude = 0 + INNER JOIN in_house_app_labels ihl + ON ihl.in_house_app_id = iha.id + AND ihl.exclude = 0 + AND ihl.require_all = 0 LEFT JOIN label_membership lm ON lm.label_id = ihl.label_id AND lm.host_id = :host_id GROUP BY @@ -3809,8 +3860,10 @@ func filterInHouseAppsByLabel( ) AS count_host_updated_after_labels FROM in_house_apps iha - INNER JOIN in_house_app_labels ihl ON - ihl.in_house_app_id = iha.id AND ihl.exclude = 1 + INNER JOIN in_house_app_labels ihl + ON ihl.in_house_app_id = iha.id + AND ihl.exclude = 1 + AND ihl.require_all = 0 INNER JOIN labels lbl ON lbl.id = ihl.label_id LEFT OUTER JOIN label_membership lm ON @@ -3821,6 +3874,25 @@ func filterInHouseAppsByLabel( count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0 + ), + include_all AS ( + SELECT + iha.id AS in_house_app_id, + COUNT(ihl.label_id) AS count_installer_labels, + COUNT(lm.label_id) AS count_host_labels, + 0 as count_host_updated_after_labels + FROM + in_house_apps iha + INNER JOIN in_house_app_labels ihl + ON ihl.in_house_app_id = iha.id + AND ihl.exclude = 0 + AND ihl.require_all = 1 + LEFT JOIN label_membership lm ON + lm.label_id = ihl.label_id AND lm.host_id = :host_id + GROUP BY + iha.id + HAVING + count_installer_labels > 0 AND count_host_labels = count_installer_labels ) SELECT iha.id AS in_house_id, @@ -3833,12 +3905,15 @@ func filterInHouseAppsByLabel( ON include_any.in_house_app_id = iha.id LEFT JOIN exclude_any ON exclude_any.in_house_app_id = iha.id + LEFT JOIN include_all + ON include_all.in_house_app_id = iha.id WHERE iha.global_or_team_id = :global_or_team_id AND iha.id IN (:in_house_ids) AND ( no_labels.in_house_app_id IS NOT NULL OR include_any.in_house_app_id IS NOT NULL OR - exclude_any.in_house_app_id IS NOT NULL + exclude_any.in_house_app_id IS NOT NULL OR + include_all.in_house_app_id IS NOT NULL ) ` @@ -4720,7 +4795,7 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt SELECT 1 FROM ( - -- no labels + -- no labels for any type of installer SELECT 0 AS count_installer_labels, 0 AS count_host_labels, 0 as count_host_updated_after_labels WHERE NOT EXISTS (SELECT 1 FROM software_installer_labels sil WHERE sil.software_installer_id = si.id) AND @@ -4741,6 +4816,7 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt WHERE sil.software_installer_id = si.id AND sil.exclude = 0 + AND sil.require_all = 0 HAVING count_installer_labels > 0 AND count_host_labels > 0 @@ -4766,11 +4842,30 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt WHERE sil.software_installer_id = si.id AND sil.exclude = 1 + AND sil.require_all = 0 HAVING count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0 UNION + -- include all for software installers + SELECT + COUNT(*) AS count_installer_labels, + COUNT(lm.label_id) AS count_host_labels, + 0 as count_host_updated_after_labels + FROM + software_installer_labels sil + LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id + AND lm.host_id = :host_id + WHERE + sil.software_installer_id = si.id + AND sil.exclude = 0 + AND sil.require_all = 1 + HAVING + count_installer_labels > 0 AND count_host_labels = count_installer_labels + + UNION + -- include any for VPP apps SELECT COUNT(*) AS count_installer_labels, @@ -4783,6 +4878,7 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt WHERE vatl.vpp_app_team_id = vat.id AND vatl.exclude = 0 + AND vatl.require_all = 0 HAVING count_installer_labels > 0 AND count_host_labels > 0 @@ -4805,11 +4901,30 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt WHERE vatl.vpp_app_team_id = vat.id AND vatl.exclude = 1 + AND vatl.require_all = 0 HAVING count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0 UNION + -- include all for VPP apps + SELECT + COUNT(*) AS count_installer_labels, + COUNT(lm.label_id) AS count_host_labels, + 0 as count_host_updated_after_labels + FROM + vpp_app_team_labels vatl + LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id + AND lm.host_id = :host_id + WHERE + vatl.vpp_app_team_id = vat.id + AND vatl.exclude = 0 + AND vatl.require_all = 1 + HAVING + count_installer_labels > 0 AND count_host_labels = count_installer_labels + + UNION + -- include any for in-house apps SELECT COUNT(*) AS count_installer_labels, @@ -4821,6 +4936,7 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt WHERE ihl.in_house_app_id = iha.id AND ihl.exclude = 0 + AND ihl.require_all = 0 HAVING count_installer_labels > 0 AND count_host_labels > 0 @@ -4839,10 +4955,28 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt LEFT OUTER JOIN labels lbl ON lbl.id = ihl.label_id LEFT OUTER JOIN label_membership lm ON lm.label_id = ihl.label_id AND lm.host_id = :host_id WHERE - ihl.in_house_app_id = iha.id AND - ihl.exclude = 1 + ihl.in_house_app_id = iha.id + AND ihl.exclude = 1 + AND ihl.require_all = 0 HAVING count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0 + + UNION + + -- include all for in-house apps + SELECT + COUNT(*) AS count_installer_labels, + COUNT(lm.label_id) AS count_host_labels, + 0 as count_host_updated_after_labels + FROM + in_house_app_labels ihl + LEFT OUTER JOIN label_membership lm ON lm.label_id = ihl.label_id AND lm.host_id = :host_id + WHERE + ihl.in_house_app_id = iha.id + AND ihl.exclude = 0 + AND ihl.require_all = 1 + HAVING + count_installer_labels > 0 AND count_host_labels = count_installer_labels ) t ) ) diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index f2f329e788..e656d57ac8 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -617,23 +617,29 @@ func setOrUpdateSoftwareInstallerLabelsDB(ctx context.Context, tx sqlx.ExtContex // insert new labels if len(labelIds) > 0 { - var exclude bool + var exclude, requireAll bool switch labels.LabelScope { case fleet.LabelScopeIncludeAny: exclude = false + requireAll = false case fleet.LabelScopeExcludeAny: exclude = true + requireAll = false + case fleet.LabelScopeIncludeAll: + exclude = false + requireAll = true default: // this should never happen return ctxerr.New(ctx, "invalid label scope") } - stmt := `INSERT INTO %[1]s_labels (%[1]s_id, label_id, exclude) VALUES %s ON DUPLICATE KEY UPDATE exclude = VALUES(exclude)` + stmt := `INSERT INTO %[1]s_labels (%[1]s_id, label_id, exclude, require_all) VALUES %s + ON DUPLICATE KEY UPDATE exclude = VALUES(exclude), require_all = VALUES(require_all)` var placeholders string var insertArgs []interface{} for _, lid := range labelIds { - placeholders += "(?, ?, ?)," - insertArgs = append(insertArgs, installerID, lid, exclude) + placeholders += "(?, ?, ?, ?)," + insertArgs = append(insertArgs, installerID, lid, exclude, requireAll) } placeholders = strings.TrimSuffix(placeholders, ",") @@ -1036,21 +1042,32 @@ LIMIT 1`, if err != nil { return nil, ctxerr.Wrap(ctx, err, "get software installer labels") } - var exclAny, inclAny []fleet.SoftwareScopeLabel + var exclAny, inclAny, inclAll []fleet.SoftwareScopeLabel for _, l := range labels { - if l.Exclude { + switch { + case l.Exclude && !l.RequireAll: exclAny = append(exclAny, l) - } else { + case !l.Exclude && l.RequireAll: + inclAll = append(inclAll, l) + case !l.Exclude && !l.RequireAll: inclAny = append(inclAny, l) + default: + ds.logger.WarnContext(ctx, "software installer has an unsupported label scope", "installer_id", dest.InstallerID, "invalid_label", fmt.Sprintf("%#v", l)) } } - if len(inclAny) > 0 && len(exclAny) > 0 { - // there's a bug somewhere - ds.logger.WarnContext(ctx, "software installer has both include and exclude labels", "installer_id", dest.InstallerID, "include", fmt.Sprintf("%v", inclAny), "exclude", fmt.Sprintf("%v", exclAny)) + var count int + for _, set := range [][]fleet.SoftwareScopeLabel{exclAny, inclAny, inclAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { + ds.logger.WarnContext(ctx, "software installer has more than one scope of labels", "installer_id", dest.InstallerID, "include_any", fmt.Sprintf("%v", inclAny), "exclude_any", fmt.Sprintf("%v", exclAny), "include_all", fmt.Sprintf("%v", inclAll)) } dest.LabelsExcludeAny = exclAny dest.LabelsIncludeAny = inclAny + dest.LabelsIncludeAll = inclAll categoryMap, err := ds.GetCategoriesForSoftwareTitles(ctx, []uint{titleID}, teamID) if err != nil { @@ -1093,7 +1110,8 @@ SELECT label_id, exclude, l.name as label_name, - si.title_id + si.title_id, + require_all FROM %[1]s_labels sil JOIN %[1]ss si ON si.id = sil.%[1]s_id @@ -2339,18 +2357,21 @@ INSERT INTO software_installer_labels ( software_installer_id, label_id, - exclude + exclude, + require_all ) VALUES %s ON DUPLICATE KEY UPDATE - exclude = VALUES(exclude) + exclude = VALUES(exclude), + require_all = VALUES(require_all) ` const loadExistingInstallerLabels = ` SELECT label_id, - exclude + exclude, + require_all FROM software_installer_labels WHERE @@ -2893,13 +2914,15 @@ WHERE } excludeLabels := installer.ValidatedLabels.LabelScope == fleet.LabelScopeExcludeAny + requireAllLabels := installer.ValidatedLabels.LabelScope == fleet.LabelScopeIncludeAll if len(existing) > 0 && !existing[0].IsMetadataModified { // load the remaining labels for that installer, so that we can detect // if any label changed (if the counts differ, then labels did change, - // otherwise if the exclude bool changed, the target did change). + // otherwise if the exclude/require all bool changed, the target did change). var existingLabels []struct { - LabelID uint `db:"label_id"` - Exclude bool `db:"exclude"` + LabelID uint `db:"label_id"` + Exclude bool `db:"exclude"` + RequireAll bool `db:"require_all"` } if err := sqlx.SelectContext(ctx, tx, &existingLabels, loadExistingInstallerLabels, installerID); err != nil { return ctxerr.Wrapf(ctx, err, "load existing labels for installer with name %q", installer.Filename) @@ -2908,8 +2931,8 @@ WHERE if len(existingLabels) != len(labelIDs) { existing[0].IsMetadataModified = true } - if len(existingLabels) > 0 && existingLabels[0].Exclude != excludeLabels { - // same labels are provided, but the include <-> exclude changed + if len(existingLabels) > 0 && (existingLabels[0].Exclude != excludeLabels || existingLabels[0].RequireAll != requireAllLabels) { + // same labels are provided, but the include <-> exclude or require all changed existing[0].IsMetadataModified = true } } @@ -2917,9 +2940,9 @@ WHERE // upsert the new labels now that obsolete ones have been deleted var upsertLabelArgs []any for _, lblID := range labelIDs { - upsertLabelArgs = append(upsertLabelArgs, installerID, lblID, excludeLabels) + upsertLabelArgs = append(upsertLabelArgs, installerID, lblID, excludeLabels, requireAllLabels) } - upsertLabelValues := strings.TrimSuffix(strings.Repeat("(?,?,?),", len(installer.ValidatedLabels.ByName)), ",") + upsertLabelValues := strings.TrimSuffix(strings.Repeat("(?,?,?,?),", len(installer.ValidatedLabels.ByName)), ",") _, err = tx.ExecContext(ctx, fmt.Sprintf(upsertInstallerLabels, upsertLabelValues), upsertLabelArgs...) if err != nil { @@ -3203,6 +3226,7 @@ func (ds *Datastore) isSoftwareLabelScoped(ctx context.Context, softwareID, host WHERE sil.%[1]s_id = :software_id AND sil.exclude = 0 + AND sil.require_all = 0 HAVING count_installer_labels > 0 AND count_host_labels > 0 @@ -3232,8 +3256,27 @@ func (ds *Datastore) isSoftwareLabelScoped(ctx context.Context, softwareID, host WHERE sil.%[1]s_id = :software_id AND sil.exclude = 1 + AND sil.require_all = 0 HAVING count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0 + + UNION + + -- include all + SELECT + COUNT(*) AS count_installer_labels, + COUNT(lm.label_id) AS count_host_labels, + 0 as count_host_updated_after_labels + FROM + %[1]s_labels sil + LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id + AND lm.host_id = :host_id + WHERE + sil.%[1]s_id = :software_id + AND sil.exclude = 0 + AND sil.require_all = 1 + HAVING + count_installer_labels > 0 AND count_host_labels = count_installer_labels ) t ` diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go index e6562808c0..37dc4c97fb 100644 --- a/server/datastore/mysql/software_test.go +++ b/server/datastore/mysql/software_test.go @@ -9,7 +9,9 @@ import ( "encoding/hex" "fmt" "log/slog" + "maps" "math/rand" + std_slices "slices" "sort" "strconv" "strings" @@ -7630,7 +7632,8 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) { require.False(t, ok) continue } - require.True(t, ok) + names := std_slices.Collect(maps.Keys(expectedInstallers)) + require.Truef(t, ok, "didn't find installer for %s in expectedInstallers (%s)", got.SoftwarePackage.Name, strings.Join(names, ", ")) require.Equal(t, want, got.SoftwarePackage) } } @@ -7971,6 +7974,65 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) { software, _, err = ds.ListHostSoftware(ctx, host, opts) require.NoError(t, err) checkSoftware(software, installer2.Filename, installer3.Filename, installer4.Filename) + + t.Run("include_all", func(t *testing.T) { + + hostIncludeAll := test.NewHost(t, ds, "host_include_all", "", "host1key_include_all", "host1uuid_include_all", time.Now(), test.WithPlatform("darwin")) + nanoEnroll(t, ds, hostIncludeAll, false) + + label4, err := ds.NewLabel(ctx, &fleet.Label{Name: "label4" + t.Name()}) + require.NoError(t, err) + + // Scope installer1 to include_all: [label1, label4]. + // hostIncludeAll has neither label yet, so installer1 should be out of scope. + err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID1, fleet.LabelIdentsWithScope{ + LabelScope: fleet.LabelScopeIncludeAll, + ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}, label4.Name: {LabelName: label4.Name, LabelID: label4.ID}}, + }, softwareTypeInstaller) + require.NoError(t, err) + + // host has no labels yet — installer1 is out of scope + scoped, err := ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, hostIncludeAll.ID) + require.NoError(t, err) + require.False(t, scoped) + + software, _, err = ds.ListHostSoftware(ctx, hostIncludeAll, opts) + require.NoError(t, err) + // installer1 should be absent (out of scope), installer4 absent (no labels on host) + checkSoftware(software, installer1.Filename, installer4.Filename) + + // add only label1: host still missing label4, so still out of scope + require.NoError(t, ds.AddLabelsToHost(ctx, hostIncludeAll.ID, []uint{label1.ID})) + hostIncludeAll.LabelUpdatedAt = time.Now() + err = ds.UpdateHost(ctx, hostIncludeAll) + require.NoError(t, err) + time.Sleep(time.Second) + + scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, hostIncludeAll.ID) + require.NoError(t, err) + require.False(t, scoped) + + software, _, err = ds.ListHostSoftware(ctx, hostIncludeAll, opts) + require.NoError(t, err) + checkSoftware(software, installer1.Filename, installer4.Filename) + + // add label4 — host now has both required labels, so installer1 is in scope + require.NoError(t, ds.AddLabelsToHost(ctx, hostIncludeAll.ID, []uint{label4.ID})) + hostIncludeAll.LabelUpdatedAt = time.Now() + err = ds.UpdateHost(ctx, hostIncludeAll) + require.NoError(t, err) + time.Sleep(time.Second) + + scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, hostIncludeAll.ID) + require.NoError(t, err) + require.True(t, scoped) + + software, _, err = ds.ListHostSoftware(ctx, hostIncludeAll, opts) + require.NoError(t, err) + // installer1 is now in scope; installer4 still absent (no labels on host match it) + checkSoftware(software, installer4.Filename) + }) + } func testListHostSoftwareVulnerableAndVPP(t *testing.T, ds *Datastore) { @@ -9094,6 +9156,63 @@ func testListHostSoftwareWithLabelScopingVPP(t *testing.T, ds *Datastore) { scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID) require.NoError(t, err) require.True(t, scoped) + + // --- include_all tests for VPP --- + // Create two fresh labels for the include_all scope tests. + label5, err := ds.NewLabel(ctx, &fleet.Label{Name: "label5" + t.Name()}) + require.NoError(t, err) + label6, err := ds.NewLabel(ctx, &fleet.Label{Name: "label6" + t.Name()}) + require.NoError(t, err) + + // Scope the VPP app to include_all: [label5, label6]. + // host currently has label1 but not label5 or label6. + err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), vppAppTeamID, fleet.LabelIdentsWithScope{ + LabelScope: fleet.LabelScopeIncludeAll, + ByName: map[string]fleet.LabelIdent{ + label5.Name: {LabelName: label5.Name, LabelID: label5.ID}, + label6.Name: {LabelName: label6.Name, LabelID: label6.ID}, + }, + }, softwareTypeVPP) + require.NoError(t, err) + + // host has neither required label — out of scope + scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID) + require.NoError(t, err) + require.False(t, scoped) + + software, _, err = ds.ListHostSoftware(ctx, host, opts) + require.NoError(t, err) + checkSoftware(software, vppApp.Name) + + // add label5 only — still missing label6, so still out of scope + require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label5.ID})) + host.LabelUpdatedAt = time.Now() + err = ds.UpdateHost(ctx, host) + require.NoError(t, err) + time.Sleep(time.Second) + + scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID) + require.NoError(t, err) + require.False(t, scoped) + + software, _, err = ds.ListHostSoftware(ctx, host, opts) + require.NoError(t, err) + checkSoftware(software, vppApp.Name) + + // add label6 — host now has both required labels, so VPP app is in scope + require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label6.ID})) + host.LabelUpdatedAt = time.Now() + err = ds.UpdateHost(ctx, host) + require.NoError(t, err) + time.Sleep(time.Second) + + scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID) + require.NoError(t, err) + require.True(t, scoped) + + software, _, err = ds.ListHostSoftware(ctx, host, opts) + require.NoError(t, err) + checkSoftware(software) } func testListHostSoftwareLastOpenedAt(t *testing.T, ds *Datastore) { diff --git a/server/datastore/mysql/software_title_icons.go b/server/datastore/mysql/software_title_icons.go index c2b4f3d910..0783bec10b 100644 --- a/server/datastore/mysql/software_title_icons.go +++ b/server/datastore/mysql/software_title_icons.go @@ -115,7 +115,7 @@ func (ds *Datastore) DeleteSoftwareTitleIcon(ctx context.Context, teamID, titleI func (ds *Datastore) DeleteIconsAssociatedWithTitlesWithoutInstallers(ctx context.Context, teamID uint) error { _, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM software_title_icons WHERE team_id = ? - AND software_title_id NOT IN (SELECT title_id FROM vpp_apps va JOIN vpp_apps_teams vat + AND software_title_id NOT IN (SELECT title_id FROM vpp_apps va JOIN vpp_apps_teams vat ON vat.adam_id = va.adam_id AND vat.platform = va.platform WHERE global_or_team_id = ?) AND software_title_id NOT IN (SELECT title_id FROM software_installers WHERE global_or_team_id = ?) AND software_title_id NOT IN (SELECT title_id FROM in_house_apps WHERE global_or_team_id = ?)`, @@ -172,9 +172,10 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te } type ActivitySoftwareLabel struct { - ID uint `db:"id"` - Name string `db:"name"` - Exclude bool `db:"exclude"` + ID uint `db:"id"` + Name string `db:"name"` + Exclude bool `db:"exclude"` + RequireAll bool `db:"require_all"` } var labels []ActivitySoftwareLabel if details.SoftwareInstallerID != nil { @@ -182,7 +183,8 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te SELECT labels.id AS id, labels.name AS name, - software_installer_labels.exclude AS exclude + software_installer_labels.exclude AS exclude, + software_installer_labels.require_all AS require_all FROM software_installer_labels INNER JOIN labels ON software_installer_labels.label_id = labels.id WHERE software_installer_id = ? @@ -196,7 +198,8 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te SELECT labels.id AS id, labels.name AS name, - vpp_app_team_labels.exclude AS exclude + vpp_app_team_labels.exclude AS exclude, + vpp_app_team_labels.require_all AS require_all FROM vpp_app_team_labels INNER JOIN labels ON vpp_app_team_labels.label_id = labels.id WHERE vpp_app_team_id = ? @@ -210,7 +213,8 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te SELECT labels.id AS id, labels.name AS name, - in_house_app_labels.exclude AS exclude + in_house_app_labels.exclude AS exclude, + in_house_app_labels.require_all AS require_all FROM in_house_app_labels INNER JOIN labels ON in_house_app_labels.label_id = labels.id WHERE in_house_app_id = ? @@ -221,16 +225,28 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te } for _, l := range labels { - if l.Exclude { + switch { + case l.Exclude && !l.RequireAll: details.LabelsExcludeAny = append(details.LabelsExcludeAny, fleet.ActivitySoftwareLabel{ ID: l.ID, Name: l.Name, }) - } else { + + case !l.Exclude && l.RequireAll: + details.LabelsIncludeAll = append(details.LabelsIncludeAll, fleet.ActivitySoftwareLabel{ + ID: l.ID, + Name: l.Name, + }) + + case !l.Exclude && !l.RequireAll: details.LabelsIncludeAny = append(details.LabelsIncludeAny, fleet.ActivitySoftwareLabel{ ID: l.ID, Name: l.Name, }) + + default: + // should never happen, we don't support ExcludeAll currently + ds.logger.ErrorContext(ctx, "unsupported label condition 'exclude-all' encountered for software", "title_id", titleID, "label_id", l.ID) } } diff --git a/server/datastore/mysql/software_title_icons_test.go b/server/datastore/mysql/software_title_icons_test.go index 9fff424df8..a6f1b014a2 100644 --- a/server/datastore/mysql/software_title_icons_test.go +++ b/server/datastore/mysql/software_title_icons_test.go @@ -303,11 +303,18 @@ func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) { "INSERT INTO software_installer_labels (software_installer_id, label_id, exclude) VALUES (?, ?, ?)", installerID, label1.ID, true) require.NoError(t, err) - // Insert include label + // Insert include any label _, err = ds.writer(ctx).ExecContext(ctx, "INSERT INTO software_installer_labels (software_installer_id, label_id, exclude) VALUES (?, ?, ?)", installerID, label2.ID, false) require.NoError(t, err) + // Insert include all label + label3, err := ds.NewLabel(ctx, &fleet.Label{Name: "label3"}) + require.NoError(t, err) + _, err = ds.writer(ctx).ExecContext(ctx, + "INSERT INTO software_installer_labels (software_installer_id, label_id, exclude, require_all) VALUES (?, ?, ?, ?)", + installerID, label3.ID, false, true) + require.NoError(t, err) _, err = ds.CreateOrUpdateSoftwareTitleIcon(ctx, &fleet.UploadSoftwareTitleIconPayload{ TeamID: teamID, @@ -335,6 +342,8 @@ func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) { require.Equal(t, "label1", activity.LabelsExcludeAny[0].Name) require.Len(t, activity.LabelsIncludeAny, 1) require.Equal(t, "label2", activity.LabelsIncludeAny[0].Name) + require.Len(t, activity.LabelsIncludeAll, 1) + require.Equal(t, "label3", activity.LabelsIncludeAll[0].Name) }}, {"vpp app", func(ds *Datastore) { teamID, titleID, err = createTeamAndSoftwareTitle(t, ctx, ds) @@ -381,11 +390,18 @@ func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) { "INSERT INTO vpp_app_team_labels (vpp_app_team_id, label_id, exclude) VALUES (?, ?, ?)", vppApp.VPPAppTeam.AppTeamID, label1.ID, true) require.NoError(t, err) - // Insert include label + // Insert include any label _, err = ds.writer(ctx).ExecContext(ctx, "INSERT INTO vpp_app_team_labels (vpp_app_team_id, label_id, exclude) VALUES (?, ?, ?)", vppApp.VPPAppTeam.AppTeamID, label2.ID, false) require.NoError(t, err) + // Insert include all label + label3, err := ds.NewLabel(ctx, &fleet.Label{Name: "label3"}) + require.NoError(t, err) + _, err = ds.writer(ctx).ExecContext(ctx, + "INSERT INTO vpp_app_team_labels (vpp_app_team_id, label_id, exclude, require_all) VALUES (?, ?, ?, ?)", + vppApp.VPPAppTeam.AppTeamID, label3.ID, false, true) + require.NoError(t, err) _, err = ds.CreateOrUpdateSoftwareTitleIcon(ctx, &fleet.UploadSoftwareTitleIconPayload{ TeamID: teamID, @@ -413,6 +429,8 @@ func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) { require.Equal(t, "label1", activity.LabelsExcludeAny[0].Name) require.Len(t, activity.LabelsIncludeAny, 1) require.Equal(t, "label2", activity.LabelsIncludeAny[0].Name) + require.Len(t, activity.LabelsIncludeAll, 1) + require.Equal(t, "label3", activity.LabelsIncludeAll[0].Name) }}, {"team id 0", func(ds *Datastore) { user := test.NewUser(t, ds, "user1", "user1@example.com", false) @@ -480,6 +498,7 @@ func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) { require.Nil(t, activity.Platform) require.Nil(t, activity.LabelsExcludeAny) require.Nil(t, activity.LabelsIncludeAny) + require.Nil(t, activity.LabelsIncludeAll) }}, {"in house app", func(ds *Datastore) { user := test.NewUser(t, ds, "user1", "user1@example.com", false) @@ -519,11 +538,18 @@ func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) { "INSERT INTO in_house_app_labels (in_house_app_id, label_id, exclude) VALUES (?, ?, ?)", installerID, label1.ID, true) require.NoError(t, err) - // Insert include label + // Insert include any label _, err = ds.writer(ctx).ExecContext(ctx, "INSERT INTO in_house_app_labels (in_house_app_id, label_id, exclude) VALUES (?, ?, ?)", installerID, label2.ID, false) require.NoError(t, err) + // Insert include all label + label3, err := ds.NewLabel(ctx, &fleet.Label{Name: "label3"}) + require.NoError(t, err) + _, err = ds.writer(ctx).ExecContext(ctx, + "INSERT INTO in_house_app_labels (in_house_app_id, label_id, exclude, require_all) VALUES (?, ?, ?, ?)", + installerID, label3.ID, false, true) + require.NoError(t, err) _, err = ds.CreateOrUpdateSoftwareTitleIcon(ctx, &fleet.UploadSoftwareTitleIconPayload{ TeamID: teamID, @@ -551,6 +577,8 @@ func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) { require.Equal(t, "label1", activity.LabelsExcludeAny[0].Name) require.Len(t, activity.LabelsIncludeAny, 1) require.Equal(t, "label2", activity.LabelsIncludeAny[0].Name) + require.Len(t, activity.LabelsIncludeAll, 1) + require.Equal(t, "label3", activity.LabelsIncludeAll[0].Name) }}, } diff --git a/server/datastore/mysql/vpp.go b/server/datastore/mysql/vpp.go index cc33599a5c..d556f99e9f 100644 --- a/server/datastore/mysql/vpp.go +++ b/server/datastore/mysql/vpp.go @@ -62,21 +62,32 @@ WHERE if err != nil { return nil, ctxerr.Wrap(ctx, err, "get vpp app labels") } - var exclAny, inclAny []fleet.SoftwareScopeLabel + var exclAny, inclAny, inclAll []fleet.SoftwareScopeLabel for _, l := range labels { - if l.Exclude { + switch { + case l.Exclude && !l.RequireAll: exclAny = append(exclAny, l) - } else { + case !l.Exclude && l.RequireAll: + inclAll = append(inclAll, l) + case !l.Exclude && !l.RequireAll: inclAny = append(inclAny, l) + default: + ds.logger.WarnContext(ctx, "vpp app has an unsupported label scope", "vpp_apps_teams_id", app.VPPAppsTeamsID, "invalid_label", fmt.Sprintf("%#v", l)) } } - if len(inclAny) > 0 && len(exclAny) > 0 { - // there's a bug somewhere - ds.logger.WarnContext(ctx, "vpp app has both include and exclude labels", "vpp_apps_teams_id", app.VPPAppsTeamsID, "include", fmt.Sprintf("%v", inclAny), "exclude", fmt.Sprintf("%v", exclAny)) + var count int + for _, set := range [][]fleet.SoftwareScopeLabel{exclAny, inclAny, inclAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { + ds.logger.WarnContext(ctx, "vpp app has more than one scope of labels", "vpp_apps_teams_id", app.VPPAppsTeamsID, "include_any", fmt.Sprintf("%v", inclAny), "exclude_any", fmt.Sprintf("%v", exclAny), "include_all", fmt.Sprintf("%v", inclAll)) } app.LabelsExcludeAny = exclAny app.LabelsIncludeAny = inclAny + app.LabelsIncludeAll = inclAll categories, err := ds.getCategoriesForVPPApp(ctx, app.VPPAppsTeamsID) if err != nil { @@ -146,7 +157,8 @@ SELECT label_id, exclude, l.name AS label_name, - va.title_id AS title_id + va.title_id AS title_id, + require_all FROM vpp_app_team_labels vatl JOIN vpp_apps_teams vat ON vat.id = vatl.vpp_app_team_id @@ -344,18 +356,29 @@ func (ds *Datastore) getExistingLabels(ctx context.Context, vppAppTeamID uint) ( } var labels fleet.LabelIdentsWithScope - var exclAny, inclAny []fleet.SoftwareScopeLabel + var exclAny, inclAny, inclAll []fleet.SoftwareScopeLabel for _, l := range existingLabels { - if l.Exclude { + switch { + case l.Exclude && !l.RequireAll: exclAny = append(exclAny, l) - } else { + case !l.Exclude && l.RequireAll: + inclAll = append(inclAll, l) + case !l.Exclude && !l.RequireAll: inclAny = append(inclAny, l) + default: + ds.logger.WarnContext(ctx, "vpp app has an unsupported existing label scope", "vpp_apps_teams_id", vppAppTeamID, "invalid_label", fmt.Sprintf("%#v", l)) } } - if len(inclAny) > 0 && len(exclAny) > 0 { + var count int + for _, set := range [][]fleet.SoftwareScopeLabel{exclAny, inclAny, inclAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { // there's a bug somewhere - return nil, ctxerr.New(ctx, "found both include and exclude labels on a vpp app") + return nil, ctxerr.New(ctx, "found labels for more than one scope on a vpp app") } switch { @@ -374,6 +397,15 @@ func (ds *Datastore) getExistingLabels(ctx context.Context, vppAppTeamID uint) ( labels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID} } return &labels, nil + + case len(inclAll) > 0: + labels.LabelScope = fleet.LabelScopeIncludeAll + labels.ByName = make(map[string]fleet.LabelIdent, len(inclAll)) + for _, l := range inclAll { + labels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID} + } + return &labels, nil + default: return nil, nil } @@ -2329,9 +2361,8 @@ FROM ( vpp_app_team_labels vatl LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id - LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id - AND lm.host_id = ? - WHERE vatl.exclude = 0 AND vpp_apps_teams.platform = 'android' + LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id AND lm.host_id = ? + WHERE vatl.exclude = 0 AND vatl.require_all = 0 AND vpp_apps_teams.platform = 'android' GROUP BY installable_id HAVING count_installer_labels > 0 @@ -2364,20 +2395,39 @@ FROM ( vpp_apps_teams.adam_id AS installable_id FROM vpp_app_team_labels vatl - LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id - JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id - LEFT OUTER JOIN labels lbl ON lbl.id = vatl.label_id - LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id - AND lm.host_id = ? - WHERE vatl.exclude = 1 AND vpp_apps_teams.platform = 'android' + LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id + JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id + LEFT OUTER JOIN labels lbl ON lbl.id = vatl.label_id + LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id AND lm.host_id = ? + WHERE vatl.exclude = 1 AND vatl.require_all = 0 AND vpp_apps_teams.platform = 'android' GROUP BY installable_id HAVING count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels - AND count_host_labels = 0) t; + AND count_host_labels = 0 + + UNION + + -- include all + SELECT + COUNT(*) AS count_installer_labels, + COUNT(lm.label_id) AS count_host_labels, + 0 AS count_host_updated_after_labels, + vpp_apps_teams.adam_id AS installable_id + FROM + vpp_app_team_labels vatl + LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id + JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id + LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id AND lm.host_id = ? + WHERE vatl.exclude = 0 AND vatl.require_all = 1 AND vpp_apps_teams.platform = 'android' + GROUP BY installable_id + HAVING + count_installer_labels > 0 + AND count_host_labels = count_installer_labels + ) t ` - err = sqlx.SelectContext(ctx, ds.reader(ctx), &applicationIDs, stmt, hostID, hostID, hostID, hostID, hostID, hostID) + err = sqlx.SelectContext(ctx, ds.reader(ctx), &applicationIDs, stmt, hostID, hostID, hostID, hostID, hostID, hostID, hostID, hostID) if err != nil { return nil, ctxerr.Wrap(ctx, err, "get in android apps in scope for host") } diff --git a/server/datastore/mysql/vpp_test.go b/server/datastore/mysql/vpp_test.go index 31f80a810a..63c465079a 100644 --- a/server/datastore/mysql/vpp_test.go +++ b/server/datastore/mysql/vpp_test.go @@ -575,6 +575,7 @@ func testVPPApps(t *testing.T, ds *Datastore) { require.Len(t, meta.LabelsIncludeAny, 2) require.Len(t, meta.LabelsExcludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) // insert a VPP app with exclude_any labels labeledApp = &fleet.VPPApp{ @@ -599,6 +600,7 @@ func testVPPApps(t *testing.T, ds *Datastore) { require.Len(t, meta.LabelsIncludeAny, 0) require.Len(t, meta.LabelsExcludeAny, 2) + require.Len(t, meta.LabelsIncludeAll, 0) }) // create a host with some non-VPP software @@ -1939,6 +1941,7 @@ func testSetTeamVPPAppsWithLabels(t *testing.T, ds *Datastore) { require.Len(t, app1Meta.LabelsIncludeAny, 2) require.Len(t, app1Meta.LabelsExcludeAny, 0) + require.Len(t, app1Meta.LabelsIncludeAll, 0) for _, l := range app1Meta.LabelsIncludeAny { _, ok := app1.VPPAppTeam.ValidatedLabels.ByName[l.LabelName] require.True(t, ok) @@ -1946,6 +1949,7 @@ func testSetTeamVPPAppsWithLabels(t *testing.T, ds *Datastore) { require.Len(t, app2Meta.LabelsExcludeAny, 2) require.Len(t, app2Meta.LabelsIncludeAny, 0) + require.Len(t, app2Meta.LabelsIncludeAll, 0) for _, l := range app2Meta.LabelsExcludeAny { _, ok := app2.VPPAppTeam.ValidatedLabels.ByName[l.LabelName] require.True(t, ok) @@ -1998,6 +2002,7 @@ func testSetTeamVPPAppsWithLabels(t *testing.T, ds *Datastore) { require.Len(t, app1Meta.LabelsIncludeAny, 0) require.Len(t, app1Meta.LabelsExcludeAny, 2) + require.Len(t, app1Meta.LabelsIncludeAll, 0) for _, l := range app1Meta.LabelsExcludeAny { _, ok := app1.VPPAppTeam.ValidatedLabels.ByName[l.LabelName] require.True(t, ok) @@ -2005,6 +2010,7 @@ func testSetTeamVPPAppsWithLabels(t *testing.T, ds *Datastore) { require.Len(t, app2Meta.LabelsExcludeAny, 0) require.Len(t, app2Meta.LabelsIncludeAny, 2) + require.Len(t, app2Meta.LabelsIncludeAll, 0) for _, l := range app2Meta.LabelsIncludeAny { _, ok := app2.VPPAppTeam.ValidatedLabels.ByName[l.LabelName] require.True(t, ok) diff --git a/server/fleet/activities.go b/server/fleet/activities.go index e7c7a3de9f..299e1eb43c 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -1091,6 +1091,7 @@ type ActivityTypeAddedSoftware struct { SoftwareTitleID uint `json:"software_title_id"` LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` + LabelsIncludeAll []ActivitySoftwareLabel `json:"labels_include_all,omitempty"` } func (a ActivityTypeAddedSoftware) ActivityName() string { @@ -1106,6 +1107,7 @@ type ActivityTypeEditedSoftware struct { SoftwareIconURL *string `json:"software_icon_url"` LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` + LabelsIncludeAll []ActivitySoftwareLabel `json:"labels_include_all,omitempty"` SoftwareTitleID uint `json:"software_title_id"` SoftwareDisplayName string `json:"software_display_name"` } @@ -1123,6 +1125,7 @@ type ActivityTypeDeletedSoftware struct { SoftwareIconURL *string `json:"software_icon_url"` LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` + LabelsIncludeAll []ActivitySoftwareLabel `json:"labels_include_all,omitempty"` } func (a ActivityTypeDeletedSoftware) ActivityName() string { @@ -1236,6 +1239,7 @@ type ActivityAddedAppStoreApp struct { SelfService bool `json:"self_service"` LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` + LabelsIncludeAll []ActivitySoftwareLabel `json:"labels_include_all,omitempty"` Configuration json.RawMessage `json:"configuration,omitempty"` } @@ -1252,6 +1256,7 @@ type ActivityDeletedAppStoreApp struct { SoftwareIconURL *string `json:"software_icon_url"` LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` + LabelsIncludeAll []ActivitySoftwareLabel `json:"labels_include_all,omitempty"` } func (a ActivityDeletedAppStoreApp) ActivityName() string { @@ -1308,6 +1313,7 @@ type ActivityEditedAppStoreApp struct { SoftwareIconURL *string `json:"software_icon_url"` LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` + LabelsIncludeAll []ActivitySoftwareLabel `json:"labels_include_all,omitempty"` SoftwareDisplayName string `json:"software_display_name"` Configuration json.RawMessage `json:"configuration,omitempty"` AutoUpdateEnabled *bool `json:"auto_update_enabled,omitempty"` diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go index 8205ceb0ed..81d96287e4 100644 --- a/server/fleet/scripts.go +++ b/server/fleet/scripts.go @@ -439,6 +439,7 @@ type SoftwareInstallerPayload struct { InstallDuringSetup *bool `json:"install_during_setup"` // if nil, do not change saved value, otherwise set it LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` + LabelsIncludeAll []string `json:"labels_include_all"` // ValidatedLabels is a struct that contains the validated labels for the // software installer. It is nil if the labels have not been validated. ValidatedLabels *LabelIdentsWithScope diff --git a/server/fleet/service.go b/server/fleet/service.go index eb1d5a3814..2e8fa8963b 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1362,7 +1362,7 @@ type Service interface { // Fleet-maintained apps // AddFleetMaintainedApp adds a Fleet-maintained app to the given team. - AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript, uninstallScript string, selfService bool, automaticInstall bool, labelsIncludeAny, labelsExcludeAny []string) (uint, error) + AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript, uninstallScript string, selfService bool, automaticInstall bool, labelsIncludeAny, labelsExcludeAny, labelsIncludeAll []string) (uint, error) // ListFleetMaintainedApps lists Fleet-maintained apps, including associated software title for supplied team ID (if any) ListFleetMaintainedApps(ctx context.Context, teamID *uint, opts ListOptions) ([]MaintainedApp, *PaginationMetadata, error) // GetFleetMaintainedApp returns a Fleet-maintained app by ID, including associated software title for supplied team ID (if any) diff --git a/server/fleet/software.go b/server/fleet/software.go index dc7d518782..42169afe3e 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -807,6 +807,7 @@ type VPPBatchPayload struct { InstallDuringSetup *bool `json:"install_during_setup"` // keep saved value if nil, otherwise set as indicated LabelsExcludeAny []string `json:"labels_exclude_any"` LabelsIncludeAny []string `json:"labels_include_any"` + LabelsIncludeAll []string `json:"labels_include_all"` // Categories is the list of names of software categories associated with this VPP app. Categories []string `json:"categories"` DisplayName string `json:"display_name"` @@ -834,6 +835,7 @@ type VPPBatchPayloadWithPlatform struct { InstallDuringSetup *bool `json:"install_during_setup"` // keep saved value if nil, otherwise set as indicated LabelsExcludeAny []string `json:"labels_exclude_any"` LabelsIncludeAny []string `json:"labels_include_any"` + LabelsIncludeAll []string `json:"labels_include_all"` // Categories is the list of names of software categories associated with this VPP app. Categories []string `json:"categories"` // CategoryIDs is the list of IDs of software categories associated with this VPP app. diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index 47e1a8258b..e464c7c3ff 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -123,6 +123,8 @@ type SoftwareInstaller struct { LabelsIncludeAny []SoftwareScopeLabel `json:"labels_include_any" db:"labels_include_any"` // LabelsExcludeAny is the list of "exclude any" labels for this software installer (if not nil). LabelsExcludeAny []SoftwareScopeLabel `json:"labels_exclude_any" db:"labels_exclude_any"` + // LabelsIncludeAll is the list of "include all" labels for this software installer (if not nil). + LabelsIncludeAll []SoftwareScopeLabel `json:"labels_include_all" db:"labels_include_all"` // Source is the osquery source for this software. Source string `json:"-" db:"source"` // Categories is the list of categories to which this software belongs: e.g. "Productivity", @@ -522,6 +524,7 @@ type UploadSoftwareInstallerPayload struct { InstallDuringSetup *bool // keep saved value if nil, otherwise set as indicated LabelsIncludeAny []string // names of "include any" labels LabelsExcludeAny []string // names of "exclude any" labels + LabelsIncludeAll []string // names of "include all" labels // ValidatedLabels is a struct that contains the validated labels for the software installer. It // is nil if the labels have not been validated. ValidatedLabels *LabelIdentsWithScope @@ -605,6 +608,7 @@ type UpdateSoftwareInstallerPayload struct { UpgradeCode string LabelsIncludeAny []string // names of "include any" labels LabelsExcludeAny []string // names of "exclude any" labels + LabelsIncludeAll []string // names of "include all" labels // ValidatedLabels is a struct that contains the validated labels for the software installer. It // can be nil if the labels have not been validated or if the labels are not being updated. ValidatedLabels *LabelIdentsWithScope @@ -617,8 +621,8 @@ type UpdateSoftwareInstallerPayload struct { func (u *UpdateSoftwareInstallerPayload) IsNoopPayload(existing *SoftwareTitle) bool { return u.SelfService == nil && u.InstallerFile == nil && u.PreInstallQuery == nil && u.InstallScript == nil && u.PostInstallScript == nil && u.UninstallScript == nil && - u.LabelsIncludeAny == nil && u.LabelsExcludeAny == nil && u.DisplayName == nil && - u.CategoryIDs == nil + u.LabelsIncludeAny == nil && u.LabelsExcludeAny == nil && u.LabelsIncludeAll == nil && + u.DisplayName == nil && u.CategoryIDs == nil } // DownloadSoftwareInstallerPayload is the payload for downloading a software installer. @@ -781,6 +785,7 @@ type SoftwarePackageSpec struct { UninstallScript TeamSpecSoftwareAsset `json:"uninstall_script"` LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` + LabelsIncludeAll []string `json:"labels_include_all"` InstallDuringSetup optjson.Bool `json:"setup_experience"` Icon TeamSpecSoftwareAsset `json:"icon"` @@ -813,8 +818,8 @@ func (spec SoftwarePackageSpec) ResolveSoftwarePackagePaths(baseDir string) Soft } func (spec SoftwarePackageSpec) IncludesFieldsDisallowedInPackageFile() bool { - return len(spec.LabelsExcludeAny) > 0 || len(spec.LabelsIncludeAny) > 0 || len(spec.Categories) > 0 || - spec.SelfService || spec.InstallDuringSetup.Valid + return len(spec.LabelsExcludeAny) > 0 || len(spec.LabelsIncludeAny) > 0 || len(spec.LabelsIncludeAll) > 0 || + len(spec.Categories) > 0 || spec.SelfService || spec.InstallDuringSetup.Valid } func resolveApplyRelativePath(baseDir string, path string) string { @@ -835,6 +840,7 @@ type MaintainedAppSpec struct { UninstallScript TeamSpecSoftwareAsset `json:"uninstall_script"` LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` + LabelsIncludeAll []string `json:"labels_include_all"` Categories []string `json:"categories"` InstallDuringSetup optjson.Bool `json:"setup_experience"` Icon TeamSpecSoftwareAsset `json:"icon"` @@ -851,6 +857,7 @@ func (spec MaintainedAppSpec) ToSoftwarePackageSpec() SoftwarePackageSpec { SelfService: spec.SelfService, LabelsIncludeAny: spec.LabelsIncludeAny, LabelsExcludeAny: spec.LabelsExcludeAny, + LabelsIncludeAll: spec.LabelsIncludeAll, InstallDuringSetup: spec.InstallDuringSetup, Icon: spec.Icon, Categories: spec.Categories, @@ -1076,10 +1083,11 @@ func NewTempFileReader(from io.Reader, tempDirFn func() string) (*TempFileReader // NOTE: depending on how/where this struct is used, fields MAY BE // UNRELIABLE insofar as they represent default, empty values. type SoftwareScopeLabel struct { - LabelName string `db:"label_name" json:"name"` - LabelID uint `db:"label_id" json:"id"` // label id in database, which may be the empty value in some cases where id is not known in advance (e.g., if labels are created during gitops processing) - Exclude bool `db:"exclude" json:"-"` // not rendered in JSON, used when processing LabelsIncludeAny and LabelsExcludeAny on parent title (may be the empty value in some cases) - TitleID uint `db:"title_id" json:"-"` // not rendered in JSON, used to store the associated title ID (may be the empty value in some cases) + LabelName string `db:"label_name" json:"name"` + LabelID uint `db:"label_id" json:"id"` // label id in database, which may be the empty value in some cases where id is not known in advance (e.g., if labels are created during gitops processing) + Exclude bool `db:"exclude" json:"-"` // not rendered in JSON, used when processing LabelsIncludeAll, LabelsIncludeAny and LabelsExcludeAny on parent title (may be the empty value in some cases) + TitleID uint `db:"title_id" json:"-"` // not rendered in JSON, used to store the associated title ID (may be the empty value in some cases) + RequireAll bool `db:"require_all" json:"-"` // not rendered in JSON, used when processing LabelsIncludeAll, LabelsIncludeAny and LabelsExcludeAny on parent title (may be the empty value in some cases) } // Max total attempts (including initial) for a non-policy software install. diff --git a/server/fleet/software_title_icons.go b/server/fleet/software_title_icons.go index b502812dee..182873c3a3 100644 --- a/server/fleet/software_title_icons.go +++ b/server/fleet/software_title_icons.go @@ -67,4 +67,5 @@ type DetailsForSoftwareIconActivity struct { Platform *InstallableDevicePlatform `json:"platform"` LabelsIncludeAny []ActivitySoftwareLabel `db:"-"` LabelsExcludeAny []ActivitySoftwareLabel `db:"-"` + LabelsIncludeAll []ActivitySoftwareLabel `db:"-"` } diff --git a/server/fleet/teams.go b/server/fleet/teams.go index bee65ada9c..9a0f586779 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -274,6 +274,7 @@ type TeamSpecAppStoreApp struct { SelfService bool `json:"self_service"` LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` + LabelsIncludeAll []string `json:"labels_include_all"` // Categories is the list of names of software categories associated with this VPP app. Categories []string `json:"categories"` // InstallDuringSetup indicates whether a package should be incorporated into setup experience; @@ -332,7 +333,7 @@ func (t *TeamMDM) Copy() *TeamMDM { clone := *t - // EnableDiskEncryption, MacOSUpdates and MacOSSetup don't have fields that + // EnableDiskEncryption, MacOS/IOS/IPadOS/WindowsUpdates don't have fields that // require cloning (all fields are basic value types, no // pointers/slices/maps). diff --git a/server/fleet/vpp.go b/server/fleet/vpp.go index 07a770c7fa..ec132992a5 100644 --- a/server/fleet/vpp.go +++ b/server/fleet/vpp.go @@ -30,14 +30,18 @@ type VPPAppTeam struct { // to false), while if not nil, it will update the flag's value in the DB. InstallDuringSetup *bool `db:"install_during_setup" json:"-"` // LabelsIncludeAny are the names of labels associated with this app. If a host has any of - // these labels, the app is in scope for that host. If this field is set, LabelsExcludeAny + // these labels, the app is in scope for that host. If this field is set, other label fields // cannot be set. LabelsIncludeAny []string `json:"labels_include_any"` // LabelsExcludeAny are the names of labels associated with this app. If a host has any of - // these labels, the app is out of scope for that host. If this field is set, LabelsIncludeAny + // these labels, the app is out of scope for that host. If this field is set, other label fields // cannot be set. LabelsExcludeAny []string `json:"labels_exclude_any"` - // ValidatedLabels are the labels (either include or exclude any) that have been validated by + // LabelsIncludeAll are the names of labels associated with this app. If a host has all of + // these labels, the app is in scope for that host. If this field is set, other label fields + // cannot be set. + LabelsIncludeAll []string `json:"labels_include_all"` + // ValidatedLabels are the labels (either include any/all or exclude any) that have been validated by // Fleet as being valid labels. This field is only used internally. ValidatedLabels *LabelIdentsWithScope `json:"-"` // AddAutoInstallPolicy indicates whether or not we should create an auto-install policy for @@ -115,6 +119,8 @@ type VPPAppStoreApp struct { LabelsIncludeAny []SoftwareScopeLabel `json:"labels_include_any" db:"labels_include_any"` // LabelsExcludeAny is the list of "exclude any" labels for this app store app (if not nil). LabelsExcludeAny []SoftwareScopeLabel `json:"labels_exclude_any" db:"labels_exclude_any"` + // LabelsIncludeAll is the list of "include all" labels for this app store app (if not nil). + LabelsIncludeAll []SoftwareScopeLabel `json:"labels_include_all" db:"labels_include_all"` // BundleIdentifier is the bundle identifier for this app. BundleIdentifier string `json:"-" db:"bundle_identifier"` // AddedAt is when the VPP app was added to the team @@ -188,6 +194,7 @@ type AppStoreAppUpdatePayload struct { SelfService *bool LabelsIncludeAny []string LabelsExcludeAny []string + LabelsIncludeAll []string Categories []string DisplayName *string Configuration json.RawMessage diff --git a/server/mock/service/service_mock.go b/server/mock/service/service_mock.go index 7c64c34b72..3a16a7a656 100644 --- a/server/mock/service/service_mock.go +++ b/server/mock/service/service_mock.go @@ -839,7 +839,7 @@ type MaybeCancelPendingSetupExperienceStepsFunc func(ctx context.Context, host * type IsAllSetupExperienceSoftwareRequiredFunc func(ctx context.Context, host *fleet.Host) (bool, error) -type AddFleetMaintainedAppFunc func(ctx context.Context, teamID *uint, appID uint, installScript string, preInstallQuery string, postInstallScript string, uninstallScript string, selfService bool, automaticInstall bool, labelsIncludeAny []string, labelsExcludeAny []string) (uint, error) +type AddFleetMaintainedAppFunc func(ctx context.Context, teamID *uint, appID uint, installScript string, preInstallQuery string, postInstallScript string, uninstallScript string, selfService bool, automaticInstall bool, labelsIncludeAny []string, labelsExcludeAny []string, labelsIncludeAll []string) (uint, error) type ListFleetMaintainedAppsFunc func(ctx context.Context, teamID *uint, opts fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) @@ -5063,11 +5063,11 @@ func (s *Service) IsAllSetupExperienceSoftwareRequired(ctx context.Context, host return s.IsAllSetupExperienceSoftwareRequiredFunc(ctx, host) } -func (s *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript string, preInstallQuery string, postInstallScript string, uninstallScript string, selfService bool, automaticInstall bool, labelsIncludeAny []string, labelsExcludeAny []string) (uint, error) { +func (s *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript string, preInstallQuery string, postInstallScript string, uninstallScript string, selfService bool, automaticInstall bool, labelsIncludeAny []string, labelsExcludeAny []string, labelsIncludeAll []string) (uint, error) { s.mu.Lock() s.AddFleetMaintainedAppFuncInvoked = true s.mu.Unlock() - return s.AddFleetMaintainedAppFunc(ctx, teamID, appID, installScript, preInstallQuery, postInstallScript, uninstallScript, selfService, automaticInstall, labelsIncludeAny, labelsExcludeAny) + return s.AddFleetMaintainedAppFunc(ctx, teamID, appID, installScript, preInstallQuery, postInstallScript, uninstallScript, selfService, automaticInstall, labelsIncludeAny, labelsExcludeAny, labelsIncludeAll) } func (s *Service) ListFleetMaintainedApps(ctx context.Context, teamID *uint, opts fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) { diff --git a/server/service/client.go b/server/service/client.go index 872b0b0d76..0868908c3d 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -898,6 +898,7 @@ func (c *Client) ApplyGroup( InstallDuringSetup: installDuringSetup, LabelsExcludeAny: app.LabelsExcludeAny, LabelsIncludeAny: app.LabelsIncludeAny, + LabelsIncludeAll: app.LabelsIncludeAll, Categories: app.Categories, DisplayName: app.DisplayName, IconPath: app.Icon.Path, @@ -1279,6 +1280,7 @@ func buildSoftwarePackagesPayload(specs []fleet.SoftwarePackageSpec, installDuri InstallDuringSetup: installDuringSetup, LabelsIncludeAny: si.LabelsIncludeAny, LabelsExcludeAny: si.LabelsExcludeAny, + LabelsIncludeAll: si.LabelsIncludeAll, SHA256: sha256Value, Categories: si.Categories, DisplayName: si.DisplayName, diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index ba1234412a..72573dd5fd 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -12060,6 +12060,7 @@ func checkSoftwareInstaller(t *testing.T, ds *mysql.Datastore, payload *fleet.Up byName[l.LabelName] = struct{}{} require.Equal(t, *meta2.TitleID, l.TitleID) require.False(t, l.Exclude) + require.False(t, l.RequireAll) } require.Len(t, byName, len(payload.LabelsIncludeAny)) for _, l := range payload.LabelsIncludeAny { @@ -12074,6 +12075,7 @@ func checkSoftwareInstaller(t *testing.T, ds *mysql.Datastore, payload *fleet.Up byName[l.LabelName] = struct{}{} require.Equal(t, *meta2.TitleID, l.TitleID) require.True(t, l.Exclude) + require.False(t, l.RequireAll) } require.Len(t, byName, len(payload.LabelsExcludeAny)) for _, l := range payload.LabelsExcludeAny { @@ -12081,6 +12083,21 @@ func checkSoftwareInstaller(t *testing.T, ds *mysql.Datastore, payload *fleet.Up require.True(t, ok) } + // check labels include all + require.Len(t, meta2.LabelsIncludeAll, len(payload.LabelsIncludeAll)) + byName = make(map[string]struct{}, len(meta2.LabelsIncludeAll)) + for _, l := range meta2.LabelsIncludeAll { + byName[l.LabelName] = struct{}{} + require.Equal(t, *meta2.TitleID, l.TitleID) + require.False(t, l.Exclude) + require.True(t, l.RequireAll) + } + require.Len(t, byName, len(payload.LabelsIncludeAll)) + for _, l := range payload.LabelsIncludeAll { + _, ok := byName[l] + require.True(t, ok) + } + return meta.InstallerID, *meta.TitleID } @@ -12126,6 +12143,21 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD Query: "select 1", }}, http.StatusOK, &labelResp) require.NotZero(t, labelResp.Label.ID) + lblA := labelResp.Label + + s.DoJSON("POST", "/api/latest/fleet/labels", &createLabelRequest{fleet.LabelPayload{ + Name: "label_b" + t.Name(), + Query: "select 1", + }}, http.StatusOK, &labelResp) + require.NotZero(t, labelResp.Label.ID) + lblB := labelResp.Label + + s.DoJSON("POST", "/api/latest/fleet/labels", &createLabelRequest{fleet.LabelPayload{ + Name: "label_c" + t.Name(), + Query: "select 1", + }}, http.StatusOK, &labelResp) + require.NotZero(t, labelResp.Label.ID) + lblC := labelResp.Label payload := &fleet.UploadSoftwareInstallerPayload{ InstallScript: "some install script", @@ -12141,6 +12173,60 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD LabelsIncludeAny: []string{t.Name()}, } + // validate that providing more than 1 type of label + // results in an error + testCases := []struct { + desc string + incAny []string + exclAny []string + incAll []string + }{ + { + desc: "include_any_exclude_any", + incAny: []string{lblA.Name}, + exclAny: []string{lblB.Name}, + }, + { + desc: "include_any_include_all", + incAny: []string{lblA.Name}, + incAll: []string{lblB.Name}, + }, + { + desc: "exclude_any_include_all", + exclAny: []string{lblA.Name}, + incAll: []string{lblB.Name}, + }, + { + desc: "all_types", + incAny: []string{lblA.Name}, + exclAny: []string{lblB.Name}, + incAll: []string{lblC.Name}, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + + payload := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "some install script", + PreInstallQuery: "some pre install query", + PostInstallScript: "some post install script", + Filename: "ruby.deb", + // additional fields below are pre-populated so we can re-use the payload later for the test assertions + Title: "ruby", + Version: "1:2.5.1", + Source: "deb_packages", + StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628", + Platform: "linux", + LabelsIncludeAny: tc.incAny, + LabelsIncludeAll: tc.incAll, + LabelsExcludeAny: tc.exclAny, + } + + s.uploadSoftwareInstaller(t, payload, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included.`) + + }) + } + s.uploadSoftwareInstaller(t, payload, http.StatusOK, "") // check the software installer @@ -12149,7 +12235,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD // check activity activityData := fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": null, "fleet_name": null, "fleet_id": null, "self_service": false, "software_title_id": %d, "labels_include_any": [{"id": %d, "name": %q}]}`, - titleID, labelResp.Label.ID, t.Name()) + titleID, lblA.ID, lblA.Name) s.lastActivityMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), activityData, 0) // upload again fails @@ -12167,7 +12253,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD }, http.StatusOK, "") activityData = fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "software_icon_url": null, "team_name": null, "team_id": null, "fleet_name": null, "fleet_id": null, "self_service": true, "software_title_id": %d, "labels_include_any": [{"id": %d, "name": %q}], "software_display_name": ""}`, - titleID, labelResp.Label.ID, t.Name()) + titleID, lblA.ID, lblA.Name) s.lastActivityMatches(fleet.ActivityTypeEditedSoftware{}.ActivityName(), activityData, 0) // patch the software installer to change the labels body, headers := generateMultipartRequest(t, "", "", nil, s.token, map[string][]string{ @@ -12177,12 +12263,12 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD s.DoRawWithHeaders("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package", titleID), body.Bytes(), http.StatusOK, headers) expectedPayload := *payload expectedPayload.LabelsIncludeAny = nil - expectedPayload.LabelsExcludeAny = []string{labelResp.Label.Name} + expectedPayload.LabelsExcludeAny = []string{lblA.Name} checkSoftwareInstaller(t, s.ds, &expectedPayload) // Create a host and assign the label to it host := createOrbitEnrolledHost(t, "linux", "label_host", s.ds) - err = s.ds.RecordLabelQueryExecutions(context.Background(), host, map[uint]*bool{labelResp.Label.ID: ptr.Bool(true)}, time.Now(), false) + err = s.ds.RecordLabelQueryExecutions(context.Background(), host, map[uint]*bool{lblA.ID: ptr.Bool(true)}, time.Now(), false) require.NoError(t, err) // Attempt to install. Should fail because label is "exclude any" @@ -12196,8 +12282,8 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD }) s.DoRawWithHeaders("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package", titleID), body.Bytes(), http.StatusOK, headers) expectedPayload.PreInstallQuery = "some other pre install query" - expectedPayload.LabelsIncludeAny = nil // no change - expectedPayload.LabelsExcludeAny = []string{labelResp.Label.Name} // no change + expectedPayload.LabelsIncludeAny = nil // no change + expectedPayload.LabelsExcludeAny = []string{lblA.Name} // no change checkSoftwareInstaller(t, s.ds, &expectedPayload) // update the label to be "include any". This should allow for the installation to happen. @@ -12205,7 +12291,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD w3 := multipart.NewWriter(&b3) require.NoError(t, w3.WriteField("team_id", "0")) require.NoError(t, w3.WriteField("pre_install_query", "some other pre install query")) - require.NoError(t, w3.WriteField("labels_include_any", labelResp.Label.Name)) + require.NoError(t, w3.WriteField("labels_include_any", lblA.Name)) w3.Close() headers = map[string]string{ "Content-Type": w3.FormDataContentType(), @@ -12214,7 +12300,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD } s.DoRawWithHeaders("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package", titleID), b3.Bytes(), http.StatusOK, headers) expectedPayload.PreInstallQuery = "some other pre install query" - expectedPayload.LabelsIncludeAny = []string{labelResp.Label.Name} + expectedPayload.LabelsIncludeAny = []string{lblA.Name} expectedPayload.LabelsExcludeAny = nil checkSoftwareInstaller(t, s.ds, &expectedPayload) @@ -12227,7 +12313,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD activityData = fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "software_icon_url": null, "team_name": null, "team_id": null, "fleet_name": null, "fleet_id": null, "self_service": true, "labels_include_any": [{"id": %d, "name": %q}], "software_title_id": %d, "software_display_name": ""}`, - labelResp.Label.ID, labelResp.Label.Name, titleID) + lblA.ID, lblA.Name, titleID) s.lastActivityMatches(fleet.ActivityTypeEditedSoftware{}.ActivityName(), activityData, 0) // orbit-downloading fails with invalid orbit node key @@ -12246,7 +12332,69 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", titleID), nil, http.StatusNoContent, "team_id", "0") activityData = fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "software_icon_url": null, "team_name": null, "team_id": null, "fleet_name": null, "fleet_id": null, "self_service": true, "labels_include_any": [{"id": %d, "name": %q}]}`, - labelResp.Label.ID, labelResp.Label.Name) + lblA.ID, lblA.Name) + s.lastActivityMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), activityData, 0) + }) + + t.Run("upload no team software installer with labels_include_all", func(t *testing.T) { + // create a label to use for include_all scoping + var labelResp createLabelResponse + s.DoJSON("POST", "/api/latest/fleet/labels", &createLabelRequest{fleet.LabelPayload{ + Name: t.Name(), + Query: "select 1", + }}, http.StatusOK, &labelResp) + require.NotZero(t, labelResp.Label.ID) + + payload := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "some install script", + PreInstallQuery: "some pre install query", + Filename: "ruby.deb", + Title: "ruby", + Version: "1:2.5.1", + Source: "deb_packages", + StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628", + Platform: "linux", + LabelsIncludeAll: []string{labelResp.Label.Name}, + } + + s.uploadSoftwareInstaller(t, payload, http.StatusOK, "") + + // check the software installer metadata: LabelsIncludeAll should be persisted + _, titleID := checkSoftwareInstaller(t, s.ds, payload) + + // check that the added-software activity carries labels_include_all + activityData := fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, + "team_id": null, "fleet_name": null, "fleet_id": null, "self_service": false, "software_title_id": %d, "labels_include_all": [{"id": %d, "name": %q}]}`, + titleID, labelResp.Label.ID, t.Name()) + s.lastActivityMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), activityData, 0) + + // patch the installer to update an unrelated field; labels_include_all should be preserved + s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{ + SelfService: ptr.Bool(true), + InstallScript: ptr.String("some install script"), + PreInstallQuery: ptr.String("some pre install query"), + Filename: "ruby.deb", + TitleID: titleID, + TeamID: nil, + }, http.StatusOK, "") + + // the edited-software activity should still carry labels_include_all + activityData = fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "software_icon_url": null, "team_name": null, + "team_id": null, "fleet_name": null, "fleet_id": null, "self_service": true, "software_title_id": %d, "labels_include_all": [{"id": %d, "name": %q}], "software_display_name": ""}`, + titleID, labelResp.Label.ID, t.Name()) + s.lastActivityMatches(fleet.ActivityTypeEditedSoftware{}.ActivityName(), activityData, 0) + + // create a host and assign the label — install should succeed since host has all required labels + host := createOrbitEnrolledHost(t, "linux", "include_all_label_host", s.ds) + err = s.ds.RecordLabelQueryExecutions(context.Background(), host, map[uint]*bool{labelResp.Label.ID: ptr.Bool(true)}, time.Now(), false) + require.NoError(t, err) + s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", host.ID, titleID), nil, http.StatusAccepted) + + // delete the installer; the deleted-software activity should carry labels_include_all + s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", titleID), nil, http.StatusNoContent, "team_id", "0") + activityData = fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "software_icon_url": null, "team_name": null, + "team_id": null, "fleet_name": null, "fleet_id": null, "self_service": true, "labels_include_all": [{"id": %d, "name": %q}]}`, + labelResp.Label.ID, t.Name()) s.lastActivityMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), activityData, 0) }) @@ -13223,8 +13371,6 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() { t := s.T() ctx := context.Background() - fmt.Printf("dev_mode.Env(\"FLEET_DEV_BATCH_RETRY_INTERVAL\"): %v\n", dev_mode.Env("FLEET_DEV_BATCH_RETRY_INTERVAL")) - // non-existent team s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{}, http.StatusNotFound, "team_name", "foo") @@ -13432,24 +13578,65 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() { titlesResp.SoftwareTitles[0].SoftwarePackage.SelfService = ptr.Bool(true) require.Equal(t, titlesResp, newTitlesResp) - // create some labels A and B + // create some labels A, B and C lblA, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "A"}) require.NoError(t, err) lblB, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "B"}) require.NoError(t, err) + lblC, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "C"}) + require.NoError(t, err) - // providing both labels include/exclude results in an error - softwareToInstall = []*fleet.SoftwareInstallerPayload{ - {URL: rubyURL, LabelsIncludeAny: []string{lblA.Name}, LabelsExcludeAny: []string{lblB.Name}}, + // validate that providing more than 1 type of label + // results in an error + testCases := []struct { + desc string + incAny []string + exclAny []string + incAll []string + }{ + { + desc: "include_any_exclude_any", + incAny: []string{lblA.Name}, + exclAny: []string{lblB.Name}, + }, + { + desc: "include_any_include_all", + incAny: []string{lblA.Name}, + incAll: []string{lblB.Name}, + }, + { + desc: "exclude_any_include_all", + exclAny: []string{lblA.Name}, + incAll: []string{lblB.Name}, + }, + { + desc: "all_types", + incAny: []string{lblA.Name}, + exclAny: []string{lblB.Name}, + incAll: []string{lblC.Name}, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + softwareToInstall = []*fleet.SoftwareInstallerPayload{ + { + URL: rubyURL, + LabelsIncludeAny: tc.incAny, + LabelsExcludeAny: tc.exclAny, + LabelsIncludeAll: tc.incAll, + }, + } + res := s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusBadRequest) + assert.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included.`) + + }) } - res := s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusBadRequest) - require.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included.`) // providing a non-existing label results in an error softwareToInstall = []*fleet.SoftwareInstallerPayload{ {URL: rubyURL, LabelsIncludeAny: []string{"no-such-label"}}, } - res = s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusBadRequest) + res := s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusBadRequest) require.Contains(t, extractServerErrorText(res.Body), `Couldn't update. Label "no-such-label" doesn't exist. Please remove the label from the software.`) // valid installer scoped by label @@ -13465,6 +13652,7 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() { meta, err := s.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, *packages[0].TitleID, false) require.NoError(t, err) require.Empty(t, meta.LabelsExcludeAny) + require.Empty(t, meta.LabelsIncludeAll) require.Len(t, meta.LabelsIncludeAny, 1) require.Equal(t, lblA.ID, meta.LabelsIncludeAny[0].LabelID) require.Equal(t, lblA.Name, meta.LabelsIncludeAny[0].LabelName) @@ -13533,7 +13721,7 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() { // with a label softwareToInstall = []*fleet.SoftwareInstallerPayload{ - {Slug: &maintained1.Slug, LabelsIncludeAny: []string{lblA.Name}}, + {Slug: &maintained1.Slug, LabelsIncludeAll: []string{lblA.Name}}, } s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse) packages = waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, "", batchResponse.RequestUUID) @@ -13544,9 +13732,10 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() { meta, err = s.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, *packages[0].TitleID, false) require.NoError(t, err) require.Empty(t, meta.LabelsExcludeAny) - require.Len(t, meta.LabelsIncludeAny, 1) - require.Equal(t, lblA.ID, meta.LabelsIncludeAny[0].LabelID) - require.Equal(t, lblA.Name, meta.LabelsIncludeAny[0].LabelName) + require.Empty(t, meta.LabelsIncludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, lblA.ID, meta.LabelsIncludeAll[0].LabelID) + require.Equal(t, lblA.Name, meta.LabelsIncludeAll[0].LabelName) // maintained app with no_check for sha, latest for version maintained2, err := s.ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ @@ -13611,7 +13800,7 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() { http.DefaultTransport = mockTransport softwareToInstall = []*fleet.SoftwareInstallerPayload{ - {Slug: &maintained2.Slug}, + {Slug: &maintained2.Slug, LabelsIncludeAll: []string{lblA.Name}}, } s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse) packages = waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, "", batchResponse.RequestUUID) @@ -13627,6 +13816,16 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() { s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", *packages[0].TitleID), nil, http.StatusOK, &titleResponse, "team_id", "0") require.Equal(t, "1.0.0", titleResponse.SoftwareTitle.SoftwarePackage.Version) + meta, err = s.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, *packages[0].TitleID, false) + require.NoError(t, err) + + // Validate labels + require.Empty(t, meta.LabelsExcludeAny) + require.Empty(t, meta.LabelsIncludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, lblA.ID, meta.LabelsIncludeAll[0].LabelID) + require.Equal(t, lblA.Name, meta.LabelsIncludeAll[0].LabelName) + http.DefaultTransport = oldTransport } @@ -18214,6 +18413,179 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, vimInstallerID) require.NoError(t, err) require.NotNil(t, host1LastInstall) + + // --- include_all tests --- + // + // Use a dedicated team so we can reuse the ruby.deb and vim.deb testdata + // files without conflicting with the no-team installers already uploaded above. + var inclAllTeamResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", fleet.Team{Name: t.Name() + "-include-all"}, http.StatusOK, &inclAllTeamResp) + inclAllTeam := inclAllTeamResp.Team + err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&inclAllTeam.ID, []uint{host.ID})) + require.NoError(t, err) + + // Host has lbl1 and lbl2. lbl3 is not on the host (lbl3 was never added + // via RecordLabelQueryExecutions in this test above). + + // Upload ruby.deb with labels_include_all: [lbl1, lbl2]. + // Host has both labels, so it IS in scope and install should be attempted. + rubyIncludeAllPayload := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "some deb install script", + Filename: "ruby.deb", + TeamID: &inclAllTeam.ID, + LabelsIncludeAll: []string{lbl1.Name, lbl2.Name}, + Platform: "linux", + } + s.uploadSoftwareInstaller(t, rubyIncludeAllPayload, http.StatusOK, "") + + resp = listSoftwareTitlesResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/software/titles", + listSoftwareTitlesRequest{}, + http.StatusOK, &resp, + "query", "ruby", + "team_id", fmt.Sprint(inclAllTeam.ID), + ) + require.Len(t, resp.SoftwareTitles, 1) + require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage) + rubyInclAllTitleID := resp.SoftwareTitles[0].ID + + var rubyInclAllDetail getSoftwareTitleResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", rubyInclAllTitleID), nil, http.StatusOK, &rubyInclAllDetail, "team_id", fmt.Sprint(inclAllTeam.ID)) + require.NotNil(t, rubyInclAllDetail.SoftwareTitle) + require.NotNil(t, rubyInclAllDetail.SoftwareTitle.SoftwarePackage) + rubyInclAllInstallerID := rubyInclAllDetail.SoftwareTitle.SoftwarePackage.InstallerID + + policy3, err := s.ds.NewTeamPolicy(ctx, inclAllTeam.ID, nil, fleet.PolicyPayload{ + Name: "policy3", + Query: "SELECT 3;", + Platform: "linux", + }) + require.NoError(t, err) + + mtplr = modifyTeamPolicyResponse{} + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", inclAllTeam.ID, policy3.ID), modifyTeamPolicyRequest{ + ModifyPolicyPayload: fleet.ModifyPolicyPayload{ + SoftwareTitleID: optjson.Any[uint]{Set: true, Valid: true, Value: rubyInclAllTitleID}, + }, + }, http.StatusOK, &mtplr) + + host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, rubyInclAllInstallerID) + require.NoError(t, err) + require.Nil(t, host1LastInstall) + + // Send back a failed result for policy3. + distributedResp = submitDistributedQueryResultsResponse{} + s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults( + host, + map[uint]*bool{ + policy3.ID: ptr.Bool(false), + }, + ), http.StatusOK, &distributedResp) + err = s.ds.UpdateHostPolicyCounts(ctx) + require.NoError(t, err) + policy3, err = s.ds.Policy(ctx, policy3.ID) + require.NoError(t, err) + // Host has all required labels, so the installer is in scope and the policy is marked failed. + require.Equal(t, uint(0), policy3.PassingHostCount) + require.Equal(t, uint(1), policy3.FailingHostCount) + + // Installation attempt was made for ruby, because host has all required labels. + host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, rubyInclAllInstallerID) + require.NoError(t, err) + require.NotNil(t, host1LastInstall) + + // Upload vim.deb with labels_include_all: [lbl1, lbl3]. + // Host has lbl1 but NOT lbl3, so it is NOT in scope and install should be skipped. + vimIncludeAllPayload := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "some deb install script", + Filename: "vim.deb", + TeamID: &inclAllTeam.ID, + LabelsIncludeAll: []string{lbl1.Name, lbl3.Name}, + Platform: "linux", + } + s.uploadSoftwareInstaller(t, vimIncludeAllPayload, http.StatusOK, "") + + resp = listSoftwareTitlesResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/software/titles", + listSoftwareTitlesRequest{}, + http.StatusOK, &resp, + "query", "vim", + "team_id", fmt.Sprint(inclAllTeam.ID), + ) + require.Len(t, resp.SoftwareTitles, 1) + require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage) + vimInclAllTitleID := resp.SoftwareTitles[0].ID + + var vimInclAllDetail getSoftwareTitleResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", vimInclAllTitleID), nil, http.StatusOK, &vimInclAllDetail, "team_id", fmt.Sprint(inclAllTeam.ID)) + require.NotNil(t, vimInclAllDetail.SoftwareTitle) + require.NotNil(t, vimInclAllDetail.SoftwareTitle.SoftwarePackage) + vimInclAllInstallerID := vimInclAllDetail.SoftwareTitle.SoftwarePackage.InstallerID + + policy4, err := s.ds.NewTeamPolicy(ctx, inclAllTeam.ID, nil, fleet.PolicyPayload{ + Name: "policy4", + Query: "SELECT 4;", + Platform: "linux", + }) + require.NoError(t, err) + + mtplr = modifyTeamPolicyResponse{} + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", inclAllTeam.ID, policy4.ID), modifyTeamPolicyRequest{ + ModifyPolicyPayload: fleet.ModifyPolicyPayload{ + SoftwareTitleID: optjson.Any[uint]{Set: true, Valid: true, Value: vimInclAllTitleID}, + }, + }, http.StatusOK, &mtplr) + + host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, vimInclAllInstallerID) + require.NoError(t, err) + require.Nil(t, host1LastInstall) + + // Send back a failed result for policy4. + distributedResp = submitDistributedQueryResultsResponse{} + s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults( + host, + map[uint]*bool{ + policy4.ID: ptr.Bool(false), + }, + ), http.StatusOK, &distributedResp) + err = s.ds.UpdateHostPolicyCounts(ctx) + require.NoError(t, err) + policy4, err = s.ds.Policy(ctx, policy4.ID) + require.NoError(t, err) + // Host is missing lbl3, so the installer is not in scope. Policy should not be counted as failed. + require.Equal(t, uint(0), policy4.PassingHostCount) + require.Equal(t, uint(0), policy4.FailingHostCount) + + // No installation attempt for vim, because host is missing one of the required labels. + host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, vimInclAllInstallerID) + require.NoError(t, err) + require.Nil(t, host1LastInstall) + + // Now add lbl3 to the host and re-run the policy failure. vim should now be in scope. + err = s.ds.RecordLabelQueryExecutions(context.Background(), host, map[uint]*bool{lbl3.ID: ptr.Bool(true)}, time.Now(), false) + require.NoError(t, err) + + distributedResp = submitDistributedQueryResultsResponse{} + s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults( + host, + map[uint]*bool{ + policy4.ID: ptr.Bool(false), + }, + ), http.StatusOK, &distributedResp) + err = s.ds.UpdateHostPolicyCounts(ctx) + require.NoError(t, err) + policy4, err = s.ds.Policy(ctx, policy4.ID) + require.NoError(t, err) + // Host now has all required labels, so the policy is marked as failed. + require.Equal(t, uint(0), policy4.PassingHostCount) + require.Equal(t, uint(1), policy4.FailingHostCount) + + // Installation attempt was made for vim now that the host has all required labels. + host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, vimInclAllInstallerID) + require.NoError(t, err) + require.NotNil(t, host1LastInstall) } func (s *integrationEnterpriseTestSuite) TestPolicyAutomationLabelScopingRetrigger() { @@ -19795,6 +20167,7 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { swTitle = titleResp.SoftwareTitle require.NotNil(t, swTitle.SoftwarePackage) require.Empty(t, swTitle.SoftwarePackage.LabelsExcludeAny) + require.Empty(t, swTitle.SoftwarePackage.LabelsIncludeAll) require.Len(t, swTitle.SoftwarePackage.LabelsIncludeAny, 2) gotNames := make(map[string]bool) for _, lbl := range swTitle.SoftwarePackage.LabelsIncludeAny { @@ -19822,7 +20195,7 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { req.LabelsExcludeAny = []string{lbl1.Name} addMAResp = addFleetMaintainedAppResponse{} r = s.Do("POST", "/api/latest/fleet/software/fleet_maintained_apps", req, http.StatusBadRequest) - require.Contains(t, extractServerErrorText(r.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included`) + require.Contains(t, extractServerErrorText(r.Body), `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included`) } func (s *integrationEnterpriseTestSuite) TestUpgradeCodesFromMaintainedApps() { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 447d7ffcaa..61fb6e09ac 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -12303,6 +12303,27 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() { require.NoError(t, err) require.Len(t, assoc, 1) + // Add a label + clr := createLabelResponse{} + s.DoJSON("POST", "/api/latest/fleet/labels", createLabelRequest{ + LabelPayload: fleet.LabelPayload{ + Name: "label1" + t.Name(), + Query: "SELECT 1;", + }, + }, http.StatusOK, &clr) + + label1 := clr.Label + + clr = createLabelResponse{} + s.DoJSON("POST", "/api/latest/fleet/labels", createLabelRequest{ + LabelPayload: fleet.LabelPayload{ + Name: "label2" + t.Name(), + Query: "SELECT 2;", + }, + }, http.StatusOK, &clr) + + label2 := clr.Label + // Associating two apps we own beforeAssociation := time.Now() s.DoJSON("POST", @@ -12310,7 +12331,7 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() { batchAssociateAppStoreAppsRequest{ Apps: []fleet.VPPBatchPayload{ {AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}, - {AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: true, Categories: []string{"Browsers"}}, + {AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: true, Categories: []string{"Browsers"}, LabelsIncludeAll: []string{label1.Name, label2.Name}}, }, }, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name, ) @@ -12332,6 +12353,11 @@ func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() { var getSWTitle getSoftwareTitleResponse s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", st.ID), nil, http.StatusOK, &getSWTitle, "team_id", fmt.Sprint(tmGood.ID)) s.Assert().ElementsMatch([]string{"Browsers"}, getSWTitle.SoftwareTitle.AppStoreApp.Categories) + var labelNames []string + for _, l := range getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAll { + labelNames = append(labelNames, l.LabelName) + } + s.Assert().ElementsMatch([]string{label1.Name, label2.Name}, labelNames) } } @@ -13249,7 +13275,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() { LabelsExcludeAny: []string{l2.Name}, } res := s.Do("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusBadRequest) - require.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included`) + require.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included`) // Now add it for real addAppReq.LabelsExcludeAny = []string{} @@ -13266,6 +13292,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() { require.NotNil(t, getSWTitle.SoftwareTitle.AppStoreApp) require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.AdamID, includeAnyApp.AdamID) require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsExcludeAny) + require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAll) require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAny, []fleet.SoftwareScopeLabel{{LabelName: l1.Name, LabelID: l1.ID}}) // Add an app with exclude_any labels @@ -13299,6 +13326,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() { require.NotNil(t, getSWTitle.SoftwareTitle.AppStoreApp) require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.AdamID, excludeAnyApp.AdamID) require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAny) + require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAll) require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsExcludeAny, []fleet.SoftwareScopeLabel{{LabelName: l2.Name, LabelID: l2.ID}}) require.True(t, getSWTitle.SoftwareTitle.AppStoreApp.SelfService) @@ -13335,7 +13363,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() { updateAppReq.LabelsIncludeAny = []string{l1.Name} updateAppReq.LabelsExcludeAny = []string{l1.Name} res = s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusBadRequest) - require.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included.`) + require.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included.`) // Attempt to update with a non-existent label. Should fail. updateAppReq.LabelsExcludeAny = []string{} @@ -13352,6 +13380,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() { require.Equal(t, updateAppResp.AppStoreApp.AdamID, excludeAnyApp.AdamID) require.Equal(t, updateAppResp.AppStoreApp.LabelsIncludeAny, []fleet.SoftwareScopeLabel{{LabelName: l2.Name, LabelID: l2.ID}}) require.Empty(t, updateAppResp.AppStoreApp.LabelsExcludeAny) + require.Empty(t, updateAppResp.AppStoreApp.LabelsIncludeAll) require.False(t, updateAppResp.AppStoreApp.SelfService) require.Equal(t, fleet.MacOSPlatform, updateAppResp.AppStoreApp.Platform) @@ -13367,6 +13396,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() { require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.AdamID, excludeAnyApp.AdamID) require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAny, []fleet.SoftwareScopeLabel{{LabelName: l2.Name, LabelID: l2.ID}}) require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsExcludeAny) + require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAll) require.False(t, getSWTitle.SoftwareTitle.AppStoreApp.SelfService) // Attempt an install on the host. This should fail because the host doesn't have the label diff --git a/server/service/maintained_apps.go b/server/service/maintained_apps.go index 36c7b9dcdb..06ec3954c8 100644 --- a/server/service/maintained_apps.go +++ b/server/service/maintained_apps.go @@ -7,7 +7,7 @@ import ( "net/http" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" + maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" ) type addFleetMaintainedAppRequest struct { @@ -20,6 +20,7 @@ type addFleetMaintainedAppRequest struct { UninstallScript string `json:"uninstall_script"` LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` + LabelsIncludeAll []string `json:"labels_include_all"` AutomaticInstall bool `json:"automatic_install"` Categories []string `json:"categories"` } @@ -82,6 +83,7 @@ func addFleetMaintainedAppEndpoint(ctx context.Context, request interface{}, svc req.AutomaticInstall, req.LabelsIncludeAny, req.LabelsExcludeAny, + req.LabelsIncludeAll, ) if err != nil { if errors.Is(err, context.DeadlineExceeded) { @@ -93,7 +95,7 @@ func addFleetMaintainedAppEndpoint(ctx context.Context, request interface{}, svc return &addFleetMaintainedAppResponse{SoftwareTitleID: titleId}, nil } -func (svc *Service) AddFleetMaintainedApp(ctx context.Context, _ *uint, _ uint, _, _, _, _ string, _ bool, _ bool, _, _ []string) (uint, error) { +func (svc *Service) AddFleetMaintainedApp(ctx context.Context, _ *uint, _ uint, _, _, _, _ string, _ bool, _ bool, _, _, _ []string) (uint, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) diff --git a/server/service/software_installers.go b/server/service/software_installers.go index 8feb6fc7f0..c5efc2c2b4 100644 --- a/server/service/software_installers.go +++ b/server/service/software_installers.go @@ -35,6 +35,7 @@ type uploadSoftwareInstallerRequest struct { UninstallScript string LabelsIncludeAny []string LabelsExcludeAny []string + LabelsIncludeAll []string AutomaticInstall bool } @@ -49,6 +50,7 @@ type updateSoftwareInstallerRequest struct { SelfService *bool LabelsIncludeAny []string LabelsExcludeAny []string + LabelsIncludeAll []string Categories []string DisplayName *string } @@ -144,8 +146,8 @@ func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http } // decode labels and categories - var inclAny, exclAny, categories []string - var existsInclAny, existsExclAny, existsCategories bool + var inclAny, exclAny, inclAll, categories []string + var existsInclAny, existsExclAny, existsInclAll, existsCategories bool inclAny, existsInclAny = r.MultipartForm.Value[string(fleet.LabelsIncludeAny)] switch { @@ -167,6 +169,16 @@ func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http decoded.LabelsExcludeAny = exclAny } + inclAll, existsInclAll = r.MultipartForm.Value[string(fleet.LabelsIncludeAll)] + switch { + case !existsInclAll: + decoded.LabelsIncludeAll = nil + case len(inclAll) == 1 && inclAll[0] == "": + decoded.LabelsIncludeAll = []string{} + default: + decoded.LabelsIncludeAll = inclAll + } + categories, existsCategories = r.MultipartForm.Value["categories"] switch { case !existsCategories: @@ -235,6 +247,7 @@ func updateSoftwareInstallerEndpoint(ctx context.Context, request interface{}, s SelfService: req.SelfService, LabelsIncludeAny: req.LabelsIncludeAny, LabelsExcludeAny: req.LabelsExcludeAny, + LabelsIncludeAll: req.LabelsIncludeAll, Categories: req.Categories, DisplayName: req.DisplayName, } @@ -355,8 +368,8 @@ func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http } // decode labels - var inclAny, exclAny []string - var existsInclAny, existsExclAny bool + var inclAny, exclAny, inclAll []string + var existsInclAny, existsExclAny, existsInclAll bool inclAny, existsInclAny = r.MultipartForm.Value[string(fleet.LabelsIncludeAny)] switch { @@ -378,6 +391,16 @@ func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http decoded.LabelsExcludeAny = exclAny } + inclAll, existsInclAll = r.MultipartForm.Value[string(fleet.LabelsIncludeAll)] + switch { + case !existsInclAll: + decoded.LabelsIncludeAll = nil + case len(inclAll) == 1 && inclAll[0] == "": + decoded.LabelsIncludeAll = []string{} + default: + decoded.LabelsIncludeAll = inclAll + } + val, ok = r.MultipartForm.Value["automatic_install"] if ok && len(val) > 0 && val[0] != "" { parsed, err := strconv.ParseBool(val[0]) @@ -434,6 +457,7 @@ func uploadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, s UninstallScript: req.UninstallScript, LabelsIncludeAny: req.LabelsIncludeAny, LabelsExcludeAny: req.LabelsExcludeAny, + LabelsIncludeAll: req.LabelsIncludeAll, AutomaticInstall: req.AutomaticInstall, } diff --git a/server/service/software_installers_test.go b/server/service/software_installers_test.go index 957ec49504..625569fd07 100644 --- a/server/service/software_installers_test.go +++ b/server/service/software_installers_test.go @@ -212,7 +212,7 @@ func TestValidateSoftwareLabels(t *testing.T) { t.Run("validate no update", func(t *testing.T) { t.Run("no auth context", func(t *testing.T) { - _, err := eeservice.ValidateSoftwareLabels(context.Background(), svc, nil, nil, nil) + _, err := eeservice.ValidateSoftwareLabels(context.Background(), svc, nil, nil, nil, nil) require.ErrorContains(t, err, "Authentication required") }) @@ -220,7 +220,7 @@ func TestValidateSoftwareLabels(t *testing.T) { ctx = authz_ctx.NewContext(ctx, &authCtx) t.Run("no auth checked", func(t *testing.T) { - _, err := eeservice.ValidateSoftwareLabels(ctx, svc, nil, nil, nil) + _, err := eeservice.ValidateSoftwareLabels(ctx, svc, nil, nil, nil, nil) require.ErrorContains(t, err, "Authentication required") }) @@ -267,6 +267,7 @@ func TestValidateSoftwareLabels(t *testing.T) { name string payloadIncludeAny []string payloadExcludeAny []string + payloadIncludeAll []string expectLabels map[string]fleet.LabelIdent expectScope fleet.LabelScope expectError string @@ -276,6 +277,7 @@ func TestValidateSoftwareLabels(t *testing.T) { nil, nil, nil, + nil, "", "", }, @@ -283,6 +285,7 @@ func TestValidateSoftwareLabels(t *testing.T) { "include labels", []string{"foo", "bar"}, nil, + nil, map[string]fleet.LabelIdent{ "foo": {LabelID: 1, LabelName: "foo"}, "bar": {LabelID: 2, LabelName: "bar"}, @@ -294,6 +297,7 @@ func TestValidateSoftwareLabels(t *testing.T) { "exclude labels", nil, []string{"bar", "baz"}, + nil, map[string]fleet.LabelIdent{ "bar": {LabelID: 2, LabelName: "bar"}, "baz": {LabelID: 3, LabelName: "baz"}, @@ -306,14 +310,16 @@ func TestValidateSoftwareLabels(t *testing.T) { []string{"foo"}, []string{"bar"}, nil, + nil, "", - `Only one of "labels_include_any" or "labels_exclude_any" can be included.`, + `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included.`, }, { "non-existent label", []string{"foo", "qux"}, nil, nil, + nil, "", `Couldn't update. Label "qux" doesn't exist. Please remove the label from the software`, }, @@ -321,6 +327,7 @@ func TestValidateSoftwareLabels(t *testing.T) { "duplicate label", []string{"foo", "foo"}, nil, + nil, map[string]fleet.LabelIdent{ "foo": {LabelID: 1, LabelName: "foo"}, }, @@ -332,6 +339,7 @@ func TestValidateSoftwareLabels(t *testing.T) { nil, []string{}, nil, + nil, "", "", }, @@ -340,13 +348,14 @@ func TestValidateSoftwareLabels(t *testing.T) { nil, []string{""}, nil, + nil, "", `Couldn't update. Label "" doesn't exist. Please remove the label from the software`, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - got, err := eeservice.ValidateSoftwareLabels(ctx, svc, nil, tt.payloadIncludeAny, tt.payloadExcludeAny) + got, err := eeservice.ValidateSoftwareLabels(ctx, svc, nil, tt.payloadIncludeAny, tt.payloadExcludeAny, tt.payloadIncludeAll) if tt.expectError != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.expectError) @@ -362,7 +371,7 @@ func TestValidateSoftwareLabels(t *testing.T) { t.Run("validate update", func(t *testing.T) { t.Run("no auth context", func(t *testing.T) { - _, _, err := eeservice.ValidateSoftwareLabelsForUpdate(context.Background(), svc, nil, nil, nil) + _, _, err := eeservice.ValidateSoftwareLabelsForUpdate(context.Background(), svc, nil, nil, nil, nil) require.ErrorContains(t, err, "Authentication required") }) @@ -370,7 +379,7 @@ func TestValidateSoftwareLabels(t *testing.T) { ctx = authz_ctx.NewContext(ctx, &authCtx) t.Run("no auth checked", func(t *testing.T) { - _, _, err := eeservice.ValidateSoftwareLabelsForUpdate(ctx, svc, nil, nil, nil) + _, _, err := eeservice.ValidateSoftwareLabelsForUpdate(ctx, svc, nil, nil, nil, nil) require.ErrorContains(t, err, "Authentication required") }) @@ -402,6 +411,7 @@ func TestValidateSoftwareLabels(t *testing.T) { existingInstaller *fleet.SoftwareInstaller payloadIncludeAny []string payloadExcludeAny []string + payloadIncludeAll []string shouldUpdate bool expectLabels map[string]fleet.LabelIdent expectScope fleet.LabelScope @@ -412,6 +422,7 @@ func TestValidateSoftwareLabels(t *testing.T) { nil, nil, []string{"foo"}, + nil, false, nil, "", @@ -422,6 +433,7 @@ func TestValidateSoftwareLabels(t *testing.T) { &fleet.SoftwareInstaller{}, nil, nil, + nil, false, nil, "", @@ -435,6 +447,7 @@ func TestValidateSoftwareLabels(t *testing.T) { }, []string{"foo", "bar"}, nil, + nil, true, map[string]fleet.LabelIdent{ "foo": {LabelID: 1, LabelName: "foo"}, @@ -451,6 +464,7 @@ func TestValidateSoftwareLabels(t *testing.T) { }, nil, []string{"foo"}, + nil, true, map[string]fleet.LabelIdent{ "foo": {LabelID: 1, LabelName: "foo"}, @@ -466,6 +480,7 @@ func TestValidateSoftwareLabels(t *testing.T) { }, []string{}, nil, + nil, true, nil, "", @@ -479,6 +494,7 @@ func TestValidateSoftwareLabels(t *testing.T) { }, []string{"foo"}, nil, + nil, false, nil, "", @@ -488,7 +504,7 @@ func TestValidateSoftwareLabels(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - shouldUpate, got, err := eeservice.ValidateSoftwareLabelsForUpdate(ctx, svc, tt.existingInstaller, tt.payloadIncludeAny, tt.payloadExcludeAny) + shouldUpate, got, err := eeservice.ValidateSoftwareLabelsForUpdate(ctx, svc, tt.existingInstaller, tt.payloadIncludeAny, tt.payloadExcludeAny, tt.payloadIncludeAll) if tt.expectError != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.expectError) diff --git a/server/service/software_title_icons_test.go b/server/service/software_title_icons_test.go index d41a0b7239..8b04a7285f 100644 --- a/server/service/software_title_icons_test.go +++ b/server/service/software_title_icons_test.go @@ -379,6 +379,7 @@ func TestDeleteSoftwareTitleIcon(t *testing.T) { Platform: nil, LabelsIncludeAny: nil, LabelsExcludeAny: nil, + LabelsIncludeAll: nil, }, nil } ds.DeleteSoftwareTitleIconFunc = func(ctx context.Context, teamID uint, titleID uint) error { @@ -401,6 +402,7 @@ func TestDeleteSoftwareTitleIcon(t *testing.T) { SoftwareIconURL: ptr.String(""), LabelsIncludeAny: nil, LabelsExcludeAny: nil, + LabelsIncludeAll: nil, SoftwareTitleID: 1, } require.Equal(t, expectedActivity, capturedActivity) @@ -426,6 +428,7 @@ func TestDeleteSoftwareTitleIcon(t *testing.T) { Platform: &platform, LabelsIncludeAny: nil, LabelsExcludeAny: nil, + LabelsIncludeAll: nil, }, nil } ds.DeleteSoftwareTitleIconFunc = func(ctx context.Context, teamID uint, titleID uint) error { @@ -450,6 +453,7 @@ func TestDeleteSoftwareTitleIcon(t *testing.T) { SoftwareIconURL: ptr.String("fleetdm.com/icon.png"), // note this is supposed to be the vpp_apps.icon_url LabelsIncludeAny: nil, LabelsExcludeAny: nil, + LabelsIncludeAll: nil, } require.Equal(t, expectedActivity, capturedActivity) }, diff --git a/server/service/testing_client.go b/server/service/testing_client.go index 4196a4f260..cee5a3644d 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -765,6 +765,11 @@ func (ts *withServer) uploadSoftwareInstallerWithErrorNameReason( require.NoError(t, w.WriteField("labels_exclude_any", l)) } } + if payload.LabelsIncludeAll != nil { + for _, l := range payload.LabelsIncludeAll { + require.NoError(t, w.WriteField("labels_include_all", l)) + } + } if payload.AutomaticInstall { require.NoError(t, w.WriteField("automatic_install", "true")) } @@ -847,6 +852,11 @@ func (ts *withServer) updateSoftwareInstaller( require.NoError(t, w.WriteField("labels_exclude_any", l)) } } + if payload.LabelsIncludeAll != nil { + for _, l := range payload.LabelsIncludeAll { + require.NoError(t, w.WriteField("labels_include_all", l)) + } + } if payload.Categories != nil { for _, c := range payload.Categories { require.NoError(t, w.WriteField("categories", c)) diff --git a/server/service/vpp.go b/server/service/vpp.go index c79cd42120..ab5a92338e 100644 --- a/server/service/vpp.go +++ b/server/service/vpp.go @@ -59,6 +59,7 @@ type addAppStoreAppRequest struct { AutomaticInstall bool `json:"automatic_install"` LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` + LabelsIncludeAll []string `json:"labels_include_all"` Categories []string `json:"categories"` Configuration json.RawMessage `json:"configuration,omitempty"` } @@ -77,6 +78,7 @@ func addAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet. SelfService: req.SelfService, LabelsIncludeAny: req.LabelsIncludeAny, LabelsExcludeAny: req.LabelsExcludeAny, + LabelsIncludeAll: req.LabelsIncludeAll, AddAutoInstallPolicy: req.AutomaticInstall, Categories: req.Categories, Configuration: req.Configuration, @@ -106,6 +108,7 @@ type updateAppStoreAppRequest struct { SelfService *bool `json:"self_service"` LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` + LabelsIncludeAll []string `json:"labels_include_all"` Categories []string `json:"categories"` Configuration json.RawMessage `json:"configuration,omitempty"` DisplayName *string `json:"display_name"` @@ -133,6 +136,7 @@ func updateAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fle SelfService: req.SelfService, LabelsIncludeAny: req.LabelsIncludeAny, LabelsExcludeAny: req.LabelsExcludeAny, + LabelsIncludeAll: req.LabelsIncludeAll, Categories: req.Categories, Configuration: req.Configuration, DisplayName: req.DisplayName, diff --git a/tools/cloner-check/generated_files/teamconfig.txt b/tools/cloner-check/generated_files/teamconfig.txt index db2a0bad09..dddee046ca 100644 --- a/tools/cloner-check/generated_files/teamconfig.txt +++ b/tools/cloner-check/generated_files/teamconfig.txt @@ -108,6 +108,7 @@ github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec PostInstallScript f github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec UninstallScript fleet.TeamSpecSoftwareAsset github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec LabelsIncludeAny []string github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec LabelsExcludeAny []string +github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec LabelsIncludeAll []string github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec InstallDuringSetup optjson.Bool github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec Icon fleet.TeamSpecSoftwareAsset github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec Slug *string @@ -129,6 +130,7 @@ github.com/fleetdm/fleet/v4/server/fleet/MaintainedAppSpec PostInstallScript fle github.com/fleetdm/fleet/v4/server/fleet/MaintainedAppSpec UninstallScript fleet.TeamSpecSoftwareAsset github.com/fleetdm/fleet/v4/server/fleet/MaintainedAppSpec LabelsIncludeAny []string github.com/fleetdm/fleet/v4/server/fleet/MaintainedAppSpec LabelsExcludeAny []string +github.com/fleetdm/fleet/v4/server/fleet/MaintainedAppSpec LabelsIncludeAll []string github.com/fleetdm/fleet/v4/server/fleet/MaintainedAppSpec Categories []string github.com/fleetdm/fleet/v4/server/fleet/MaintainedAppSpec InstallDuringSetup optjson.Bool github.com/fleetdm/fleet/v4/server/fleet/MaintainedAppSpec Icon fleet.TeamSpecSoftwareAsset @@ -140,6 +142,7 @@ github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp AppStoreID string github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp SelfService bool github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp LabelsIncludeAny []string github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp LabelsExcludeAny []string +github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp LabelsIncludeAll []string github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp Categories []string github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp InstallDuringSetup optjson.Bool github.com/fleetdm/fleet/v4/server/fleet/TeamSpecAppStoreApp Icon fleet.TeamSpecSoftwareAsset