Improved GitOps consistency for Windows BatchSetMDMProfiles (#48467)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves
https://github.com/fleetdm/confidential/issues/16293

Test failures are not related to this change. They are currently failing
on main.

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

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **Bug Fixes**
* Improved consistency when applying Windows configuration profiles in
batch by validating against the latest server MDM state.
* Fixed an issue where a temporary “assume enabled” setting could affect
real configuration updates; it now applies only to dry runs.
* Ensured team profile validation uses the freshly persisted server
state during the same GitOps execution.
* Added a regression test covering Windows MDM “assume enabled” behavior
for dry-run vs real runs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-06-30 21:33:07 +01:00
committed by GitHub
parent 5d531c10d9
commit a90eab6f62
4 changed files with 102 additions and 3 deletions
@@ -0,0 +1 @@
- Improved GitOps consistency by validating batch-applied Windows configuration profiles against the server's current MDM configuration state, while continuing to support previewing (dry run) a config that enables Windows MDM and applies profiles in a single run.
+5 -1
View File
@@ -1001,6 +1001,10 @@ func (c *Client) ApplyGroup(
}
if len(tmFileContents) > 0 {
// A prior step in this GitOps run may have updated AppConfig (e.g. enabled Windows MDM), so bypass the cached AppConfig on the
// server. This lets profile validation read the freshly persisted state.
teamProfilesOpts := teamOpts
teamProfilesOpts.NoCache = true
for tmName, profs := range tmFileContents {
// For non-dry run, currentTeamName and tmName are the same
currentTeamName := getTeamName(tmName)
@@ -1013,7 +1017,7 @@ func (c *Client) ApplyGroup(
} else {
logfn("[+] applying MDM profiles for fleet %s\n", tmName)
}
if err := c.ApplyTeamProfiles(currentTeamName, profs, teamOpts); err != nil {
if err := c.ApplyTeamProfiles(currentTeamName, profs, teamProfilesOpts); err != nil {
return nil, nil, nil, nil, fmt.Errorf("applying custom settings for fleet %q: %w", tmName, err)
}
}
+3 -2
View File
@@ -2093,7 +2093,7 @@ type batchSetMDMProfilesRequest struct {
TeamID *uint `json:"-" query:"team_id,optional" renameto:"fleet_id"`
TeamName *string `json:"-" query:"team_name,optional" renameto:"fleet_name"`
DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
AssumeEnabled *bool `json:"-" query:"assume_enabled,optional"` // if true, assume MDM is enabled
AssumeEnabled *bool `json:"-" query:"assume_enabled,optional"` // if true, assume Windows MDM is enabled; honored on dry run only
Profiles backwardsCompatProfilesParam `json:"profiles"`
NoCache bool `json:"-" query:"no_cache,optional"`
}
@@ -2173,7 +2173,8 @@ func (svc *Service) BatchSetMDMProfiles(
ctx = ctxdb.BypassCachedMysql(ctx, false)
}
if assumeEnabled != nil {
// assume_enabled is only honored on dry runs
if dryRun && assumeEnabled != nil {
appCfg.MDM.WindowsEnabledAndConfigured = *assumeEnabled
}
+93
View File
@@ -2224,6 +2224,99 @@ func TestMDMBatchSetProfilesAppleConfigProfileScopeValidation(t *testing.T) {
require.ErrorContains(t, err, "conflicting scopes")
}
// TestMDMBatchSetProfilesWindowsAssumeEnabled is a regression test ensuring the assume_enabled flag is only honored on dry runs
func TestMDMBatchSetProfilesWindowsAssumeEnabled(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true})
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin)}})
var windowsEnabled bool
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: windowsEnabled,
},
}, nil
}
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
return &fleet.GroupedCertificateAuthorities{}, nil
}
ds.VerifyAppleConfigProfileScopesDoNotConflictFunc = func(ctx context.Context, cps []*fleet.MDMAppleConfigProfile) error {
return nil
}
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, profVars []fleet.MDMProfileIdentifierFleetVariables,
) (fleet.MDMProfilesUpdates, error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string,
hostUUIDs []string,
) (fleet.MDMProfilesUpdates, error) {
return fleet.MDMProfilesUpdates{}, nil
}
windowsProfiles := []fleet.MDMProfileBatchPayload{
{Name: "win-profile", Contents: []byte(`<Replace></Replace>`)},
}
testCases := []struct {
name string
windowsEnabled bool
assumeEnabled *bool
dryRun bool
wantErr string
wantDSInvoked bool
}{
{
name: "assume_enabled true on real run is a no-op when Windows MDM is disabled",
windowsEnabled: false,
assumeEnabled: new(true),
dryRun: false,
wantErr: fleet.ErrWindowsMDMNotConfigured.Error(),
wantDSInvoked: false,
},
{
// The legitimate GitOps dry-run flow
name: "assume_enabled true on dry run validates when Windows MDM is disabled",
windowsEnabled: false,
assumeEnabled: new(true),
dryRun: true,
wantErr: "",
wantDSInvoked: false, // dry run never persists
},
{
// The legitimate real run
name: "real run succeeds without assume_enabled when Windows MDM is enabled",
windowsEnabled: true,
assumeEnabled: nil,
dryRun: false,
wantErr: "",
wantDSInvoked: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
windowsEnabled = tc.windowsEnabled
ds.BatchSetMDMProfilesFuncInvoked = false
err := svc.BatchSetMDMProfiles(ctx, nil, nil, windowsProfiles, tc.dryRun, false, tc.assumeEnabled, false)
if tc.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.wantDSInvoked, ds.BatchSetMDMProfilesFuncInvoked)
})
}
}
func TestValidateProfiles(t *testing.T) {
tests := []struct {
name string