Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions cmd/fleetctl/fleetctl/apply_deprecated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,12 @@ spec:
GracePeriodDays: optjson.SetInt(0),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
EndUserLocalAccountType: optjson.SetString("admin"),
},
WindowsSettings: fleet.WindowsSettings{
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
},
WindowsEnabledAndConfigured: true,
}, currentAppConfig.MDM)
Expand Down Expand Up @@ -376,7 +381,12 @@ spec:
GracePeriodDays: optjson.SetInt(0),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
EndUserLocalAccountType: optjson.SetString("admin"),
},
WindowsSettings: fleet.WindowsSettings{
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
},
WindowsEnabledAndConfigured: true,
}, currentAppConfig.MDM)
Expand Down
28 changes: 26 additions & 2 deletions cmd/fleetctl/fleetctl/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -823,8 +823,20 @@ spec:
grace_period_days: 1
`)

// the managed local account alias fields are force-synced from the canonical
// macos_setup values on save (see SyncManagedLocalAccountAliases)
aliasSyncedMacOSSettings := fleet.MacOSSettings{
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
EndUserLocalAccountType: optjson.SetString("admin"),
}
aliasSyncedWindowsSettings := fleet.WindowsSettings{
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
}

newMDMSettings := fleet.MDM{
DeprecatedAppleBMDefaultTeam: "team1",
MacOSSettings: aliasSyncedMacOSSettings,
WindowsSettings: aliasSyncedWindowsSettings,
AppleBMTermsExpired: false,
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Expand Down Expand Up @@ -910,6 +922,8 @@ spec:

newMDMSettings = fleet.MDM{
DeprecatedAppleBMDefaultTeam: "team1",
MacOSSettings: aliasSyncedMacOSSettings,
WindowsSettings: aliasSyncedWindowsSettings,
AppleBMTermsExpired: false,
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Expand Down Expand Up @@ -1642,7 +1656,12 @@ spec:
GracePeriodDays: optjson.SetInt(0),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
EndUserLocalAccountType: optjson.SetString("admin"),
},
WindowsSettings: fleet.WindowsSettings{
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
},
WindowsEnabledAndConfigured: true,
}, currentAppConfig.MDM)
Expand Down Expand Up @@ -1688,7 +1707,12 @@ spec:
GracePeriodDays: optjson.SetInt(0),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
EndUserLocalAccountType: optjson.SetString("admin"),
},
WindowsSettings: fleet.WindowsSettings{
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)},
},
WindowsEnabledAndConfigured: true,
}, currentAppConfig.MDM)
Expand Down
39 changes: 32 additions & 7 deletions cmd/fleetctl/fleetctl/generate_gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ func (cmd *GenerateGitopsCommand) Run() error {
IPadOSUpdates: cmd.AppConfig.MDM.IPadOSUpdates,
WindowsUpdates: cmd.AppConfig.MDM.WindowsUpdates,
MacOSSetup: cmd.AppConfig.MDM.MacOSSetup,
WindowsSettings: cmd.AppConfig.MDM.WindowsSettings,
}

// Collect failing policy IDs from webhook settings so we can output
Expand Down Expand Up @@ -1348,6 +1349,7 @@ func (cmd *GenerateGitopsCommand) generateControls(teamId *uint, teamName string
macosSettingsT := reflect.TypeFor[fleet.MacOSSettings]()
windowsSettingsT := reflect.TypeFor[fleet.WindowsSettings]()
androidSettingsT := reflect.TypeFor[fleet.AndroidSettings]()
managedLocalAccountT := reflect.TypeFor[fleet.ManagedLocalAccountSettings]()

if cmd.AppConfig.MDM.EnabledAndConfigured {
macosSettings := map[string]any{}
Expand All @@ -1364,16 +1366,39 @@ func (cmd *GenerateGitopsCommand) generateControls(teamId *uint, teamName string
if len(assets) > 0 {
macosSettings[jsonFieldName(macosSettingsT, "Assets")] = assets
}
// The managed local account fields under apple_settings alias the deprecated
// setup_experience fields; emit them here (only when set to non-default values) so the
// generated YAML uses the new spelling. The account type is emitted independently of
// the enabled flag so a stored non-admin type is never silently dropped.
if cmd.AppConfig.License.IsPremium() && teamMdm != nil {
if teamMdm.MacOSSetup.EnableManagedLocalAccount.Value {
macosSettings[jsonFieldName(macosSettingsT, "ManagedLocalAccountSettings")] = map[string]any{
jsonFieldName(managedLocalAccountT, "Enabled"): true,
}
}
if accountType := teamMdm.MacOSSetup.EndUserLocalAccountType.Value; accountType != "" && accountType != "admin" {
macosSettings[jsonFieldName(macosSettingsT, "EndUserLocalAccountType")] = accountType
}
}
if len(macosSettings) > 0 {
result[jsonFieldName(t, "MacOSSettings")] = macosSettings
}
}
if cmd.AppConfig.MDM.WindowsEnabledAndConfigured && profiles != nil {
if len(profiles["windows_profiles"].([]map[string]interface{})) > 0 {
result[jsonFieldName(t, "WindowsSettings")] = map[string]interface{}{
jsonFieldName(windowsSettingsT, "CustomSettings"): profiles["windows_profiles"],
if cmd.AppConfig.MDM.WindowsEnabledAndConfigured {
windowsSettings := map[string]any{}
if profiles != nil {
if windowsProfiles, _ := profiles["windows_profiles"].([]map[string]any); len(windowsProfiles) > 0 {
windowsSettings[jsonFieldName(windowsSettingsT, "CustomSettings")] = windowsProfiles
}
}
if cmd.AppConfig.License.IsPremium() && teamMdm != nil && teamMdm.WindowsSettings.ManagedLocalAccountSettings.Enabled.Value {
windowsSettings[jsonFieldName(windowsSettingsT, "ManagedLocalAccountSettings")] = map[string]any{
jsonFieldName(managedLocalAccountT, "Enabled"): true,
}
}
if len(windowsSettings) > 0 {
result[jsonFieldName(t, "WindowsSettings")] = windowsSettings
}
}
if cmd.AppConfig.MDM.AndroidEnabledAndConfigured && profiles != nil {
if len(profiles["android_profiles"].([]map[string]interface{})) > 0 {
Expand Down Expand Up @@ -1505,10 +1530,10 @@ func (cmd *GenerateGitopsCommand) generateControls(teamId *uint, teamName string
hasEnrollmentProfile := enrollmentProfile != nil

// If the team has any of these configured, we need to generate the macos_setup section.
// The managed local account fields are not part of this placeholder: they are emitted
// under apple_settings.managed_local_account_settings above.
if hasBootstrapPackage || hasSetupScript || hasEnrollmentProfile ||
(teamMdm != nil && (teamMdm.MacOSSetup.EnableEndUserAuthentication ||
teamMdm.MacOSSetup.EnableManagedLocalAccount.Value ||
(teamMdm.MacOSSetup.EndUserLocalAccountType.Valid && teamMdm.MacOSSetup.EndUserLocalAccountType.Value != "admin"))) {
(teamMdm != nil && teamMdm.MacOSSetup.EnableEndUserAuthentication) {
result[jsonFieldName(mdmT, "MacOSSetup")] = "TODO: update with your setup_experience configuration"
cmd.Messages.Notes = append(cmd.Messages.Notes, Note{
Filename: teamName,
Expand Down
82 changes: 82 additions & 0 deletions cmd/fleetctl/fleetctl/generate_gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2959,3 +2959,85 @@ func TestGeneratePoliciesPatchPolicyOrphanedFromFleetMaintainedApp(t *testing.T)
require.Error(t, err)
require.ErrorContains(t, err, "Team patch policy")
}

// TestGenerateControlsManagedLocalAccount verifies that the managed local account settings are
// emitted under the per-platform settings sections (new apple_settings spelling) instead of the
// shared setup_experience TODO placeholder (#48720).
func TestGenerateControlsManagedLocalAccount(t *testing.T) {
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)

cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]any),
AppConfig: appConfig,
ScriptList: make(map[uint]string),
}

mdmConfig := fleet.TeamMDM{
MacOSSetup: fleet.MacOSSetup{
EnableManagedLocalAccount: optjson.SetBool(true),
EndUserLocalAccountType: optjson.SetString("standard"),
},
WindowsSettings: fleet.WindowsSettings{
ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(true)},
},
}

controlsRaw, err := cmd.generateControls(new(uint), "no_team", &mdmConfig)
require.NoError(t, err)

// no TODO placeholder: the managed local account settings alone must not trigger it
_, ok := controlsRaw["macos_setup"]
require.False(t, ok, "expected no macos_setup TODO placeholder")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the generated placeholder key.

generateControls emits the placeholder as setup_experience, not macos_setup, so this assertion always passes and cannot catch the regression it describes.

Proposed fix
- _, ok := controlsRaw["macos_setup"]
+ _, ok := controlsRaw["setup_experience"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// no TODO placeholder: the managed local account settings alone must not trigger it
_, ok := controlsRaw["macos_setup"]
require.False(t, ok, "expected no macos_setup TODO placeholder")
// no TODO placeholder: the managed local account settings alone must not trigger it
_, ok := controlsRaw["setup_experience"]
require.False(t, ok, "expected no macos_setup TODO placeholder")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/fleetctl/fleetctl/generate_gitops_test.go` around lines 2993 - 2995,
Update the assertion in the generateControls test to check controlsRaw for the
emitted setup_experience placeholder key instead of macos_setup, while
preserving the expectation that the key is absent when only managed local
account settings are configured.


