From c10ee875f23e5df13e19dd20d2cbec3ec4dbf693 Mon Sep 17 00:00:00 2001 From: gillespi314 <73313222+gillespi314@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:48:32 -0500 Subject: [PATCH] Fix validations for applying MDM config changes (#14517) --- changes/12997-mdm-config-validations | 1 + ee/server/service/teams.go | 57 ++- server/service/appconfig.go | 23 +- server/service/integration_mdm_test.go | 644 +++++++++++++++++++++++++ 4 files changed, 692 insertions(+), 33 deletions(-) create mode 100644 changes/12997-mdm-config-validations diff --git a/changes/12997-mdm-config-validations b/changes/12997-mdm-config-validations new file mode 100644 index 0000000000..1d1534cd8b --- /dev/null +++ b/changes/12997-mdm-config-validations @@ -0,0 +1 @@ +- Fixed issue where applying config changes would cause validation errors when MDM features were not enabled. \ No newline at end of file diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 9a6cb8b0f1..b752be6d98 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -771,19 +771,19 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, func (svc *Service) createTeamFromSpec( ctx context.Context, spec *fleet.TeamSpec, - defaults *fleet.AppConfig, + appCfg *fleet.AppConfig, secrets []*fleet.EnrollSecret, dryRun bool, ) (*fleet.Team, error) { agentOptions := &spec.AgentOptions if len(spec.AgentOptions) == 0 { - agentOptions = defaults.AgentOptions + agentOptions = appCfg.AgentOptions } // if a team spec is not provided, use the global features, otherwise // build a new config from the spec with default values applied. var err error - features := defaults.Features + features := appCfg.Features if spec.Features != nil { features, err = unmarshalWithGlobalDefaults(spec.Features) if err != nil { @@ -796,8 +796,8 @@ func (svc *Service) createTeamFromSpec( return nil, err } macOSSetup := spec.MDM.MacOSSetup - if macOSSetup.MacOSSetupAssistant.Set || macOSSetup.BootstrapPackage.Set { - if !defaults.MDM.EnabledAndConfigured { + if macOSSetup.MacOSSetupAssistant.Value != "" || macOSSetup.BootstrapPackage.Value != "" { + if !appCfg.MDM.EnabledAndConfigured { return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_setup", `Couldn't update macos_setup 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.`)) } @@ -809,7 +809,7 @@ func (svc *Service) createTeamFromSpec( } } - if enableDiskEncryption && !defaults.MDM.AtLeastOnePlatformEnabledAndConfigured() { + if enableDiskEncryption && !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() { return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)) } @@ -836,7 +836,8 @@ func (svc *Service) createTeamFromSpec( return nil, err } - if enableDiskEncryption && defaults.MDM.EnabledAndConfigured { + if enableDiskEncryption && appCfg.MDM.EnabledAndConfigured { + // TODO: Are we missing an activity or anything else for BitLocker here? if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil { return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow") } @@ -883,7 +884,7 @@ func (svc *Service) editTeamFromSpec( team.Config.MDM.MacOSUpdates = spec.MDM.MacOSUpdates } - oldMacOSDiskEncryption := team.Config.MDM.EnableDiskEncryption + oldEnableDiskEncryption := team.Config.MDM.EnableDiskEncryption if err := svc.applyTeamMacOSSettings(ctx, spec, &team.Config.MDM.MacOSSettings); err != nil { return err } @@ -896,38 +897,41 @@ func (svc *Service) editTeamFromSpec( } else if de := team.Config.MDM.MacOSSettings.DeprecatedEnableDiskEncryption; de != nil { team.Config.MDM.EnableDiskEncryption = *de } - if team.Config.MDM.EnableDiskEncryption && !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() { + didUpdateDiskEncryption := team.Config.MDM.EnableDiskEncryption != oldEnableDiskEncryption + if !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() && didUpdateDiskEncryption && team.Config.MDM.EnableDiskEncryption { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)) } oldMacOSSetup := team.Config.MDM.MacOSSetup - if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set || spec.MDM.MacOSSetup.BootstrapPackage.Set { - if !appCfg.MDM.EnabledAndConfigured { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_setup", - `Couldn't update macos_setup 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.`)) - } - if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set { - team.Config.MDM.MacOSSetup.MacOSSetupAssistant = spec.MDM.MacOSSetup.MacOSSetupAssistant - } - if spec.MDM.MacOSSetup.BootstrapPackage.Set { - team.Config.MDM.MacOSSetup.BootstrapPackage = spec.MDM.MacOSSetup.BootstrapPackage - } + var didUpdateSetupAssistant, didUpdateBootstrapPackage bool + if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set { + didUpdateSetupAssistant = oldMacOSSetup.MacOSSetupAssistant.Value != spec.MDM.MacOSSetup.MacOSSetupAssistant.Value + team.Config.MDM.MacOSSetup.MacOSSetupAssistant = spec.MDM.MacOSSetup.MacOSSetupAssistant + } + if spec.MDM.MacOSSetup.BootstrapPackage.Set { + didUpdateBootstrapPackage = oldMacOSSetup.BootstrapPackage.Value != spec.MDM.MacOSSetup.BootstrapPackage.Value + team.Config.MDM.MacOSSetup.BootstrapPackage = spec.MDM.MacOSSetup.BootstrapPackage + } + if !appCfg.MDM.EnabledAndConfigured && + ((didUpdateSetupAssistant && team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value != "") || + (didUpdateBootstrapPackage && team.Config.MDM.MacOSSetup.BootstrapPackage.Value != "")) { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_setup", + `Couldn't update macos_setup 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.`)) } - var didUpdateMacOSEndUserAuth bool - if spec.MDM.MacOSSetup.EnableEndUserAuthentication != oldMacOSSetup.EnableEndUserAuthentication { + didUpdateMacOSEndUserAuth := spec.MDM.MacOSSetup.EnableEndUserAuthentication != oldMacOSSetup.EnableEndUserAuthentication + if didUpdateMacOSEndUserAuth && spec.MDM.MacOSSetup.EnableEndUserAuthentication { if !appCfg.MDM.EnabledAndConfigured { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_setup.enable_end_user_authentication", `Couldn't update macos_setup.enable_end_user_authentication 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.`)) } - if spec.MDM.MacOSSetup.EnableEndUserAuthentication && appCfg.MDM.EndUserAuthentication.IsEmpty() { + if appCfg.MDM.EndUserAuthentication.IsEmpty() { // TODO: update this error message to include steps to resolve the issue once docs for IdP // config are available return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_setup.enable_end_user_authentication", `Couldn't enable macos_setup.enable_end_user_authentication because no IdP is configured for MDM features.`)) } - didUpdateMacOSEndUserAuth = true } team.Config.MDM.MacOSSetup.EnableEndUserAuthentication = spec.MDM.MacOSSetup.EnableEndUserAuthentication @@ -953,7 +957,8 @@ func (svc *Service) editTeamFromSpec( return err } } - if appCfg.MDM.EnabledAndConfigured && oldMacOSDiskEncryption != team.Config.MDM.EnableDiskEncryption { + if appCfg.MDM.EnabledAndConfigured && didUpdateDiskEncryption { + // TODO: Are we missing an activity or anything else for BitLocker here? var act fleet.ActivityDetails if team.Config.MDM.EnableDiskEncryption { act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name} @@ -1016,6 +1021,8 @@ func (svc *Service) applyTeamMacOSSettings(ctx context.Context, spec *fleet.Team field = "enable_disk_encryption" } if !appCfg.MDM.EnabledAndConfigured { + // TODO: Address potential edge cases when teams that previously utilized MDM features + // are edited later edited when MDM disabled return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError(fmt.Sprintf("macos_settings.%s", field), `Couldn't update macos_settings 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.`)) } diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 0fb9d8469e..884d73345b 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -606,34 +606,34 @@ func (svc *Service) validateMDM( if mdm.EnableDiskEncryption.Value && !license.IsPremium() { invalid.Append("macos_settings.enable_disk_encryption", ErrMissingLicense.Error()) } - if oldMdm.MacOSSetup.MacOSSetupAssistant.Value != mdm.MacOSSetup.MacOSSetupAssistant.Value && !license.IsPremium() { + if mdm.MacOSSetup.MacOSSetupAssistant.Value != "" && oldMdm.MacOSSetup.MacOSSetupAssistant.Value != mdm.MacOSSetup.MacOSSetupAssistant.Value && !license.IsPremium() { invalid.Append("macos_setup.macos_setup_assistant", ErrMissingLicense.Error()) } - if oldMdm.MacOSSetup.BootstrapPackage.Value != mdm.MacOSSetup.BootstrapPackage.Value && !license.IsPremium() { + if mdm.MacOSSetup.BootstrapPackage.Value != "" && oldMdm.MacOSSetup.BootstrapPackage.Value != mdm.MacOSSetup.BootstrapPackage.Value && !license.IsPremium() { invalid.Append("macos_setup.bootstrap_package", ErrMissingLicense.Error()) } - if oldMdm.MacOSSetup.EnableEndUserAuthentication != mdm.MacOSSetup.EnableEndUserAuthentication && !license.IsPremium() { + if mdm.MacOSSetup.EnableEndUserAuthentication && oldMdm.MacOSSetup.EnableEndUserAuthentication != mdm.MacOSSetup.EnableEndUserAuthentication && !license.IsPremium() { invalid.Append("macos_setup.enable_end_user_authentication", ErrMissingLicense.Error()) } // we want to use `oldMdm` here as this boolean is set by the fleet // server at startup and can't be modified by the user if !oldMdm.EnabledAndConfigured { - if len(mdm.MacOSSettings.CustomSettings) != len(oldMdm.MacOSSettings.CustomSettings) { + if len(mdm.MacOSSettings.CustomSettings) > 0 && len(mdm.MacOSSettings.CustomSettings) != len(oldMdm.MacOSSettings.CustomSettings) { invalid.Append("macos_settings.custom_settings", `Couldn't update macos_settings 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.`) } - if oldMdm.MacOSSetup.MacOSSetupAssistant.Value != mdm.MacOSSetup.MacOSSetupAssistant.Value { + if mdm.MacOSSetup.MacOSSetupAssistant.Value != "" && oldMdm.MacOSSetup.MacOSSetupAssistant.Value != mdm.MacOSSetup.MacOSSetupAssistant.Value { invalid.Append("macos_setup.macos_setup_assistant", `Couldn't update macos_setup 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.`) } - if oldMdm.MacOSSetup.BootstrapPackage.Value != mdm.MacOSSetup.BootstrapPackage.Value { + if mdm.MacOSSetup.BootstrapPackage.Value != "" && oldMdm.MacOSSetup.BootstrapPackage.Value != mdm.MacOSSetup.BootstrapPackage.Value { invalid.Append("macos_setup.bootstrap_package", `Couldn't update macos_setup 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.`) } - if oldMdm.MacOSSetup.EnableEndUserAuthentication != mdm.MacOSSetup.EnableEndUserAuthentication { + if mdm.MacOSSetup.EnableEndUserAuthentication && oldMdm.MacOSSetup.EnableEndUserAuthentication != mdm.MacOSSetup.EnableEndUserAuthentication { invalid.Append("macos_setup.enable_end_user_authentication", `Couldn't update macos_setup 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.`) } @@ -656,6 +656,8 @@ func (svc *Service) validateMDM( mdm.MacOSUpdates.Deadline != oldMdm.MacOSUpdates.Deadline if updatingVersion || updatingDeadline { + // TODO: Should we validate MDM configured on here too? + if !license.IsPremium() { invalid.Append("macos_updates.minimum_version", ErrMissingLicense.Error()) return @@ -668,6 +670,8 @@ func (svc *Service) validateMDM( // EndUserAuthentication // only validate SSO settings if they changed if mdm.EndUserAuthentication.SSOProviderSettings != oldMdm.EndUserAuthentication.SSOProviderSettings { + // TODO: Should we validate MDM configured on here too? + if !license.IsPremium() { invalid.Append("end_user_authentication", ErrMissingLicense.Error()) return @@ -684,6 +688,7 @@ func (svc *Service) validateMDM( invalid.Append("macos_setup.enable_end_user_authentication", `Couldn't enable macos_setup.enable_end_user_authentication because no IdP is configured for MDM features.`) } + // TODO: Should we validate MDM configured on here too? } updatingMacOSMigration := mdm.MacOSMigration.Enable != oldMdm.MacOSMigration.Enable || @@ -692,6 +697,8 @@ func (svc *Service) validateMDM( // MacOSMigration validation if updatingMacOSMigration { + // TODO: Should we validate MDM configured on here too? + if mdm.MacOSMigration.Enable { if license.Tier != fleet.TierPremium { invalid.Append("macos_migration.enable", ErrMissingLicense.Error()) @@ -719,7 +726,7 @@ func (svc *Service) validateMDM( // if either macOS or Windows MDM is enabled, this setting can be set. if !mdm.AtLeastOnePlatformEnabledAndConfigured() { - if mdm.EnableDiskEncryption.Valid && mdm.EnableDiskEncryption.Value != oldMdm.EnableDiskEncryption.Value { + if mdm.EnableDiskEncryption.Valid && mdm.EnableDiskEncryption.Value && mdm.EnableDiskEncryption.Value != oldMdm.EnableDiskEncryption.Value { invalid.Append("mdm.enable_disk_encryption", `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`) } diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 8c1d661b99..9891173a22 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -7387,6 +7387,650 @@ func (s *integrationMDMTestSuite) TestHostDiskEncryptionKey() { require.Equal(t, "", hostResp.Host.MDM.OSSettings.DiskEncryption.Detail) } +// /////////////////////////////////////////////////////////////////////////// +// Common MDM config test + +func (s *integrationMDMTestSuite) TestMDMEnabledAndConfigured() { + t := s.T() + ctx := context.Background() + + appConfig, err := s.ds.AppConfig(ctx) + originalCopy := appConfig.Copy() + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, s.ds.SaveAppConfig(ctx, originalCopy)) + }) + + checkAppConfig := func(t *testing.T, mdmEnabled, winEnabled bool) appConfigResponse { + acResp := appConfigResponse{} + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.True(t, acResp.AppConfig.MDM.AppleBMEnabledAndConfigured) + require.Equal(t, mdmEnabled, acResp.AppConfig.MDM.EnabledAndConfigured) + require.Equal(t, winEnabled, acResp.AppConfig.MDM.WindowsEnabledAndConfigured) + return acResp + } + + compareMacOSSetupValues := (func(t *testing.T, got fleet.MacOSSetup, want fleet.MacOSSetup) { + require.Equal(t, want.BootstrapPackage.Value, got.BootstrapPackage.Value) + require.Equal(t, want.MacOSSetupAssistant.Value, got.MacOSSetupAssistant.Value) + require.Equal(t, want.EnableEndUserAuthentication, got.EnableEndUserAuthentication) + }) + + insertBootstrapPackageAndSetupAssistant := func(t *testing.T, teamID *uint) { + var tmID uint + if teamID != nil { + tmID = *teamID + } + + // cleanup any residual bootstrap package + _ = s.ds.DeleteMDMAppleBootstrapPackage(ctx, tmID) + + // add new bootstrap package + require.NoError(t, s.ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{ + TeamID: tmID, + Name: "foo", + Token: uuid.New().String(), + Bytes: []byte("foo"), + Sha256: []byte("foo-sha256"), + })) + + // add new setup assistant + _, err := s.ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{ + TeamID: teamID, + Name: "bar", + ProfileUUID: uuid.New().String(), + Profile: []byte("{}"), + }) + require.NoError(t, err) + } + + // TODO: SOme global MDM config settings don't have MDMEnabledAndConfigured or + // WindowsMDMEnabledAndConfigured validations currently. Either add validations + // and test them or test abscence of validation. + t.Run("apply app config spec", func(t *testing.T) { + t.Run("disk encryption", func(t *testing.T) { + t.Cleanup(func() { + require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig)) + }) + + acResp := checkAppConfig(t, true, true) + require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabled by default + + // initialize our test app config + ac := appConfig.Copy() + ac.AgentOptions = nil + + // enable disk encryption + ac.MDM.EnableDiskEncryption = optjson.SetBool(true) + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, true) // both mac and windows mdm enabled + require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // enabled + + // directly set MDM.EnabledAndConfigured to false + ac.MDM.EnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabling mdm doesn't change disk encryption + + // making an unrelated change should not cause validation error + ac.OrgInfo.OrgName = "f1337" + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // no change + require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName) + + // disabling disk encryption doesn't cause validation error because Windows is still enabled + ac.MDM.EnableDiskEncryption = optjson.SetBool(false) + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabled + require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName) + + // enabling disk encryption doesn't cause validation error because Windows is still enabled + ac.MDM.EnableDiskEncryption = optjson.SetBool(true) + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // enabled + + // directly set MDM.WindowsEnabledAndConfigured to false + ac.MDM.WindowsEnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled + require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabling mdm doesn't change disk encryption + + // changing unrelated config doesn't cause validation error + ac.OrgInfo.OrgName = "f1338" + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled + require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // no change + require.Equal(t, "f1338", acResp.AppConfig.OrgInfo.OrgName) + + // changing MDM config doesn't cause validation error when switching to default values + ac.MDM.EnableDiskEncryption = optjson.SetBool(false) + // TODO: Should it be ok to disable disk encryption when MDM is disabled? + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled + require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // changed to disabled + + // changing MDM config does cause validation error when switching to non-default vailes + ac.MDM.EnableDiskEncryption = optjson.SetBool(true) + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp) + acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled + require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // still disabled + }) + + t.Run("macos setup", func(t *testing.T) { + t.Cleanup(func() { + require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig)) + }) + + acResp := checkAppConfig(t, true, true) + compareMacOSSetupValues(t, fleet.MacOSSetup{}, acResp.AppConfig.MDM.MacOSSetup) // disabled by default + + // initialize our test app config + ac := appConfig.Copy() + ac.AgentOptions = nil + ac.MDM.EndUserAuthentication = fleet.MDMEndUserAuthentication{ + SSOProviderSettings: fleet.SSOProviderSettings{ + EntityID: "sso-provider", + IDPName: "sso-provider", + MetadataURL: "https://sso-provider.example.com/metadata", + }, + } + + // add db records for bootstrap package and setup assistant + insertBootstrapPackageAndSetupAssistant(t, nil) + + // enable MacOSSetup options + ac.MDM.MacOSSetup = fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString("foo"), + EnableEndUserAuthentication: true, + MacOSSetupAssistant: optjson.SetString("bar"), + } + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, true) // both mac and windows mdm enabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied + + // directly set MDM.EnabledAndConfigured to false + ac.MDM.EnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // still applied + + // making an unrelated change should not cause validation error + ac.OrgInfo.OrgName = "f1337" + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // still applied + require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName) + + // disabling doesn't cause validation error + ac.MDM.MacOSSetup = fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString(""), + EnableEndUserAuthentication: false, + MacOSSetupAssistant: optjson.SetString(""), + } + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied + require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName) + + // bootstrap package and setup assistant were removed so reinsert records for next test + insertBootstrapPackageAndSetupAssistant(t, nil) + + // enable MacOSSetup options fails because only Windows is enabled. + ac.MDM.MacOSSetup = fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString("foo"), + EnableEndUserAuthentication: true, + MacOSSetupAssistant: optjson.SetString("bar"), + } + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp) + acResp = checkAppConfig(t, false, true) // only windows enabled + + // directly set MDM.EnabledAndConfigured to true and windows to false + ac.MDM.EnabledAndConfigured = true + ac.MDM.WindowsEnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp = checkAppConfig(t, true, false) // mac enabled, windows disabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // directly applied + + // changing unrelated config doesn't cause validation error + ac.OrgInfo.OrgName = "f1338" + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, false) // mac enabled, windows disabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // no change + require.Equal(t, "f1338", acResp.AppConfig.OrgInfo.OrgName) + + // disabling doesn't cause validation error + ac.MDM.MacOSSetup = fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString(""), + EnableEndUserAuthentication: false, + MacOSSetupAssistant: optjson.SetString(""), + } + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, false) // only windows mdm enabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied + + // bootstrap package and setup assistant were removed so reinsert records for next test + insertBootstrapPackageAndSetupAssistant(t, nil) + + // enable MacOSSetup options succeeds because only Windows is disabled + ac.MDM.MacOSSetup = fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString("foo"), + EnableEndUserAuthentication: true, + MacOSSetupAssistant: optjson.SetString("bar"), + } + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, false) // only windows enabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied + + // directly set MDM.EnabledAndConfigured to false + ac.MDM.EnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // still applied + + // changing unrelated config doesn't cause validation error + ac.OrgInfo.OrgName = "f1339" + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, false) // both disabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // no change + require.Equal(t, "f1339", acResp.AppConfig.OrgInfo.OrgName) + + // setting macos setup empty values doesn't cause validation error when mdm is disabled + ac.MDM.MacOSSetup = fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString(""), + EnableEndUserAuthentication: false, + MacOSSetupAssistant: optjson.SetString(""), + } + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, false) // both disabled + compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied + + // setting macos setup to non-empty values fails because mdm disabled + ac.MDM.MacOSSetup = fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString("foo"), + EnableEndUserAuthentication: true, + MacOSSetupAssistant: optjson.SetString("bar"), + } + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp) + acResp = checkAppConfig(t, false, false) // both disabled + }) + + t.Run("macos settings", func(t *testing.T) { + t.Cleanup(func() { + require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig)) + }) + + // initialize our test app config + ac := appConfig.Copy() + ac.AgentOptions = nil + ac.MDM.MacOSSettings.CustomSettings = []string{} + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp := checkAppConfig(t, true, true) + require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings) + + // add custom settings + ac.MDM.MacOSSettings.CustomSettings = []string{"foo", "bar"} + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, true) // both mac and windows mdm enabled + require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // applied + + // directly set MDM.EnabledAndConfigured to false + ac.MDM.EnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // still applied + + // making an unrelated change should not cause validation error + ac.OrgInfo.OrgName = "f1337" + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // still applied + require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName) + + // remove custom settings + ac.MDM.MacOSSettings.CustomSettings = []string{} + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, true) // only windows mdm enabled + require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings) + + // add custom settings fails because only windows is enabled + ac.MDM.MacOSSettings.CustomSettings = []string{"foo", "bar"} + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp) + acResp = checkAppConfig(t, false, true) // only windows enabled + require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings) + + // directly set MDM.EnabledAndConfigured to true and windows to false + ac.MDM.EnabledAndConfigured = true + ac.MDM.WindowsEnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp = checkAppConfig(t, true, false) // mac enabled, windows disabled + require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // directly applied + + // changing unrelated config doesn't cause validation error + ac.OrgInfo.OrgName = "f1338" + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, false) // mac enabled, windows disabled + require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // no change + require.Equal(t, "f1338", acResp.AppConfig.OrgInfo.OrgName) + + // remove custom settings doesn't cause validation error + ac.MDM.MacOSSettings.CustomSettings = []string{} + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, false) // only windows mdm enabled + require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings) + + // add custom settings suceeds because only Windows is disabled + ac.MDM.MacOSSettings.CustomSettings = []string{"foo", "bar"} + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, true, false) // both mac and windows mdm enabled + require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // applied + + // directly set MDM.WindowsEnabledAndConfigured to false + ac.MDM.EnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled + require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // applied + + // changing unrelated config doesn't cause validation error + ac.OrgInfo.OrgName = "f1339" + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, false) // both disabled + require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // applied + require.Equal(t, "f1339", acResp.AppConfig.OrgInfo.OrgName) + + // setting empty values doesn't cause validation error when mdm is disabled + ac.MDM.MacOSSettings.CustomSettings = []string{} + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, false) // both disabled + require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings) + + // setting non-empty values fails because mdm disabled + ac.MDM.MacOSSettings.CustomSettings = []string{"foo", "bar"} + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp) + acResp = checkAppConfig(t, false, false) // both disabled + require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings) + }) + }) + + // TODO: Improve validations and related test coverage of team MDM config. + // Some settings don't have MDMEnabledAndConfigured or WindowsMDMEnabledAndConfigured + // validations currently. Either add vailidations and test them or test abscence + // of validation. Also, the tests below only cover a limited set of permutations + // compared to the app config tests above and should be expanded accordingly. + t.Run("modify team", func(t *testing.T) { + t.Cleanup(func() { + require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig)) + }) + + checkTeam := func(t *testing.T, team *fleet.Team, checkMDM *fleet.TeamPayloadMDM) teamResponse { + var wantDiskEncryption bool + var wantMacOSSetup fleet.MacOSSetup + if checkMDM != nil { + if checkMDM.MacOSSetup != nil { + wantMacOSSetup = *checkMDM.MacOSSetup + // bootstrap package always ignored by modify team endpoint so expect original value + wantMacOSSetup.BootstrapPackage = team.Config.MDM.MacOSSetup.BootstrapPackage + // setup assistant always ignored by modify team endpoint so expect original value + wantMacOSSetup.MacOSSetupAssistant = team.Config.MDM.MacOSSetup.MacOSSetupAssistant + } + wantDiskEncryption = checkMDM.EnableDiskEncryption.Value + } + + var resp teamResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &resp) + require.Equal(t, team.Name, resp.Team.Name) + require.Equal(t, wantDiskEncryption, resp.Team.Config.MDM.EnableDiskEncryption) + require.Equal(t, wantMacOSSetup.BootstrapPackage.Value, resp.Team.Config.MDM.MacOSSetup.BootstrapPackage.Value) + require.Equal(t, wantMacOSSetup.MacOSSetupAssistant.Value, resp.Team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value) + require.Equal(t, wantMacOSSetup.EnableEndUserAuthentication, resp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) + + return resp + } + + // initialize our test app config + ac := appConfig.Copy() + ac.AgentOptions = nil + ac.MDM.EnabledAndConfigured = false + ac.MDM.WindowsEnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + checkAppConfig(t, false, false) // both mac and windows mdm disabled + + var createTeamResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", createTeamRequest{fleet.TeamPayload{ + Name: ptr.String("Ninjas"), + MDM: &fleet.TeamPayloadMDM{EnableDiskEncryption: optjson.SetBool(true)}, // mdm is ignored by the create team endpoint + }}, http.StatusOK, &createTeamResp) + team := createTeamResp.Team + getTeamResp := checkTeam(t, team, nil) // newly created team has empty mdm config + + t.Cleanup(func() { + require.NoError(t, s.ds.DeleteTeam(ctx, team.ID)) + }) + + // TODO: Add cases for other team MDM config (e.g., macos settings, macos updates, + // migration) and for other permutations of starting values (see app config tests above). + cases := []struct { + name string + mdm *fleet.TeamPayloadMDM + expectedStatus int + }{ + { + "mdm empty", + &fleet.TeamPayloadMDM{}, + http.StatusOK, + }, + { + "mdm all zero values", + &fleet.TeamPayloadMDM{ + EnableDiskEncryption: optjson.SetBool(false), + MacOSSetup: &fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString(""), + EnableEndUserAuthentication: false, + MacOSSetupAssistant: optjson.SetString(""), + }, + }, + http.StatusOK, + }, + { + "bootstrap package", + &fleet.TeamPayloadMDM{ + MacOSSetup: &fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString("some-package"), + }, + }, + // bootstrap package is always ignored by the modify team endpoint + http.StatusOK, + }, + { + "setup assistant", + &fleet.TeamPayloadMDM{ + MacOSSetup: &fleet.MacOSSetup{ + MacOSSetupAssistant: optjson.SetString("some-setup-assistant"), + }, + }, + // setup assistant is always ignored by the modify team endpoint + http.StatusOK, + }, + { + "enable disk encryption", + &fleet.TeamPayloadMDM{ + EnableDiskEncryption: optjson.SetBool(true), + }, + // disk encryption requires mdm enabled and configured + http.StatusUnprocessableEntity, + }, + { + "enable end user auth", + &fleet.TeamPayloadMDM{ + MacOSSetup: &fleet.MacOSSetup{ + EnableEndUserAuthentication: true, + }, + }, + // disk encryption requires mdm enabled and configured + http.StatusUnprocessableEntity, + }, + } + + for _, c := range cases { + // TODO: Add tests for other combinations of mac and windows mdm enabled/disabled + t.Run(c.name, func(t *testing.T) { + checkAppConfig(t, false, false) // both mac and windows mdm disabled + + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ + Name: &team.Name, + Description: ptr.String(c.name), + MDM: c.mdm, + }, c.expectedStatus, &getTeamResp) + + if c.expectedStatus == http.StatusOK { + getTeamResp = checkTeam(t, team, c.mdm) + require.Equal(t, c.name, getTeamResp.Team.Description) + } else { + checkTeam(t, team, nil) + } + }) + } + }) + + // TODO: Improve validations and related test coverage of team MDM config. + // Some settings don't have MDMEnabledAndConfigured or WindowsMDMEnabledAndConfigured + // validations currently. Either add vailidations and test them or test abscence + // of validation. Also, the tests below only cover a limited set of permutations + // compared to the app config tests above and should be expanded accordingly. + t.Run("edit team spec", func(t *testing.T) { + t.Cleanup(func() { + require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig)) + }) + + checkTeam := func(t *testing.T, team *fleet.Team, checkMDM *fleet.TeamSpecMDM) teamResponse { + var wantDiskEncryption bool + var wantMacOSSetup fleet.MacOSSetup + if checkMDM != nil { + wantMacOSSetup = checkMDM.MacOSSetup + wantDiskEncryption = checkMDM.EnableDiskEncryption.Value + } + + var resp teamResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &resp) + require.Equal(t, team.Name, resp.Team.Name) + require.Equal(t, wantDiskEncryption, resp.Team.Config.MDM.EnableDiskEncryption) + require.Equal(t, wantMacOSSetup.BootstrapPackage.Value, resp.Team.Config.MDM.MacOSSetup.BootstrapPackage.Value) + require.Equal(t, wantMacOSSetup.MacOSSetupAssistant.Value, resp.Team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value) + require.Equal(t, wantMacOSSetup.EnableEndUserAuthentication, resp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) + + return resp + } + + // initialize our test app config + ac := appConfig.Copy() + ac.AgentOptions = nil + ac.MDM.EnabledAndConfigured = false + ac.MDM.WindowsEnabledAndConfigured = false + require.NoError(t, s.ds.SaveAppConfig(ctx, ac)) + checkAppConfig(t, false, false) // both mac and windows mdm disabled + + // create a team from spec + tmSpecReq := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "Pirates"}}} + var tmSpecResp applyTeamSpecsResponse + s.DoJSON("POST", "/api/latest/fleet/spec/teams", tmSpecReq, http.StatusOK, &tmSpecResp) + teamID, ok := tmSpecResp.TeamIDsByName["Pirates"] + require.True(t, ok) + team := fleet.Team{ID: teamID, Name: "Pirates"} + checkTeam(t, &team, nil) // newly created team has empty mdm config + + t.Cleanup(func() { + require.NoError(t, s.ds.DeleteTeam(ctx, team.ID)) + }) + + // TODO: Add cases for other team MDM config (e.g., macos settings, macos updates, + // migration) and for other permutations of starting values (see app config tests above). + cases := []struct { + name string + mdm *fleet.TeamSpecMDM + expectedStatus int + }{ + { + "mdm empty", + &fleet.TeamSpecMDM{}, + http.StatusOK, + }, + { + "mdm all zero values", + &fleet.TeamSpecMDM{ + EnableDiskEncryption: optjson.SetBool(false), + MacOSSetup: fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString(""), + EnableEndUserAuthentication: false, + MacOSSetupAssistant: optjson.SetString(""), + }, + }, + http.StatusOK, + }, + { + "bootstrap package", + &fleet.TeamSpecMDM{ + MacOSSetup: fleet.MacOSSetup{ + BootstrapPackage: optjson.SetString("some-package"), + }, + }, + // bootstrap package requires mdm enabled and configured + http.StatusUnprocessableEntity, + }, + { + "setup assistant", + &fleet.TeamSpecMDM{ + MacOSSetup: fleet.MacOSSetup{ + MacOSSetupAssistant: optjson.SetString("some-setup-assistant"), + }, + }, + // setup assistant requires mdm enabled and configured + http.StatusUnprocessableEntity, + }, + { + "enable disk encryption", + &fleet.TeamSpecMDM{ + EnableDiskEncryption: optjson.SetBool(true), + }, + // disk encryption requires mdm enabled and configured + http.StatusUnprocessableEntity, + }, + { + "enable end user auth", + &fleet.TeamSpecMDM{ + MacOSSetup: fleet.MacOSSetup{ + EnableEndUserAuthentication: true, + }, + }, + // disk encryption requires mdm enabled and configured + http.StatusUnprocessableEntity, + }, + } + + for _, c := range cases { + // TODO: Add tests for other combinations of mac and windows mdm enabled/disabled + t.Run(c.name, func(t *testing.T) { + checkAppConfig(t, false, false) // both mac and windows mdm disabled + + tmSpecReq = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ + Name: team.Name, + MDM: *c.mdm, + }}} + s.DoJSON("POST", "/api/latest/fleet/spec/teams", tmSpecReq, c.expectedStatus, &tmSpecResp) + + if c.expectedStatus == http.StatusOK { + checkTeam(t, &team, c.mdm) + } else { + checkTeam(t, &team, nil) + } + }) + } + }) +} + // /////////////////////////////////////////////////////////////////////////// // Common helpers