Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
83 changes: 83 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,86 @@ 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
// (generateControls emits the placeholder under the renamed setup_experience key)
_, ok := controlsRaw["setup_experience"]
require.False(t, ok, "expected no setup_experience TODO placeholder")

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