macosSettings, ok := controlsRaw["apple_settings"].(map[string]any)
require.True(t, ok, "expected a macos_settings section")
require.Equal(t, map[string]any{"enabled": true}, macosSettings["managed_local_account_settings"])
require.Equal(t, "standard", macosSettings["end_user_local_account_type"])

windowsSettings, ok := controlsRaw["windows_settings"].(map[string]any)
require.True(t, ok, "expected a windows_settings section")
require.Equal(t, map[string]any{"enabled": true}, windowsSettings["managed_local_account_settings"])

// default account type is omitted, and disabled platforms emit nothing
mdmConfig = fleet.TeamMDM{
MacOSSetup: fleet.MacOSSetup{
EnableManagedLocalAccount: optjson.SetBool(true),
EndUserLocalAccountType: optjson.SetString("admin"),
},
}
controlsRaw, err = cmd.generateControls(new(uint), "no_team", &mdmConfig)
require.NoError(t, err)
macosSettings, ok = controlsRaw["apple_settings"].(map[string]any)
require.True(t, ok, "expected a macos_settings section")
require.Equal(t, map[string]any{"enabled": true}, macosSettings["managed_local_account_settings"])
require.NotContains(t, macosSettings, "end_user_local_account_type")
if windowsSection, ok := controlsRaw["windows_settings"].(map[string]any); ok {
require.NotContains(t, windowsSection, "managed_local_account_settings")
}

