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:
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user