Backend: Support labels_include_all for installers/apps (#41324)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40721 

# Checklist for submitter

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

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [ ] QA'd all new/changed functionality manually

I (Martin) did test `labels_include_all` for FMA, custom installer, IPA
and VPP apps, and it seemed to all work great for gitops apply and
gitops generate, **except for VPP apps** which seem to have 2 important
pre-existing bugs, see
https://github.com/fleetdm/fleet/issues/40723#issuecomment-4041780707

## New Fleet configuration settings

- [ ] Verified that the setting is exported via `fleetctl
generate-gitops`
- [ ] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [ ] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [ ] Verified that any relevant UI is disabled when GitOps mode is
enabled

---------

Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
This commit is contained in:
Martin Angers 2026-03-18 13:27:53 -04:00 committed by GitHub
parent f8fa379732
commit ba04887100
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1628 additions and 239 deletions

View file

@ -0,0 +1 @@
- Added support for `labels_include_all` conditional scoping for software installers and apps.

View file

@ -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
}

View file

@ -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,

View file

@ -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)
}

View file

@ -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"

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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)

View file

@ -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)
}

View file

@ -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{},
},
}

View file

@ -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")
}

View file

@ -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")
}

View file

@ -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)

View file

@ -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
}

View file

@ -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")

View file

@ -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,

View file

@ -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,
},

View file

@ -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

View file

@ -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))

View file

@ -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 {

View file

@ -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
)
)

View file

@ -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
`

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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)
}},
}

View file

@ -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")
}

View file

@ -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)

View file

@ -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"`

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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.

View file

@ -67,4 +67,5 @@ type DetailsForSoftwareIconActivity struct {
Platform *InstallableDevicePlatform `json:"platform"`
LabelsIncludeAny []ActivitySoftwareLabel `db:"-"`
LabelsExcludeAny []ActivitySoftwareLabel `db:"-"`
LabelsIncludeAll []ActivitySoftwareLabel `db:"-"`
}

View file

@ -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).

View file

@ -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

View file

@ -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) {

View file

@ -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,

View file

@ -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() {

View file

@ -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

View file

@ -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)

View file

@ -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,
}

View file

@ -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)

View file

@ -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)
},

View file

@ -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))

View file

@ -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,

View file

@ -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