// feature fully disabled: no managed local account keys at all
controlsRaw, err = cmd.generateControls(new(uint), "no_team", &fleet.TeamMDM{})
require.NoError(t, err)
if macosSection, ok := controlsRaw["apple_settings"].(map[string]any); ok {
require.NotContains(t, macosSection, "managed_local_account_settings")
}

// a stored non-admin account type is emitted even when the feature is disabled, so it is
// never silently dropped from generated output
mdmConfig = fleet.TeamMDM{
MacOSSetup: fleet.MacOSSetup{
EndUserLocalAccountType: optjson.SetString("standard"),
},
}
controlsRaw, err = cmd.generateControls(new(uint), "no_team", &mdmConfig)
require.NoError(t, err)
macosSettings, ok = controlsRaw["apple_settings"].(map[string]any)
require.True(t, ok, "expected an apple_settings section")
require.Equal(t, "standard", macosSettings["end_user_local_account_type"])
require.NotContains(t, macosSettings, "managed_local_account_settings")
}
75 changes: 75 additions & 0 deletions cmd/fleetctl/fleetctl/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8792,3 +8792,78 @@ func TestGetLabelUsagePolicyScopes(t *testing.T) {
})
}
}

// TestGitOpsManagedLocalAccount covers the new managed_local_account_settings keys end to end
// through the real gitops client and server (#48720): the apple_settings surface converges onto
// the deprecated setup_experience storage, the Windows toggle persists, repeated applies are
// stable, and removing the keys from the YAML declaratively disables the feature.
func TestGitOpsManagedLocalAccount(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables.
ds, savedAppConfigPtr, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

const fleetServerURL = "https://fleet.example.com"
t.Setenv("FLEET_SERVER_URL", fleetServerURL)

writeConfig := func(controls string) string {
f, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = f.WriteString(fmt.Sprintf(`
controls:
%s
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: %s
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: GitOps Managed Local Account Test
secrets:
- secret: globalSecret
software:
`, controls, fleetServerURL))
require.NoError(t, err)
return f.Name()
}

// enable via the new nested keys on both platforms
enabledFile := writeConfig(` windows_enabled_and_configured: true
apple_settings:
managed_local_account_settings:
enabled: true
end_user_local_account_type: standard
windows_settings:
managed_local_account_settings:
enabled: true`)
_ = runAppForTest(t, []string{"gitops", "-f", enabledFile})
require.True(t, ds.SaveAppConfigFuncInvoked)
require.True(t, (*savedAppConfigPtr).MDM.MacOSSetup.EnableManagedLocalAccount.Value,
"apple_settings.managed_local_account_settings.enabled must converge onto the deprecated storage")
require.Equal(t, "standard", (*savedAppConfigPtr).MDM.MacOSSetup.EndUserLocalAccountType.Value)
require.True(t, (*savedAppConfigPtr).MDM.WindowsSettings.ManagedLocalAccountSettings.Enabled.Value)

// a second apply of the same file must be stable (no oscillation against the stored values)
_ = runAppForTest(t, []string{"gitops", "-f", enabledFile})
require.True(t, (*savedAppConfigPtr).MDM.MacOSSetup.EnableManagedLocalAccount.Value)
require.Equal(t, "standard", (*savedAppConfigPtr).MDM.MacOSSetup.EndUserLocalAccountType.Value)
require.True(t, (*savedAppConfigPtr).MDM.WindowsSettings.ManagedLocalAccountSettings.Enabled.Value)

// removing the keys from the YAML declaratively disables the feature on both platforms
disabledFile := writeConfig(` windows_enabled_and_configured: true`)
_ = runAppForTest(t, []string{"gitops", "-f", disabledFile})
require.False(t, (*savedAppConfigPtr).MDM.MacOSSetup.EnableManagedLocalAccount.Value)
require.Equal(t, "admin", (*savedAppConfigPtr).MDM.MacOSSetup.EndUserLocalAccountType.Value)
require.False(t, (*savedAppConfigPtr).MDM.WindowsSettings.ManagedLocalAccountSettings.Enabled.Value)

// the deprecated setup_experience spelling still applies, to the Apple value only
deprecatedFile := writeConfig(` windows_enabled_and_configured: true
setup_experience:
enable_create_local_admin_account: true`)
_ = runAppForTest(t, []string{"gitops", "-f", deprecatedFile})
require.True(t, (*savedAppConfigPtr).MDM.MacOSSetup.EnableManagedLocalAccount.Value)
require.False(t, (*savedAppConfigPtr).MDM.WindowsSettings.ManagedLocalAccountSettings.Enabled.Value,
"the deprecated field must not control the Windows setting")
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,19 @@ spec:
webhook_url: ""
macos_settings:
custom_settings: null
managed_local_account_settings:
enabled: false
end_user_local_account_type: admin
apple_settings:
configuration_profiles: null
managed_local_account_settings:
enabled: false
end_user_local_account_type: admin
windows_settings:
custom_settings: null
configuration_profiles: null
managed_local_account_settings:
enabled: false
android_settings:
custom_settings: null
configuration_profiles: null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,19 @@ spec:
webhook_url: ""
macos_settings:
custom_settings: null
managed_local_account_settings:
enabled: false
end_user_local_account_type: admin
apple_settings:
configuration_profiles: null
managed_local_account_settings:
enabled: false
end_user_local_account_type: admin
windows_settings:
custom_settings: null
configuration_profiles: null
managed_local_account_settings:
enabled: false
android_settings:
custom_settings: null
configuration_profiles: null
Expand Down
Loading
Loading