Add combined include/exclude label targeting for MDM profiles (API and GitOps) (#46437)
**Related issue:** Resolves #45180 # 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, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * MDM profiles can combine label inclusion (include-all/include-any) with exclusion (exclude-any) so you can target hosts by labels while excluding specific labeled hosts. * Profile validation now enforces a single include-mode and explicitly rejects any label used in both include and exclude lists. * **Bug Fixes** * Deleting a label that’s referenced by an MDM configuration profile or declaration is blocked and returns an error to prevent broken targeting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
* `labels_exclude_any` can now be combined with `labels_include_all` or `labels_include_any` when uploading MDM configuration profiles, allowing hosts to be included by label membership and excluded by another set of labels simultaneously.
|
||||
* Fleet now prevents deleting a label that is in use by an MDM configuration profile or declaration, returning an error instead of silently breaking the profile's label targeting.
|
||||
@@ -851,24 +851,19 @@ func getLabelUsage(config *spec.GitOps) (map[string][]LabelUsage, error) {
|
||||
if osSettings, ok := getCustomSettings(osSettingName); ok {
|
||||
for _, setting := range osSettings {
|
||||
var labels []string
|
||||
err := fmt.Errorf("configuration profile '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`.", filepath.Base(setting.Path))
|
||||
|
||||
if len(setting.LabelsIncludeAny) > 0 {
|
||||
labels = setting.LabelsIncludeAny
|
||||
}
|
||||
if len(setting.LabelsIncludeAll) > 0 && len(setting.LabelsIncludeAny) > 0 {
|
||||
return nil, fmt.Errorf("Couldn't edit configuration profiles. For profile '%s', only one of \"labels_include_all\" or \"labels_include_any\" can be included.", filepath.Base(setting.Path))
|
||||
}
|
||||
if len(setting.LabelsIncludeAll) > 0 {
|
||||
if len(labels) > 0 {
|
||||
return nil, err
|
||||
}
|
||||
labels = setting.LabelsIncludeAll
|
||||
}
|
||||
if len(setting.LabelsExcludeAny) > 0 {
|
||||
if len(labels) > 0 {
|
||||
return nil, err
|
||||
}
|
||||
labels = setting.LabelsExcludeAny
|
||||
if overlap := fleet.ProfileLabelOverlap(labels, setting.LabelsExcludeAny); overlap != "" {
|
||||
return nil, fmt.Errorf("configuration profile '%s': label %q cannot appear in both include and exclude lists.", filepath.Base(setting.Path), overlap)
|
||||
}
|
||||
|
||||
labels = append(labels, setting.LabelsExcludeAny...)
|
||||
updateLabelUsage(labels, filepath.Base(setting.Path), "configuration profile", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,68 +3,115 @@ package fleetctl
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/pkg/spec"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func makeProfileConfig(t *testing.T, profiles []fleet.MDMProfileSpec, osName string) *spec.GitOps {
|
||||
t.Helper()
|
||||
config := &spec.GitOps{}
|
||||
switch osName {
|
||||
case "macos":
|
||||
config.Controls.MacOSSettings = &fleet.MacOSSettings{CustomSettings: profiles}
|
||||
case "windows":
|
||||
config.Controls.WindowsSettings = &fleet.WindowsSettings{CustomSettings: optjson.SetSlice(profiles)}
|
||||
default:
|
||||
t.Fatalf("unknown os: %s", osName)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func TestGetLabelUsageProfilePathShortened(t *testing.T) {
|
||||
// Simulate what happens when spec parsing resolves a relative path to absolute.
|
||||
absPath := "/home/runner/work/Detroit-GitOps-Workshop/Detroit-GitOps-Workshop/lib/macos/configuration-profiles/disable-bluetooth-file-sharing.mobileconfig"
|
||||
profiles := []fleet.MDMProfileSpec{{Path: absPath, LabelsIncludeAll: []string{"nonexistent-label"}}}
|
||||
|
||||
config := &spec.GitOps{
|
||||
Controls: spec.GitOpsControls{
|
||||
MacOSSettings: &fleet.MacOSSettings{
|
||||
CustomSettings: []fleet.MDMProfileSpec{
|
||||
{
|
||||
Path: absPath,
|
||||
LabelsIncludeAll: []string{"nonexistent-label"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
for _, osName := range []string{"macos", "windows"} {
|
||||
t.Run(osName, func(t *testing.T) {
|
||||
usage, err := getLabelUsage(makeProfileConfig(t, profiles, osName))
|
||||
require.NoError(t, err)
|
||||
|
||||
// The label "nonexistent-label" should be in the usage map.
|
||||
entries, ok := usage["nonexistent-label"]
|
||||
require.True(t, ok, "expected label to be in usage map")
|
||||
require.Len(t, entries, 1)
|
||||
|
||||
// The Name should be the base filename, not the full absolute path.
|
||||
assert.Equal(t, "disable-bluetooth-file-sharing.mobileconfig", entries[0].Name,
|
||||
"profile path should be shortened to just the filename")
|
||||
|
||||
// The Type should be "configuration profile", not "MDM Profile".
|
||||
assert.Equal(t, "configuration profile", entries[0].Type,
|
||||
"type should say 'configuration profile' not 'MDM Profile'")
|
||||
})
|
||||
}
|
||||
|
||||
usage, err := getLabelUsage(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The label "nonexistent-label" should be in the usage map.
|
||||
entries, ok := usage["nonexistent-label"]
|
||||
require.True(t, ok, "expected label to be in usage map")
|
||||
require.Len(t, entries, 1)
|
||||
|
||||
// The Name should be the base filename, not the full absolute path.
|
||||
assert.Equal(t, "disable-bluetooth-file-sharing.mobileconfig", entries[0].Name,
|
||||
"profile path should be shortened to just the filename")
|
||||
|
||||
// The Type should be "configuration profile", not "MDM Profile".
|
||||
assert.Equal(t, "configuration profile", entries[0].Type,
|
||||
"type should say 'configuration profile' not 'MDM Profile'")
|
||||
}
|
||||
|
||||
func TestGetLabelUsageMultipleLabelKeysError(t *testing.T) {
|
||||
absPath := "/absolute/path/to/profile.mobileconfig"
|
||||
profiles := []fleet.MDMProfileSpec{{
|
||||
Path: absPath,
|
||||
LabelsIncludeAll: []string{"label-a"},
|
||||
LabelsIncludeAny: []string{"label-b"},
|
||||
}}
|
||||
|
||||
config := &spec.GitOps{
|
||||
Controls: spec.GitOpsControls{
|
||||
MacOSSettings: &fleet.MacOSSettings{
|
||||
CustomSettings: []fleet.MDMProfileSpec{
|
||||
{
|
||||
Path: absPath,
|
||||
LabelsIncludeAll: []string{"label-a"},
|
||||
LabelsIncludeAny: []string{"label-b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Two include modes together is still invalid.
|
||||
for _, osName := range []string{"macos", "windows"} {
|
||||
t.Run(osName, func(t *testing.T) {
|
||||
_, err := getLabelUsage(makeProfileConfig(t, profiles, osName))
|
||||
require.Error(t, err)
|
||||
|
||||
// Error should use "configuration profile" and the short filename.
|
||||
assert.Contains(t, err.Error(), "configuration profile")
|
||||
assert.Contains(t, err.Error(), "profile.mobileconfig")
|
||||
assert.NotContains(t, err.Error(), "/absolute/path/to/")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLabelUsageIncludeExcludeOverlapError(t *testing.T) {
|
||||
absPath := "/absolute/path/to/profile.mobileconfig"
|
||||
|
||||
// A label appearing in both include and exclude is rejected before any apply.
|
||||
cases := []fleet.MDMProfileSpec{
|
||||
{Path: absPath, LabelsIncludeAll: []string{"shared-label"}, LabelsExcludeAny: []string{"shared-label"}},
|
||||
{Path: absPath, LabelsIncludeAny: []string{"shared-label"}, LabelsExcludeAny: []string{"shared-label"}},
|
||||
{Path: absPath, LabelsIncludeAll: []string{"ok-label", "shared-label"}, LabelsExcludeAny: []string{"shared-label", "other-label"}},
|
||||
}
|
||||
for _, profileSpec := range cases {
|
||||
for _, osName := range []string{"macos", "windows"} {
|
||||
t.Run(osName, func(t *testing.T) {
|
||||
_, err := getLabelUsage(makeProfileConfig(t, []fleet.MDMProfileSpec{profileSpec}, osName))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "shared-label")
|
||||
assert.Contains(t, err.Error(), "profile.mobileconfig")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLabelUsageIncludeAndExcludeAllowed(t *testing.T) {
|
||||
absPath := "/absolute/path/to/profile.mobileconfig"
|
||||
|
||||
// include + exclude combination is valid for configuration profiles.
|
||||
cases := []fleet.MDMProfileSpec{
|
||||
{Path: absPath, LabelsIncludeAll: []string{"include-label"}, LabelsExcludeAny: []string{"exclude-label"}},
|
||||
{Path: absPath, LabelsIncludeAny: []string{"include-label"}, LabelsExcludeAny: []string{"exclude-label"}},
|
||||
{Path: absPath, LabelsExcludeAny: []string{"exclude-label"}},
|
||||
}
|
||||
for _, profileSpec := range cases {
|
||||
for _, osName := range []string{"macos", "windows"} {
|
||||
t.Run(osName, func(t *testing.T) {
|
||||
usage, err := getLabelUsage(makeProfileConfig(t, []fleet.MDMProfileSpec{profileSpec}, osName))
|
||||
require.NoError(t, err)
|
||||
allLabels := append(profileSpec.LabelsIncludeAll, append(profileSpec.LabelsIncludeAny, profileSpec.LabelsExcludeAny...)...) //nolint:gocritic
|
||||
for _, label := range allLabels {
|
||||
assert.Contains(t, usage, label)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_, err := getLabelUsage(config)
|
||||
require.Error(t, err)
|
||||
|
||||
// Error should use "configuration profile" and the short filename.
|
||||
assert.Contains(t, err.Error(), "configuration profile")
|
||||
assert.Contains(t, err.Error(), "profile.mobileconfig")
|
||||
assert.NotContains(t, err.Error(), "/absolute/path/to/")
|
||||
}
|
||||
|
||||
@@ -3647,13 +3647,13 @@ func TestGitOpsCustomSettings(t *testing.T) {
|
||||
}{
|
||||
{"testdata/gitops/global_macos_windows_custom_settings_valid.yml", ""},
|
||||
{"testdata/gitops/global_macos_custom_settings_valid_deprecated.yml", ""},
|
||||
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
|
||||
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
|
||||
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml", `only one of "labels_include_all" or "labels_include_any" can be included`},
|
||||
{"testdata/gitops/global_windows_custom_settings_invalid_label_mix_2.yml", `only one of "labels_include_all" or "labels_include_any" can be included`},
|
||||
{"testdata/gitops/global_windows_custom_settings_unknown_label.yml", `Please create the missing labels, or update your settings to not refer to these labels.`},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_valid.yml", ""},
|
||||
{"testdata/gitops/team_macos_custom_settings_valid_deprecated.yml", ""},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml", "please choose one of `labels_include_any`, `labels_include_all` or `labels_exclude_any`"},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix.yml", `only one of "labels_include_all", "labels_include_any" or "labels" can be included`},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_invalid_labels_mix_2.yml", `only one of "labels_include_all", "labels_include_any" or "labels" can be included`},
|
||||
{"testdata/gitops/team_macos_windows_custom_settings_unknown_label.yml", `Please create the missing labels, or update your settings to not refer to these labels.`},
|
||||
}
|
||||
for _, c := range cases {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ controls:
|
||||
- path: ./lib/windows-screenlock.xml
|
||||
labels_include_all:
|
||||
- B
|
||||
labels_exclude_any:
|
||||
labels_include_any:
|
||||
- C
|
||||
scripts:
|
||||
enable_disk_encryption: false
|
||||
|
||||
@@ -1440,9 +1440,9 @@ func (svc *Service) createTeamFromSpec(
|
||||
if enableDiskEncryption && svc.config.Server.PrivateKey == "" {
|
||||
return nil, ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
validateTeamCustomSettings(invalid, "macos", macOSSettings.CustomSettings)
|
||||
validateTeamCustomSettings(invalid, "windows", spec.MDM.WindowsSettings.CustomSettings.Value)
|
||||
validateTeamCustomSettings(invalid, "android", spec.MDM.AndroidSettings.CustomSettings.Value)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "macos", macOSSettings.CustomSettings)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "windows", spec.MDM.WindowsSettings.CustomSettings.Value)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "android", spec.MDM.AndroidSettings.CustomSettings.Value)
|
||||
|
||||
var hostExpirySettings fleet.HostExpirySettings
|
||||
if spec.HostExpirySettings != nil {
|
||||
@@ -1845,9 +1845,9 @@ func (svc *Service) editTeamFromSpec(
|
||||
team.Config.HostExpirySettings = *spec.HostExpirySettings
|
||||
}
|
||||
|
||||
validateTeamCustomSettings(invalid, "apple", team.Config.MDM.MacOSSettings.CustomSettings)
|
||||
validateTeamCustomSettings(invalid, "windows", team.Config.MDM.WindowsSettings.CustomSettings.Value)
|
||||
validateTeamCustomSettings(invalid, "android", team.Config.MDM.AndroidSettings.CustomSettings.Value)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "apple", team.Config.MDM.MacOSSettings.CustomSettings)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "windows", team.Config.MDM.WindowsSettings.CustomSettings.Value)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "android", team.Config.MDM.AndroidSettings.CustomSettings.Value)
|
||||
|
||||
// If host status webhook is not provided, do not change it
|
||||
if spec.WebhookSettings.HostStatusWebhook != nil {
|
||||
@@ -2061,25 +2061,6 @@ func (svc *Service) editTeamFromSpec(
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateTeamCustomSettings(invalid *fleet.InvalidArgumentError, prefix string, customSettings []fleet.MDMProfileSpec) {
|
||||
for i, prof := range customSettings {
|
||||
count := 0
|
||||
for _, b := range []bool{len(prof.Labels) > 0, len(prof.LabelsIncludeAll) > 0, len(prof.LabelsIncludeAny) > 0, len(prof.LabelsExcludeAny) > 0} {
|
||||
if b {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
invalid.Append(fmt.Sprintf("%s_settings.configuration_profiles", prefix),
|
||||
fmt.Sprintf(`Couldn't edit %s_settings.configuration_profiles. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`, prefix))
|
||||
}
|
||||
if len(prof.Labels) > 0 {
|
||||
customSettings[i].LabelsIncludeAll = customSettings[i].Labels
|
||||
customSettings[i].Labels = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *Service) validateTeamCalendarIntegrations(
|
||||
calendarIntegration *fleet.TeamGoogleCalendarIntegration,
|
||||
appCfg *fleet.AppConfig, dryRun bool, invalid *fleet.InvalidArgumentError,
|
||||
|
||||
@@ -309,7 +309,7 @@ func testNewMDMAppleConfigProfileDuplicateIdentifier(t *testing.T, ds *Datastore
|
||||
// simulate a broken label by nullifying its id in the join table
|
||||
// (direct DeleteLabel is now blocked when referenced by a profile)
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE label_id = ?`, lbl.ID)
|
||||
_, err := q.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE apple_profile_uuid = ? AND label_id = ?`, labelProf.ProfileUUID, lbl.ID)
|
||||
return err
|
||||
})
|
||||
|
||||
|
||||
@@ -699,6 +699,19 @@ func (ds *Datastore) DeleteLabel(ctx context.Context, name string, filter fleet.
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "getting label id to delete")
|
||||
}
|
||||
var usedByProfile bool
|
||||
if err := sqlx.GetContext(ctx, tx, &usedByProfile, `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM mdm_configuration_profile_labels WHERE label_id = ?
|
||||
UNION ALL
|
||||
SELECT 1 FROM mdm_declaration_labels WHERE label_id = ?
|
||||
)`, labelID, labelID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "checking if label is used by configuration profiles")
|
||||
}
|
||||
if usedByProfile {
|
||||
return ctxerr.Wrap(ctx, foreignKey("configuration profile labels", name), "delete label")
|
||||
}
|
||||
|
||||
if err := deleteLabelsInTx(ctx, tx, []uint{labelID}); err != nil {
|
||||
if isMySQLForeignKey(err) {
|
||||
return ctxerr.Wrap(ctx, foreignKey("labels", name), "delete label")
|
||||
|
||||
@@ -1351,6 +1351,54 @@ func testDeleteLabel(t *testing.T, db *Datastore) {
|
||||
// Admin with team filter can delete
|
||||
err = db.DeleteLabel(ctx, team2Label.Name, fleet.TeamFilter{User: adminUser, TeamID: &team2.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a label referenced by a configuration profile — deletion must be blocked
|
||||
l3, err := db.NewLabel(ctx, &fleet.Label{
|
||||
Name: t.Name() + "3",
|
||||
Query: "query3",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
prof, err := db.NewMDMAppleConfigProfile(ctx, *generateAppleCP("test-prof", "com.example.test", 0), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ExecAdhocSQL(t, db, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx,
|
||||
`INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)`,
|
||||
prof.ProfileUUID, l3.Name, l3.ID,
|
||||
)
|
||||
return err
|
||||
})
|
||||
|
||||
err = db.DeleteLabel(ctx, l3.Name, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsForeignKey(err))
|
||||
|
||||
// create a label referenced only by a declaration — deletion must also be blocked
|
||||
l4, err := db.NewLabel(ctx, &fleet.Label{
|
||||
Name: t.Name() + "4",
|
||||
Query: "query4",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
decl, err := db.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
||||
Identifier: "com.example.decl-test",
|
||||
Name: "test-decl",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "com.example.decl-test"}`),
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ExecAdhocSQL(t, db, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx,
|
||||
`INSERT INTO mdm_declaration_labels (apple_declaration_uuid, label_name, label_id) VALUES (?, ?, ?)`,
|
||||
decl.DeclarationUUID, l4.Name, l4.ID,
|
||||
)
|
||||
return err
|
||||
})
|
||||
|
||||
err = db.DeleteLabel(ctx, l4.Name, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsForeignKey(err))
|
||||
}
|
||||
|
||||
func testLabelsSummaryAndListTeamFiltering(t *testing.T, db *Datastore) {
|
||||
|
||||
@@ -2383,11 +2383,15 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, profs, 3)
|
||||
|
||||
// Null label reference first (FK is now RESTRICT), then delete — the profile
|
||||
// will appear broken and we shouldn't see it in the results
|
||||
simulateBrokenLabel(t, ds, ctx, testLabel4.Name)
|
||||
err = ds.DeleteLabel(ctx, testLabel4.Name, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
require.NoError(t, err)
|
||||
// Simulate the label being broken — direct DeleteLabel is now blocked when
|
||||
// referenced by a profile, so we nullify label_id in the join tables instead.
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
if _, err := q.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE label_id = ?`, testLabel4.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := q.ExecContext(ctx, `UPDATE mdm_declaration_labels SET label_id = NULL WHERE label_id = ?`, testLabel4.ID)
|
||||
return err
|
||||
})
|
||||
|
||||
return team.ID, host
|
||||
}
|
||||
@@ -2886,11 +2890,15 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, profs, 3)
|
||||
|
||||
// Null label reference first (FK is now RESTRICT), then delete — the profile
|
||||
// will appear broken and we shouldn't see it in the results
|
||||
simulateBrokenLabel(t, ds, ctx, label.Name)
|
||||
err = ds.DeleteLabel(ctx, label.Name, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
require.NoError(t, err)
|
||||
// Simulate the label being broken — direct DeleteLabel is now blocked when
|
||||
// referenced by a profile, so we nullify label_id in the join tables instead.
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
if _, err := q.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE label_id = ?`, label.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := q.ExecContext(ctx, `UPDATE mdm_declaration_labels SET label_id = NULL WHERE label_id = ?`, label.ID)
|
||||
return err
|
||||
})
|
||||
|
||||
return team.ID, host
|
||||
}
|
||||
|
||||
@@ -2851,7 +2851,7 @@ func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
|
||||
// simulate a broken label by nullifying its id in the join table
|
||||
// (direct DeleteLabel is now blocked when referenced by a profile)
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE label_id = ?`, label.ID)
|
||||
_, err := q.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE windows_profile_uuid = ? AND label_id = ?`, profWithLabel.ProfileUUID, label.ID)
|
||||
return err
|
||||
})
|
||||
|
||||
|
||||
@@ -1380,3 +1380,46 @@ const (
|
||||
// SSOInitiatorAppleMDMSSO is used for automatic MDM Apple enrollment SSO flow.
|
||||
SSOInitiatorAppleMDMSSO = "mdm_sso"
|
||||
)
|
||||
|
||||
// ValidateMDMProfileSpecs validates the label configuration for each profile spec: exactly one
|
||||
// include mode may be set, no label may appear in both include and exclude lists, and the legacy
|
||||
// Labels field is normalised to LabelsIncludeAll. Errors are accumulated into invalid.
|
||||
func ValidateMDMProfileSpecs(invalid *InvalidArgumentError, prefix string, customSettings []MDMProfileSpec) {
|
||||
for i, prof := range customSettings {
|
||||
includeCount := 0
|
||||
for _, b := range []bool{len(prof.Labels) > 0, len(prof.LabelsIncludeAll) > 0, len(prof.LabelsIncludeAny) > 0} {
|
||||
if b {
|
||||
includeCount++
|
||||
}
|
||||
}
|
||||
if includeCount > 1 {
|
||||
invalid.Append(fmt.Sprintf("%s_settings.configuration_profiles", prefix),
|
||||
fmt.Sprintf(`Couldn't edit %s_settings.configuration_profiles. For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`, prefix))
|
||||
}
|
||||
includeLabels := slices.Concat(prof.Labels, prof.LabelsIncludeAll, prof.LabelsIncludeAny)
|
||||
if overlap := ProfileLabelOverlap(includeLabels, prof.LabelsExcludeAny); overlap != "" {
|
||||
invalid.Append(fmt.Sprintf("%s_settings.configuration_profiles", prefix),
|
||||
fmt.Sprintf(`Couldn't edit %s_settings.configuration_profiles. Label %q cannot appear in both include and exclude lists.`, prefix, overlap))
|
||||
}
|
||||
if len(prof.Labels) > 0 {
|
||||
customSettings[i].LabelsIncludeAll = customSettings[i].Labels
|
||||
customSettings[i].Labels = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProfileLabelOverlap returns the first label name that appears in both
|
||||
// the include list and the exclude list, or an empty string if there is none.
|
||||
// include should be the union of labels_include_all and labels_include_any.
|
||||
func ProfileLabelOverlap(include, exclude []string) string {
|
||||
seen := make(map[string]struct{}, len(include))
|
||||
for _, n := range include {
|
||||
seen[n] = struct{}{}
|
||||
}
|
||||
for _, n := range exclude {
|
||||
if _, overlapExists := seen[n]; overlapExists {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -913,9 +913,9 @@ type Service interface {
|
||||
GetHostDEPAssignmentDetails(ctx context.Context, hostID uint) (*HostDEPAssignment, *godep.Device, error)
|
||||
|
||||
// NewMDMAppleConfigProfile creates a new configuration profile for the specified team.
|
||||
NewMDMAppleConfigProfile(ctx context.Context, teamID uint, data []byte, labels []string, labelsMembershipMode MDMLabelsMode) (*MDMAppleConfigProfile, error)
|
||||
NewMDMAppleConfigProfile(ctx context.Context, teamID uint, data []byte, labelsInclude []string, labelsMembershipMode MDMLabelsMode, labelsExcludeAny []string) (*MDMAppleConfigProfile, error)
|
||||
// NewMDMAppleConfigProfileWithPayload creates a new declaration for the specified team.
|
||||
NewMDMAppleDeclaration(ctx context.Context, teamID uint, data []byte, labels []string, name string, labelsMembershipMode MDMLabelsMode) (*MDMAppleDeclaration, error)
|
||||
NewMDMAppleDeclaration(ctx context.Context, teamID uint, data []byte, labelsInclude []string, name string, labelsMembershipMode MDMLabelsMode, labelsExcludeAny []string) (*MDMAppleDeclaration, error)
|
||||
|
||||
// GetMDMAppleConfigProfileByDeprecatedID retrieves the specified Apple
|
||||
// configuration profile via its numeric ID. This method is deprecated and
|
||||
@@ -1212,7 +1212,7 @@ type Service interface {
|
||||
|
||||
// NewMDMWindowsConfigProfile creates a new Windows configuration profile for
|
||||
// the specified team.
|
||||
NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode MDMLabelsMode) (*MDMWindowsConfigProfile, error)
|
||||
NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labelsInclude []string, labelsMembershipMode MDMLabelsMode, labelsExcludeAny []string) (*MDMWindowsConfigProfile, error)
|
||||
|
||||
// NewMDMUnsupportedConfigProfile is called when a profile with an
|
||||
// unsupported extension is uploaded.
|
||||
@@ -1248,7 +1248,7 @@ type Service interface {
|
||||
// Android MDM
|
||||
|
||||
// NewMDMAndroidConfigProfile creates a new Android configuration profile
|
||||
NewMDMAndroidConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode MDMLabelsMode) (*MDMAndroidConfigProfile, error)
|
||||
NewMDMAndroidConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labelsInclude []string, labelsMembershipMode MDMLabelsMode, labelsExcludeAny []string) (*MDMAndroidConfigProfile, error)
|
||||
|
||||
// DeleteMDMAndroidConfigProfile deletes the specified Android profile.
|
||||
DeleteMDMAndroidConfigProfile(ctx context.Context, profileUUID string) error
|
||||
|
||||
@@ -574,9 +574,9 @@ type GetHostDEPAssignmentFunc func(ctx context.Context, host *fleet.Host) (*flee
|
||||
|
||||
type GetHostDEPAssignmentDetailsFunc func(ctx context.Context, hostID uint) (*fleet.HostDEPAssignment, *godep.Device, error)
|
||||
|
||||
type NewMDMAppleConfigProfileFunc func(ctx context.Context, teamID uint, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleConfigProfile, error)
|
||||
type NewMDMAppleConfigProfileFunc func(ctx context.Context, teamID uint, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAppleConfigProfile, error)
|
||||
|
||||
type NewMDMAppleDeclarationFunc func(ctx context.Context, teamID uint, data []byte, labels []string, name string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleDeclaration, error)
|
||||
type NewMDMAppleDeclarationFunc func(ctx context.Context, teamID uint, data []byte, labelsInclude []string, name string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAppleDeclaration, error)
|
||||
|
||||
type GetMDMAppleConfigProfileByDeprecatedIDFunc func(ctx context.Context, profileID uint) (*fleet.MDMAppleConfigProfile, error)
|
||||
|
||||
@@ -744,7 +744,7 @@ type DeleteMDMWindowsConfigProfileFunc func(ctx context.Context, profileUUID str
|
||||
|
||||
type GetMDMWindowsProfilesSummaryFunc func(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error)
|
||||
|
||||
type NewMDMWindowsConfigProfileFunc func(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMWindowsConfigProfile, error)
|
||||
type NewMDMWindowsConfigProfileFunc func(ctx context.Context, teamID uint, profileName string, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMWindowsConfigProfile, error)
|
||||
|
||||
type NewMDMUnsupportedConfigProfileFunc func(ctx context.Context, teamID uint, filename string) error
|
||||
|
||||
@@ -758,7 +758,7 @@ type LinuxHostDiskEncryptionStatusFunc func(ctx context.Context, host fleet.Host
|
||||
|
||||
type GetMDMLinuxProfilesSummaryFunc func(ctx context.Context, teamId *uint) (fleet.MDMProfilesSummary, error)
|
||||
|
||||
type NewMDMAndroidConfigProfileFunc func(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAndroidConfigProfile, error)
|
||||
type NewMDMAndroidConfigProfileFunc func(ctx context.Context, teamID uint, profileName string, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAndroidConfigProfile, error)
|
||||
|
||||
type DeleteMDMAndroidConfigProfileFunc func(ctx context.Context, profileUUID string) error
|
||||
|
||||
@@ -4233,18 +4233,18 @@ func (s *Service) GetHostDEPAssignmentDetails(ctx context.Context, hostID uint)
|
||||
return s.GetHostDEPAssignmentDetailsFunc(ctx, hostID)
|
||||
}
|
||||
|
||||
func (s *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleConfigProfile, error) {
|
||||
func (s *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAppleConfigProfile, error) {
|
||||
s.mu.Lock()
|
||||
s.NewMDMAppleConfigProfileFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.NewMDMAppleConfigProfileFunc(ctx, teamID, data, labels, labelsMembershipMode)
|
||||
return s.NewMDMAppleConfigProfileFunc(ctx, teamID, data, labelsInclude, labelsMembershipMode, labelsExcludeAny)
|
||||
}
|
||||
|
||||
func (s *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, data []byte, labels []string, name string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (s *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, data []byte, labelsInclude []string, name string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAppleDeclaration, error) {
|
||||
s.mu.Lock()
|
||||
s.NewMDMAppleDeclarationFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.NewMDMAppleDeclarationFunc(ctx, teamID, data, labels, name, labelsMembershipMode)
|
||||
return s.NewMDMAppleDeclarationFunc(ctx, teamID, data, labelsInclude, name, labelsMembershipMode, labelsExcludeAny)
|
||||
}
|
||||
|
||||
func (s *Service) GetMDMAppleConfigProfileByDeprecatedID(ctx context.Context, profileID uint) (*fleet.MDMAppleConfigProfile, error) {
|
||||
@@ -4828,11 +4828,11 @@ func (s *Service) GetMDMWindowsProfilesSummary(ctx context.Context, teamID *uint
|
||||
return s.GetMDMWindowsProfilesSummaryFunc(ctx, teamID)
|
||||
}
|
||||
|
||||
func (s *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
func (s *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
s.mu.Lock()
|
||||
s.NewMDMWindowsConfigProfileFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.NewMDMWindowsConfigProfileFunc(ctx, teamID, profileName, data, labels, labelsMembershipMode)
|
||||
return s.NewMDMWindowsConfigProfileFunc(ctx, teamID, profileName, data, labelsInclude, labelsMembershipMode, labelsExcludeAny)
|
||||
}
|
||||
|
||||
func (s *Service) NewMDMUnsupportedConfigProfile(ctx context.Context, teamID uint, filename string) error {
|
||||
@@ -4877,11 +4877,11 @@ func (s *Service) GetMDMLinuxProfilesSummary(ctx context.Context, teamId *uint)
|
||||
return s.GetMDMLinuxProfilesSummaryFunc(ctx, teamId)
|
||||
}
|
||||
|
||||
func (s *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAndroidConfigProfile, error) {
|
||||
func (s *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAndroidConfigProfile, error) {
|
||||
s.mu.Lock()
|
||||
s.NewMDMAndroidConfigProfileFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.NewMDMAndroidConfigProfileFunc(ctx, teamID, profileName, data, labels, labelsMembershipMode)
|
||||
return s.NewMDMAndroidConfigProfileFunc(ctx, teamID, profileName, data, labelsInclude, labelsMembershipMode, labelsExcludeAny)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteMDMAndroidConfigProfile(ctx context.Context, profileUUID string) error {
|
||||
|
||||
@@ -1680,30 +1680,7 @@ func (svc *Service) validateMDM(
|
||||
`Couldn't update setup_experience because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)
|
||||
}
|
||||
}
|
||||
checkCustomSettings := func(prefix string, customSettings []fleet.MDMProfileSpec) {
|
||||
for i, prof := range customSettings {
|
||||
count := 0
|
||||
for _, b := range []bool{
|
||||
len(prof.Labels) > 0,
|
||||
len(prof.LabelsIncludeAll) > 0,
|
||||
len(prof.LabelsIncludeAny) > 0,
|
||||
len(prof.LabelsExcludeAny) > 0,
|
||||
} {
|
||||
if b {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
invalid.Append(fmt.Sprintf("%s_settings.configuration_profiles", prefix),
|
||||
fmt.Sprintf(`Couldn't edit %s_settings.configuration_profiles. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`, prefix))
|
||||
}
|
||||
if len(prof.Labels) > 0 {
|
||||
customSettings[i].LabelsIncludeAll = customSettings[i].Labels
|
||||
customSettings[i].Labels = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
checkCustomSettings("macos", mdm.MacOSSettings.CustomSettings)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "macos", mdm.MacOSSettings.CustomSettings)
|
||||
|
||||
if !mdm.WindowsEnabledAndConfigured {
|
||||
if mdm.WindowsSettings.CustomSettings.Set &&
|
||||
@@ -1713,7 +1690,7 @@ func (svc *Service) validateMDM(
|
||||
`Couldn’t edit windows_settings.configuration_profiles. Windows MDM isn’t turned on. This can be enabled by setting "controls.windows_enabled_and_configured: true" in the default configuration. Visit https://fleetdm.com/guides/windows-mdm-setup and https://fleetdm.com/docs/configuration/yaml-files#controls to learn more about enabling MDM.`)
|
||||
}
|
||||
}
|
||||
checkCustomSettings("windows", mdm.WindowsSettings.CustomSettings.Value)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "windows", mdm.WindowsSettings.CustomSettings.Value)
|
||||
|
||||
// Check oldMdm as we bypass the patching of this value, as it's enabled and disabled elsewhere.
|
||||
if !oldMdm.AndroidEnabledAndConfigured {
|
||||
@@ -1724,7 +1701,7 @@ func (svc *Service) validateMDM(
|
||||
`Couldn’t edit android_settings.configuration_profiles. Android MDM isn’t turned on. This can be enabled by setting "controls.android_enabled_and_configured: true" in the default configuration. Visit https://fleetdm.com/guides/android-mdm-setup and https://fleetdm.com/docs/configuration/yaml-files#controls to learn more about enabling MDM.`)
|
||||
}
|
||||
}
|
||||
checkCustomSettings("android", mdm.AndroidSettings.CustomSettings.Value)
|
||||
fleet.ValidateMDMProfileSpecs(invalid, "android", mdm.AndroidSettings.CustomSettings.Value)
|
||||
|
||||
// MacOSUpdates
|
||||
updatingMacOSVersion := mdm.MacOSUpdates.MinimumVersion.Value != "" &&
|
||||
|
||||
+17
-28
@@ -353,7 +353,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{},
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
// providing an empty set of labels since this endpoint is only maintained for backwards compat
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, data, nil, fleet.LabelsIncludeAll)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, data, nil, fleet.LabelsIncludeAll, nil)
|
||||
if err != nil {
|
||||
return &newMDMAppleConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
@@ -362,7 +362,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleConfigProfile, error) {
|
||||
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAppleConfigProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
@@ -437,20 +437,20 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, d
|
||||
cp.Mobileconfig = data
|
||||
cp.SecretsUpdatedAt = secretsUpdatedAt
|
||||
|
||||
labelMap, err := svc.validateProfileLabels(ctx, &teamID, labels)
|
||||
if overlap := fleet.ProfileLabelOverlap(labelsInclude, labelsExcludeAny); overlap != "" {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("labels", fmt.Sprintf("label %q cannot appear in both include and exclude lists", overlap)))
|
||||
}
|
||||
includeLabels, excludeLabels, err := svc.validateProfileLabelSets(ctx, &teamID, labelsInclude, labelsExcludeAny)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
switch labelsMembershipMode {
|
||||
case fleet.LabelsIncludeAll:
|
||||
cp.LabelsIncludeAll = labelMap
|
||||
cp.LabelsIncludeAll = includeLabels
|
||||
case fleet.LabelsIncludeAny:
|
||||
cp.LabelsIncludeAny = labelMap
|
||||
case fleet.LabelsExcludeAny:
|
||||
cp.LabelsExcludeAny = labelMap
|
||||
default:
|
||||
// TODO what happens if mode is not set?s
|
||||
cp.LabelsIncludeAny = includeLabels
|
||||
}
|
||||
cp.LabelsExcludeAny = excludeLabels
|
||||
|
||||
// Convert profile variable names to FleetVarName type
|
||||
varNames := make([]fleet.FleetVarName, 0, len(profileVars))
|
||||
@@ -857,7 +857,7 @@ func additionalNDESValidation(contents string, ndesVars *NDESVarsFound) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, data []byte, labels []string, name string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, data []byte, labelsInclude []string, name string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAppleDeclaration, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
@@ -890,7 +890,10 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, dat
|
||||
teamName = tm.Name
|
||||
}
|
||||
|
||||
validatedLabels, err := svc.validateDeclarationLabels(ctx, labels, teamID)
|
||||
if overlap := fleet.ProfileLabelOverlap(labelsInclude, labelsExcludeAny); overlap != "" {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("labels", fmt.Sprintf("label %q cannot appear in both include and exclude lists", overlap)))
|
||||
}
|
||||
validatedIncludeLabels, excludeLabels, err := svc.validateDeclarationLabelSets(ctx, teamID, labelsInclude, labelsExcludeAny)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -933,13 +936,12 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, dat
|
||||
|
||||
switch labelsMembershipMode {
|
||||
case fleet.LabelsIncludeAny:
|
||||
d.LabelsIncludeAny = validatedLabels
|
||||
case fleet.LabelsExcludeAny:
|
||||
d.LabelsExcludeAny = validatedLabels
|
||||
d.LabelsIncludeAny = validatedIncludeLabels
|
||||
default:
|
||||
// default to include all
|
||||
d.LabelsIncludeAll = validatedLabels
|
||||
d.LabelsIncludeAll = validatedIncludeLabels
|
||||
}
|
||||
d.LabelsExcludeAny = excludeLabels
|
||||
|
||||
if err := svc.handleDeclarationSoftwareUpdate(ctx, rawDecl, teamID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "handling declaration software update")
|
||||
@@ -1246,19 +1248,6 @@ func (svc *Service) batchValidateDeclarationLabels(ctx context.Context, labelNam
|
||||
return profLabels, nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateDeclarationLabels(ctx context.Context, labelNames []string, teamID uint) ([]fleet.ConfigurationProfileLabel, error) {
|
||||
labelMap, err := svc.batchValidateDeclarationLabels(ctx, labelNames, teamID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating declaration labels")
|
||||
}
|
||||
|
||||
var declLabels []fleet.ConfigurationProfileLabel
|
||||
for _, label := range labelMap {
|
||||
declLabels = append(declLabels, label)
|
||||
}
|
||||
return declLabels, nil
|
||||
}
|
||||
|
||||
type listMDMAppleConfigProfilesRequest struct {
|
||||
TeamID uint `query:"team_id,optional" renameto:"fleet_id"`
|
||||
}
|
||||
|
||||
@@ -741,11 +741,11 @@ func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// test authz create new profile (no team)
|
||||
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll, nil)
|
||||
checkShouldFail(err, tt.shouldFailGlobal)
|
||||
|
||||
// test authz create new profile (team 1)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, mcBytes, nil, fleet.LabelsIncludeAll)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, mcBytes, nil, fleet.LabelsIncludeAll, nil)
|
||||
checkShouldFail(err, tt.shouldFailTeam)
|
||||
|
||||
// test authz list profiles (no team)
|
||||
@@ -808,7 +808,7 @@ func TestNewMDMAppleConfigProfile(t *testing.T) {
|
||||
return &fleet.GroupedCertificateAuthorities{}, nil
|
||||
}
|
||||
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Foo", cp.Name)
|
||||
assert.Equal(t, identifier, cp.Identifier)
|
||||
@@ -816,12 +816,12 @@ func TestNewMDMAppleConfigProfile(t *testing.T) {
|
||||
|
||||
// Unsupported Fleet variable
|
||||
mcBytes = mcBytesForTest("Foo", identifier, "UUID${FLEET_VAR_BOZO}")
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll, nil)
|
||||
assert.ErrorContains(t, err, "Fleet variable")
|
||||
|
||||
// Test profile with FLEET_SECRET in PayloadDisplayName
|
||||
mcBytes = mcBytesForTest("Profile $FLEET_SECRET_PASSWORD", "test.identifier", "UUID")
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll)
|
||||
_, err = svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll, nil)
|
||||
assert.ErrorContains(t, err, "PayloadDisplayName cannot contain FLEET_SECRET variables")
|
||||
}
|
||||
|
||||
@@ -870,7 +870,7 @@ func TestNewMDMAppleDeclarationFreeLicenseTeam(t *testing.T) {
|
||||
|
||||
b := declBytesForTest("D1", "d1content")
|
||||
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 1, b, nil, "name", fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 1, b, nil, "name", fleet.LabelsIncludeAll, nil)
|
||||
assert.ErrorIs(t, err, fleet.ErrMissingLicense)
|
||||
}
|
||||
|
||||
@@ -880,12 +880,12 @@ func TestNewMDMAppleDeclaration(t *testing.T) {
|
||||
|
||||
// Unsupported Fleet variable
|
||||
b := declBytesForTest("D1", "d1content $FLEET_VAR_BOZO")
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll, nil)
|
||||
assert.ErrorContains(t, err, "Fleet variable")
|
||||
|
||||
// decl type missing actual type
|
||||
b = declarationForTestWithType("D1", "com.apple.configuration")
|
||||
_, err = svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll)
|
||||
_, err = svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll, nil)
|
||||
assert.ErrorContains(t, err, "Only configuration declarations (com.apple.configuration.) are supported")
|
||||
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
@@ -898,7 +898,7 @@ func TestNewMDMAppleDeclaration(t *testing.T) {
|
||||
|
||||
// Good declaration
|
||||
b = declBytesForTest("D1", "d1content")
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, d)
|
||||
}
|
||||
@@ -962,7 +962,7 @@ func TestNewMDMAppleDeclarationSkipValidation(t *testing.T) {
|
||||
"Type": "com.apple.configuration.management.status-subscriptions",
|
||||
"Identifier": "test-status-sub"
|
||||
}`)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-status-sub", fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-status-sub", fleet.LabelsIncludeAll, nil)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "status subscription type")
|
||||
})
|
||||
@@ -987,7 +987,7 @@ func TestNewMDMAppleDeclarationSkipValidation(t *testing.T) {
|
||||
"Type": "com.apple.configuration.management.status-subscriptions",
|
||||
"Identifier": "test-status-sub"
|
||||
}`)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-status-sub", fleet.LabelsIncludeAll)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-status-sub", fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, d)
|
||||
})
|
||||
@@ -1005,7 +1005,7 @@ func TestNewMDMAppleDeclarationSkipValidation(t *testing.T) {
|
||||
"Type": "com.example.invalid",
|
||||
"Identifier": "test-invalid"
|
||||
}`)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-invalid", fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-invalid", fleet.LabelsIncludeAll, nil)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "Only configuration declarations")
|
||||
})
|
||||
@@ -1030,7 +1030,7 @@ func TestNewMDMAppleDeclarationSkipValidation(t *testing.T) {
|
||||
"Type": "com.example.invalid",
|
||||
"Identifier": "test-invalid"
|
||||
}`)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-invalid", fleet.LabelsIncludeAll)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-invalid", fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, d)
|
||||
})
|
||||
@@ -1108,7 +1108,7 @@ func TestNewMDMAppleDeclarationSoftwareUpdate(t *testing.T) {
|
||||
t.Run("non software-update declaration skips OS update checks", func(t *testing.T) {
|
||||
svc, ctx, ds := setup(t, true)
|
||||
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, []byte(otherDecl), nil, "test-passcode", fleet.LabelsIncludeAll)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, []byte(otherDecl), nil, "test-passcode", fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, d)
|
||||
assert.False(t, ds.TeamMDMConfigFuncInvoked)
|
||||
@@ -1117,7 +1117,7 @@ func TestNewMDMAppleDeclarationSoftwareUpdate(t *testing.T) {
|
||||
t.Run("software-update declaration requires premium license", func(t *testing.T) {
|
||||
svc, ctx, ds := setup(t, false)
|
||||
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll, nil)
|
||||
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
||||
// The gate fails before the declaration is inserted.
|
||||
assert.False(t, ds.NewMDMAppleDeclarationFuncInvoked)
|
||||
@@ -1128,7 +1128,7 @@ func TestNewMDMAppleDeclarationSoftwareUpdate(t *testing.T) {
|
||||
svc, ctx, ds := setup(t, true)
|
||||
ds.AppConfigFunc = appConfigWith(nil)
|
||||
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 0, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, d)
|
||||
assert.False(t, ds.TeamMDMConfigFuncInvoked)
|
||||
@@ -1142,7 +1142,7 @@ func TestNewMDMAppleDeclarationSoftwareUpdate(t *testing.T) {
|
||||
return &fleet.TeamMDM{}, nil
|
||||
}
|
||||
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 5, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll)
|
||||
d, err := svc.NewMDMAppleDeclaration(ctx, 5, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, d)
|
||||
assert.True(t, ds.TeamMDMConfigFuncInvoked)
|
||||
@@ -1160,7 +1160,7 @@ func TestNewMDMAppleDeclarationSoftwareUpdate(t *testing.T) {
|
||||
svc, ctx, ds := setup(t, true)
|
||||
ds.AppConfigFunc = appConfigWith(apply)
|
||||
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 0, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "OS updates are already configured")
|
||||
// The gate fails before the declaration is inserted.
|
||||
@@ -1184,7 +1184,7 @@ func TestNewMDMAppleDeclarationSoftwareUpdate(t *testing.T) {
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 5, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAppleDeclaration(ctx, 5, []byte(osUpdateDecl), nil, "test-os-update", fleet.LabelsIncludeAll, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, fleet.OSUpdatesAlreadyConfiguredErrorMessage)
|
||||
assert.False(t, ds.NewMDMAppleDeclarationFuncInvoked)
|
||||
|
||||
@@ -2187,42 +2187,30 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMCustomSettings() {
|
||||
assert.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
|
||||
assert.Equal(t, optjson.Slice[fleet.MDMProfileSpec]{Set: true, Value: []fleet.MDMProfileSpec{}}, acResp.MDM.WindowsSettings.CustomSettings)
|
||||
|
||||
// mix of labels fields returns an error
|
||||
// combining two include modes returns an error; combining include + exclude is valid
|
||||
res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"macos_settings": {
|
||||
"custom_settings": [
|
||||
{"path": "foo", "labels": ["a"], "labels_exclude_any": ["b"]}
|
||||
{"path": "foo", "labels_include_all": ["a"], "labels_include_any": ["b"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_settings": {
|
||||
"custom_settings": [
|
||||
{"path": "foo", "labels_include_all": ["a"], "labels_exclude_any": ["b"]}
|
||||
{"path": "foo", "labels": ["a"], "labels_include_all": ["b"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_settings": {
|
||||
"custom_settings": [
|
||||
{"path": "foo", "labels_include_any": ["a"], "labels_exclude_any": ["b"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
@@ -2234,7 +2222,44 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMCustomSettings() {
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_settings": {
|
||||
"custom_settings": [
|
||||
{"path": "foo", "labels_include_all": ["a"], "labels_include_any": ["b"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
// same label in both include and exclude lists returns an error
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"macos_settings": {
|
||||
"custom_settings": [
|
||||
{"path": "foo", "labels_include_all": ["a"], "labels_exclude_any": ["a"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `Label "a" cannot appear in both include and exclude lists.`)
|
||||
|
||||
res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"windows_settings": {
|
||||
"custom_settings": [
|
||||
{"path": "foo", "labels_include_any": ["b"], "labels_exclude_any": ["b"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `Label "b" cannot appear in both include and exclude lists.`)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() {
|
||||
@@ -2353,7 +2378,7 @@ func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() {
|
||||
}}}
|
||||
res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
assert.Contains(t, errMsg, `For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestBatchSetMDMAppleProfiles() {
|
||||
@@ -3461,15 +3486,21 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
|
||||
assertWindowsProfile("win-profile-with-labels.xml", "./Test", 0, []string{"does-not-exist", "bar"}, http.StatusBadRequest, `Couldn't update. Label "does-not-exist" doesn't exist. Please remove the label from the configuration profile.`)
|
||||
assertAndroidProfile("android-profile-with-labels.json", 0, []string{"does-not-exist", "bar"}, http.StatusBadRequest, `Couldn't update. Label "does-not-exist" doesn't exist. Please remove the label from the configuration profile.`)
|
||||
|
||||
// profiles with invalid mix of labels
|
||||
assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "-bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"-foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAndroidProfile("android-invalid-profile-with-labels.json", 0, []string{"-foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAndroidProfile("android-invalid-profile-with-labels.json", 0, []string{"-foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
// profiles with invalid mix of labels (two include modes; exclude may combine with either include mode)
|
||||
assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"!foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAndroidProfile("android-invalid-profile-with-labels.json", 0, []string{"foo", "~bar"}, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
assertAndroidProfile("android-invalid-profile-with-labels.json", 0, []string{"foo", "!bar"}, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`)
|
||||
|
||||
// profiles with same label in both include and exclude lists
|
||||
assertAppleProfile("apple-invalid-profile-with-labels.mobileconfig", "apple-invalid-profile-with-labels", "ident-with-labels", 0, []string{"foo", "-foo"}, http.StatusBadRequest, `Label "foo" cannot appear in both include and exclude lists.`)
|
||||
assertAppleDeclaration("apple-invalid-decl-with-labels.json", "ident-decl-with-labels", 0, []string{"~foo", "-foo"}, http.StatusBadRequest, `Label "foo" cannot appear in both include and exclude lists.`)
|
||||
assertWindowsProfile("win-invalid-profile-with-labels.xml", "./Test", 0, []string{"bar", "-bar"}, http.StatusBadRequest, `Label "bar" cannot appear in both include and exclude lists.`)
|
||||
assertAndroidProfile("android-invalid-profile-with-labels.json", 0, []string{"~bar", "-bar"}, http.StatusBadRequest, `Label "bar" cannot appear in both include and exclude lists.`)
|
||||
|
||||
// profiles with valid labels
|
||||
uuidAppleWithLabel := assertAppleProfile("apple-profile-with-labels.mobileconfig", "apple-profile-with-labels", "ident-with-labels", 0, []string{"!foo"}, http.StatusOK, "")
|
||||
@@ -4848,7 +4879,7 @@ func (s *integrationMDMTestSuite) TestApplyTeamsMDMWindowsProfiles() {
|
||||
}
|
||||
`), http.StatusUnprocessableEntity)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
assert.Contains(t, errMsg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
assert.Contains(t, errMsg, `For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
|
||||
@@ -5066,12 +5097,19 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
|
||||
msg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `Couldn't update. Label "no-such-label" doesn't exist. Please remove the label from the configuration profile.`)
|
||||
|
||||
// mix of labels fields
|
||||
// two include modes in the same profile is invalid (exclude may combine with either include mode)
|
||||
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N1", Contents: mobileconfigForTest("N1", "I1"), Labels: []string{lbl1.Name}, LabelsExcludeAny: []string{lbl2.Name}},
|
||||
{Name: "N1", Contents: mobileconfigForTest("N1", "I1"), LabelsIncludeAll: []string{lbl1.Name}, LabelsIncludeAny: []string{lbl2.Name}},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
// same label in both include and exclude is invalid
|
||||
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N1", Contents: mobileconfigForTest("N1", "I1"), LabelsIncludeAll: []string{lbl1.Name}, LabelsExcludeAny: []string{lbl1.Name}},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, fmt.Sprintf(`Label %q cannot appear in both include and exclude lists.`, lbl1.Name))
|
||||
|
||||
// successful batch-set
|
||||
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
@@ -5367,12 +5405,12 @@ func (s *integrationMDMTestSuite) TestBatchModifyMDMProfiles() {
|
||||
msg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `Couldn't update. Label "no-such-label" doesn't exist. Please remove the label from the configuration profile.`)
|
||||
|
||||
// mix of labels fields
|
||||
// two include modes in the same profile is invalid (exclude may combine with either include mode)
|
||||
res = s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
|
||||
{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1"), LabelsIncludeAll: []string{lbl1.Name}, LabelsExcludeAny: []string{lbl2.Name}},
|
||||
{DisplayName: "N1", Profile: mobileconfigForTest("N1", "I1"), LabelsIncludeAll: []string{lbl1.Name}, LabelsIncludeAny: []string{lbl2.Name}},
|
||||
}}, http.StatusUnprocessableEntity)
|
||||
msg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
require.Contains(t, msg, `For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
|
||||
// successful batch-set
|
||||
s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
|
||||
|
||||
+66
-34
@@ -1679,27 +1679,32 @@ func (newMDMConfigProfileRequest) DecodeRequest(ctx context.Context, r *http.Req
|
||||
}
|
||||
|
||||
// add labels
|
||||
var existsInclAll, existsInclAny, existsExclAny, existsDepr bool
|
||||
var existsInclAll, existsInclAny, existsDepr bool
|
||||
var deprecatedLabels []string
|
||||
decoded.LabelsIncludeAll, existsInclAll = r.MultipartForm.Value[string(fleet.LabelsIncludeAll)]
|
||||
decoded.LabelsIncludeAny, existsInclAny = r.MultipartForm.Value[string(fleet.LabelsIncludeAny)]
|
||||
decoded.LabelsExcludeAny, existsExclAny = r.MultipartForm.Value[string(fleet.LabelsExcludeAny)]
|
||||
decoded.LabelsExcludeAny = r.MultipartForm.Value[string(fleet.LabelsExcludeAny)]
|
||||
deprecatedLabels, existsDepr = r.MultipartForm.Value["labels"]
|
||||
|
||||
// validate that only one of the labels type is provided
|
||||
var count int
|
||||
for _, b := range []bool{existsInclAll, existsInclAny, existsExclAny, existsDepr} {
|
||||
// validate that at most one include mode is provided; labels_exclude_any may be combined with any include mode
|
||||
var includeCount int
|
||||
for _, b := range []bool{existsInclAll, existsInclAny, existsDepr} {
|
||||
if b {
|
||||
count++
|
||||
includeCount++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, &fleet.BadRequestError{Message: `Only one of "labels_exclude_any", "labels_include_all", "labels_include_any", or "labels" can be included.`}
|
||||
if includeCount > 1 {
|
||||
return nil, &fleet.BadRequestError{Message: `Only one of "labels_include_all", "labels_include_any", or "labels" can be included.`}
|
||||
}
|
||||
if existsDepr {
|
||||
decoded.LabelsIncludeAll = deprecatedLabels
|
||||
}
|
||||
|
||||
includeLabels := append(decoded.LabelsIncludeAll, decoded.LabelsIncludeAny...) //nolint:gocritic
|
||||
if overlap := fleet.ProfileLabelOverlap(includeLabels, decoded.LabelsExcludeAny); overlap != "" {
|
||||
return nil, &fleet.BadRequestError{Message: fmt.Sprintf(`Label %q cannot appear in both include and exclude lists.`, overlap)}
|
||||
}
|
||||
|
||||
return &decoded, nil
|
||||
}
|
||||
|
||||
@@ -1729,15 +1734,13 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
||||
isMobileConfig := strings.EqualFold(fileExt, ".mobileconfig")
|
||||
isJSON := strings.EqualFold(fileExt, ".json")
|
||||
|
||||
// determine include mode; labels_exclude_any may be combined with any include mode
|
||||
var labels []string
|
||||
var labelsMode fleet.MDMLabelsMode
|
||||
switch {
|
||||
case len(req.LabelsIncludeAny) > 0:
|
||||
labels = req.LabelsIncludeAny
|
||||
labelsMode = fleet.LabelsIncludeAny
|
||||
case len(req.LabelsExcludeAny) > 0:
|
||||
labels = req.LabelsExcludeAny
|
||||
labelsMode = fleet.LabelsExcludeAny
|
||||
default:
|
||||
// default include all
|
||||
labels = req.LabelsIncludeAll
|
||||
@@ -1765,7 +1768,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
||||
if isMobileConfig || isAppleDeclarationJSON {
|
||||
// Then it's an Apple configuration file
|
||||
if isJSON {
|
||||
decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, data, labels, profileName, labelsMode)
|
||||
decl, err := svc.NewMDMAppleDeclaration(ctx, req.TeamID, data, labels, profileName, labelsMode, req.LabelsExcludeAny)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "MDMAppleDeclaration.Name") && strings.Contains(errStr, "already exists") {
|
||||
@@ -1782,7 +1785,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
||||
|
||||
}
|
||||
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, data, labels, labelsMode)
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, req.TeamID, data, labels, labelsMode, req.LabelsExcludeAny)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
@@ -1792,7 +1795,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
||||
}
|
||||
|
||||
if isAndroidJSON {
|
||||
cp, err := svc.NewMDMAndroidConfigProfile(ctx, req.TeamID, profileName, data, labels, labelsMode)
|
||||
cp, err := svc.NewMDMAndroidConfigProfile(ctx, req.TeamID, profileName, data, labels, labelsMode, req.LabelsExcludeAny)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
@@ -1802,7 +1805,7 @@ func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc f
|
||||
}
|
||||
|
||||
if isWindows := strings.EqualFold(fileExt, ".xml"); isWindows {
|
||||
cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, data, labels, labelsMode)
|
||||
cp, err := svc.NewMDMWindowsConfigProfile(ctx, req.TeamID, profileName, data, labels, labelsMode, req.LabelsExcludeAny)
|
||||
if err != nil {
|
||||
return &newMDMConfigProfileResponse{Err: err}, nil
|
||||
}
|
||||
@@ -1837,7 +1840,7 @@ func (svc *Service) NewMDMUnsupportedConfigProfile(ctx context.Context, teamID u
|
||||
return &fleet.BadRequestError{Message: "Couldn't add profile. The file should be a .mobileconfig, XML, or JSON file."}
|
||||
}
|
||||
|
||||
func (svc *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMAndroidConfigProfile, error) {
|
||||
func (svc *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMAndroidConfigProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
@@ -1872,19 +1875,21 @@ func (svc *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint,
|
||||
return nil, ctxerr.Wrap(ctx, err, "validate profile")
|
||||
}
|
||||
|
||||
labelMap, err := svc.validateProfileLabels(ctx, &teamID, labels)
|
||||
if overlap := fleet.ProfileLabelOverlap(labelsInclude, labelsExcludeAny); overlap != "" {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("labels", fmt.Sprintf("label %q cannot appear in both include and exclude lists", overlap)))
|
||||
}
|
||||
includeLabels, excludeLabels, err := svc.validateProfileLabelSets(ctx, &teamID, labelsInclude, labelsExcludeAny)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
switch labelsMembershipMode {
|
||||
case fleet.LabelsIncludeAny:
|
||||
cp.LabelsIncludeAny = labelMap
|
||||
case fleet.LabelsExcludeAny:
|
||||
cp.LabelsExcludeAny = labelMap
|
||||
cp.LabelsIncludeAny = includeLabels
|
||||
default:
|
||||
// default include all
|
||||
cp.LabelsIncludeAll = labelMap
|
||||
cp.LabelsIncludeAll = includeLabels
|
||||
}
|
||||
cp.LabelsExcludeAny = excludeLabels
|
||||
|
||||
newCP, err := svc.ds.NewMDMAndroidConfigProfile(ctx, cp)
|
||||
if err != nil {
|
||||
@@ -1957,17 +1962,40 @@ func (svc *Service) batchValidateProfileLabels(ctx context.Context, teamID *uint
|
||||
return profLabels, nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateProfileLabels(ctx context.Context, teamID *uint, labelNames []string) ([]fleet.ConfigurationProfileLabel, error) {
|
||||
labelMap, err := svc.batchValidateProfileLabels(ctx, teamID, labelNames)
|
||||
// validateDeclarationLabelSets validates both include and exclude label sets in a single DB round-trip
|
||||
// and returns the resolved slices ready to assign to the declaration struct.
|
||||
func (svc *Service) validateDeclarationLabelSets(ctx context.Context, teamID uint, labelsInclude, labelsExcludeAny []string) (include, exclude []fleet.ConfigurationProfileLabel, err error) {
|
||||
allLabelMap, err := svc.batchValidateDeclarationLabels(ctx, slices.Concat(labelsInclude, labelsExcludeAny), teamID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating profile labels")
|
||||
return nil, nil, err
|
||||
}
|
||||
include = make([]fleet.ConfigurationProfileLabel, 0, len(labelsInclude))
|
||||
for _, name := range labelsInclude {
|
||||
include = append(include, allLabelMap[name])
|
||||
}
|
||||
exclude = make([]fleet.ConfigurationProfileLabel, 0, len(labelsExcludeAny))
|
||||
for _, name := range labelsExcludeAny {
|
||||
exclude = append(exclude, allLabelMap[name])
|
||||
}
|
||||
return include, exclude, nil
|
||||
}
|
||||
|
||||
var profLabels []fleet.ConfigurationProfileLabel
|
||||
for _, label := range labelMap {
|
||||
profLabels = append(profLabels, label)
|
||||
// validateProfileLabelSets validates both include and exclude label sets in a single DB round-trip
|
||||
// and returns the resolved slices ready to assign to the profile struct.
|
||||
func (svc *Service) validateProfileLabelSets(ctx context.Context, teamID *uint, labelsInclude, labelsExcludeAny []string) (include, exclude []fleet.ConfigurationProfileLabel, err error) {
|
||||
allLabelMap, err := svc.batchValidateProfileLabels(ctx, teamID, slices.Concat(labelsInclude, labelsExcludeAny))
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
return profLabels, nil
|
||||
include = make([]fleet.ConfigurationProfileLabel, 0, len(labelsInclude))
|
||||
for _, name := range labelsInclude {
|
||||
include = append(include, allLabelMap[name])
|
||||
}
|
||||
exclude = make([]fleet.ConfigurationProfileLabel, 0, len(labelsExcludeAny))
|
||||
for _, name := range labelsExcludeAny {
|
||||
exclude = append(exclude, allLabelMap[name])
|
||||
}
|
||||
return include, exclude, nil
|
||||
}
|
||||
|
||||
type batchModifyMDMConfigProfilesRequest struct {
|
||||
@@ -2806,20 +2834,24 @@ func getAndroidProfiles(ctx context.Context,
|
||||
|
||||
func validateProfiles(profiles map[int]fleet.MDMProfileBatchPayload) error {
|
||||
for _, profile := range profiles {
|
||||
// validate that only one of labels, labels_include_all and labels_exclude_any is provided.
|
||||
var count int
|
||||
// validate that at most one include mode is provided; labels_exclude_any may be combined with any include mode
|
||||
var includeCount int
|
||||
for _, b := range []bool{
|
||||
len(profile.LabelsIncludeAll) > 0,
|
||||
len(profile.LabelsIncludeAny) > 0,
|
||||
len(profile.LabelsExcludeAny) > 0,
|
||||
len(profile.Labels) > 0,
|
||||
} {
|
||||
if b {
|
||||
count++
|
||||
includeCount++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
return fleet.NewInvalidArgumentError("mdm", `Couldn't edit configuration_profiles. For each profile, only one of "labels_exclude_any", "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
if includeCount > 1 {
|
||||
return fleet.NewInvalidArgumentError("mdm", `Couldn't edit configuration_profiles. For each profile, only one of "labels_include_all", "labels_include_any" or "labels" can be included.`)
|
||||
}
|
||||
|
||||
includeLabels := append(profile.LabelsIncludeAll, append(profile.Labels, profile.LabelsIncludeAny...)...) //nolint:gocritic
|
||||
if overlap := fleet.ProfileLabelOverlap(includeLabels, profile.LabelsExcludeAny); overlap != "" {
|
||||
return fleet.NewInvalidArgumentError("mdm", fmt.Sprintf(`Couldn't edit configuration_profiles. Label %q cannot appear in both include and exclude lists.`, overlap))
|
||||
}
|
||||
|
||||
if len(profile.Contents) > 1024*1024 {
|
||||
|
||||
@@ -1386,11 +1386,11 @@ func TestMDMWindowsConfigProfileAuthz(t *testing.T) {
|
||||
checkShouldFail(t, err, tt.shouldFailTeamRead)
|
||||
|
||||
// test authz create new profile (no team)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", []byte(winProfContent), nil, fleet.LabelsIncludeAll)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", []byte(winProfContent), nil, fleet.LabelsIncludeAll, nil)
|
||||
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
|
||||
|
||||
// test authz create new profile (team 1)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", []byte(winProfContent), nil, fleet.LabelsIncludeAll)
|
||||
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", []byte(winProfContent), nil, fleet.LabelsIncludeAll, nil)
|
||||
checkShouldFail(t, err, tt.shouldFailTeamWrite)
|
||||
|
||||
// test authz delete config profile (no team)
|
||||
@@ -1486,7 +1486,7 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
ctx = test.UserContext(ctx, test.UserAdmin)
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", []byte(c.profile), nil, fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", []byte(c.profile), nil, fleet.LabelsIncludeAll, nil)
|
||||
if c.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
@@ -3113,7 +3113,7 @@ func TestNewMDMProfilePremiumOnlyAndroid(t *testing.T) {
|
||||
}
|
||||
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: tier})
|
||||
|
||||
_, err := svc.NewMDMAndroidConfigProfile(ctx, tt.teamID, tt.name, []byte(tt.profile), nil, fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMAndroidConfigProfile(ctx, tt.teamID, tt.name, []byte(tt.profile), nil, fleet.LabelsIncludeAll, nil)
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.NewMDMAndroidConfigProfileFuncInvoked)
|
||||
|
||||
@@ -180,6 +180,16 @@ func (ts *withServer) commonTearDownTest(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
// Null label_id references in MDM profile/declaration label tables before deleting labels,
|
||||
// since the application layer now blocks label deletion while referenced by a profile.
|
||||
mysqltest.ExecAdhocSQL(t, ts.ds, func(q sqlx.ExtContext) error {
|
||||
if _, err := q.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL`); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := q.ExecContext(ctx, `UPDATE mdm_declaration_labels SET label_id = NULL`)
|
||||
return err
|
||||
})
|
||||
|
||||
lbls, err := ts.ds.ListLabels(ctx, filter, fleet.ListOptions{}, false)
|
||||
require.NoError(t, err)
|
||||
for _, lbl := range lbls {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/variables"
|
||||
)
|
||||
|
||||
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labelsInclude []string, labelsMembershipMode fleet.MDMLabelsMode, labelsExcludeAny []string) (*fleet.MDMWindowsConfigProfile, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
@@ -66,19 +66,21 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
|
||||
return nil, ctxerr.Wrap(ctx, err, "validate profile")
|
||||
}
|
||||
|
||||
labelMap, err := svc.validateProfileLabels(ctx, &teamID, labels)
|
||||
if overlap := fleet.ProfileLabelOverlap(labelsInclude, labelsExcludeAny); overlap != "" {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("labels", fmt.Sprintf("label %q cannot appear in both include and exclude lists", overlap)))
|
||||
}
|
||||
includeLabels, excludeLabels, err := svc.validateProfileLabelSets(ctx, &teamID, labelsInclude, labelsExcludeAny)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
||||
}
|
||||
switch labelsMembershipMode {
|
||||
case fleet.LabelsIncludeAny:
|
||||
cp.LabelsIncludeAny = labelMap
|
||||
case fleet.LabelsExcludeAny:
|
||||
cp.LabelsExcludeAny = labelMap
|
||||
cp.LabelsIncludeAny = includeLabels
|
||||
default:
|
||||
// default include all
|
||||
cp.LabelsIncludeAll = labelMap
|
||||
cp.LabelsIncludeAll = includeLabels
|
||||
}
|
||||
cp.LabelsExcludeAny = excludeLabels
|
||||
|
||||
if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{string(cp.SyncML)}); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", err.Error()))
|
||||
|
||||
@@ -378,7 +378,7 @@ func TestNewMDMWindowsConfigProfileSoftwareUpdate(t *testing.T) {
|
||||
svc, ctx, ds := setup(t, true)
|
||||
ds.AppConfigFunc = appConfigWith(nil)
|
||||
|
||||
p, err := svc.NewMDMWindowsConfigProfile(ctx, 0, "other", otherSyncML, nil, fleet.LabelsIncludeAll)
|
||||
p, err := svc.NewMDMWindowsConfigProfile(ctx, 0, "other", otherSyncML, nil, fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, p)
|
||||
assert.False(t, ds.TeamMDMConfigFuncInvoked)
|
||||
@@ -388,7 +388,7 @@ func TestNewMDMWindowsConfigProfileSoftwareUpdate(t *testing.T) {
|
||||
svc, ctx, ds := setup(t, false)
|
||||
ds.AppConfigFunc = appConfigWith(nil)
|
||||
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, 0, "other", osUpdateSyncML, nil, fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, 0, "other", osUpdateSyncML, nil, fleet.LabelsIncludeAll, nil)
|
||||
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
||||
// The gate fails before the profile is inserted.
|
||||
assert.False(t, ds.NewMDMWindowsConfigProfileFuncInvoked)
|
||||
@@ -399,7 +399,7 @@ func TestNewMDMWindowsConfigProfileSoftwareUpdate(t *testing.T) {
|
||||
svc, ctx, ds := setup(t, true)
|
||||
ds.AppConfigFunc = appConfigWith(nil)
|
||||
|
||||
p, err := svc.NewMDMWindowsConfigProfile(ctx, 0, "os-update", osUpdateSyncML, nil, fleet.LabelsIncludeAll)
|
||||
p, err := svc.NewMDMWindowsConfigProfile(ctx, 0, "os-update", osUpdateSyncML, nil, fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, p)
|
||||
assert.False(t, ds.TeamMDMConfigFuncInvoked)
|
||||
@@ -414,7 +414,7 @@ func TestNewMDMWindowsConfigProfileSoftwareUpdate(t *testing.T) {
|
||||
return &fleet.TeamMDM{}, nil
|
||||
}
|
||||
|
||||
p, err := svc.NewMDMWindowsConfigProfile(ctx, 5, "os-update", osUpdateSyncML, nil, fleet.LabelsIncludeAll)
|
||||
p, err := svc.NewMDMWindowsConfigProfile(ctx, 5, "os-update", osUpdateSyncML, nil, fleet.LabelsIncludeAll, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, p)
|
||||
assert.True(t, ds.TeamMDMConfigFuncInvoked)
|
||||
@@ -427,7 +427,7 @@ func TestNewMDMWindowsConfigProfileSoftwareUpdate(t *testing.T) {
|
||||
ac.MDM.WindowsUpdates = configuredSettings()
|
||||
})
|
||||
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, 0, "os-update", osUpdateSyncML, nil, fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, 0, "os-update", osUpdateSyncML, nil, fleet.LabelsIncludeAll, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, fleet.OSUpdatesAlreadyConfiguredErrorMessage)
|
||||
// The gate fails before the profile is inserted.
|
||||
@@ -441,7 +441,7 @@ func TestNewMDMWindowsConfigProfileSoftwareUpdate(t *testing.T) {
|
||||
return &fleet.TeamMDM{WindowsUpdates: configuredSettings()}, nil
|
||||
}
|
||||
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, 5, "os-update", osUpdateSyncML, nil, fleet.LabelsIncludeAll)
|
||||
_, err := svc.NewMDMWindowsConfigProfile(ctx, 5, "os-update", osUpdateSyncML, nil, fleet.LabelsIncludeAll, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, fleet.OSUpdatesAlreadyConfiguredErrorMessage)
|
||||
assert.False(t, ds.NewMDMWindowsConfigProfileFuncInvoked)
|
||||
|
||||
Reference in New Issue
Block a user