From a9584dc32f22b4e98df7772b11eb7293b4640f1c Mon Sep 17 00:00:00 2001 From: gillespi314 <73313222+gillespi314@users.noreply.github.com> Date: Wed, 10 May 2023 15:22:08 -0500 Subject: [PATCH] Allow end user authentication during automatic MDM enrollment to be enabled on a per-team basis (#11566) --- ...issue-10999-11000-mdm-enable-end-user-auth | 3 + cmd/fleetctl/apply_test.go | 109 ++++++++ .../expectedGetConfigAppConfigJson.json | 1 + .../expectedGetConfigAppConfigYaml.yml | 1 + ...ectedGetConfigIncludeServerConfigJson.json | 1 + ...pectedGetConfigIncludeServerConfigYaml.yml | 1 + .../testdata/expectedGetTeamsJson.json | 2 + .../testdata/expectedGetTeamsYaml.yml | 2 + .../macosSetupExpectedAppConfigEmpty.yml | 1 + .../macosSetupExpectedAppConfigSet.yml | 1 + .../macosSetupExpectedTeam1And2Empty.yml | 1 + .../macosSetupExpectedTeam1And2Set.yml | 1 + .../testdata/macosSetupExpectedTeam1Empty.yml | 1 + docs/Using-Fleet/Audit-Activities.md | 34 +++ docs/Using-Fleet/Permissions.md | 2 + docs/Using-Fleet/REST-API.md | 49 +++- ee/server/service/mdm.go | 87 +++++- ee/server/service/teams.go | 67 ++++- server/datastore/mysql/schema.sql | 2 +- server/fleet/activities.go | 41 +++ server/fleet/app.go | 5 +- server/fleet/apple_mdm.go | 12 + server/fleet/service.go | 4 + server/mdm/apple/apple_mdm.go | 9 +- server/service/appconfig.go | 48 +++- server/service/apple_mdm.go | 35 +++ server/service/apple_mdm_test.go | 177 +++++++++++- server/service/handler.go | 1 + server/service/integration_mdm_test.go | 261 ++++++++++++++++++ 29 files changed, 922 insertions(+), 37 deletions(-) create mode 100644 changes/issue-10999-11000-mdm-enable-end-user-auth diff --git a/changes/issue-10999-11000-mdm-enable-end-user-auth b/changes/issue-10999-11000-mdm-enable-end-user-auth new file mode 100644 index 0000000000..639372073c --- /dev/null +++ b/changes/issue-10999-11000-mdm-enable-end-user-auth @@ -0,0 +1,3 @@ +- Added `PATCH /mdm/apple/setup` endpoint. +- Added `enable_end_user_authentication` to `mdm.macos_setup` in global app config and team config + objects. diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 6fc50e3359..20b27db3b3 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -1258,6 +1258,14 @@ kind: config spec: mdm: macos_setup: +` + appConfigSpecEnableEndUserAuth = ` +apiVersion: v1 +kind: config +spec: + mdm: + macos_setup: + enable_end_user_authentication: %s ` team1Spec = ` apiVersion: v1 @@ -1299,6 +1307,16 @@ spec: macos_setup: bootstrap_package: %s macos_setup_assistant: %s +` + team1SpecEnableEndUserAuth = ` +apiVersion: v1 +kind: team +spec: + team: + name: tm1 + mdm: + macos_setup: + enable_end_user_authentication: %s ` ) @@ -1338,6 +1356,23 @@ spec: assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked) assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked) assert.False(t, ds.SaveTeamFuncInvoked) + + // enable_end_user_authentication is premium only + name = writeTmpYml(t, fmt.Sprintf(appConfigSpecEnableEndUserAuth, "true")) + runAppCheckErr(t, []string{"apply", "-f", name}, `applying fleet config: PATCH /api/latest/fleet/config received status 422 Validation Failed: missing or invalid license`) + assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked) + assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked) + assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked) + assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked) + assert.False(t, ds.SaveTeamFuncInvoked) + + name = writeTmpYml(t, fmt.Sprintf(team1SpecEnableEndUserAuth, "true")) + runAppCheckErr(t, []string{"apply", "-f", name}, `applying teams: missing or invalid license`) + assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked) + assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked) + assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked) + assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked) + assert.False(t, ds.SaveTeamFuncInvoked) }) t.Run("setup assistant invalid file, not json, invalid json", func(t *testing.T) { @@ -1711,6 +1746,80 @@ spec: assert.Equal(t, "", mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value) mockStore.Unlock() }) + + // // TODO: restore this test when we have a way to mock the Apple Business Manager API in + // // fleetctl tests + // t.Run("enable end user authentication", func(t *testing.T) { + // ds := setupServer(t, true) + + // // setup app config + // b, err := os.ReadFile(filepath.Join("testdata", "macosSetupExpectedAppConfigEmpty.yml")) + // require.NoError(t, err) + // expectedNotSetAppConfg := string(b) + // assert.YAMLEq(t, expectedNotSetAppConfg, runAppForTest(t, []string{"get", "config", "--yaml"})) + + // // enable end user auth in app config + // name := writeTmpYml(t, fmt.Sprintf(appConfigSpecEnableEndUserAuth, "true")) + // _, err = runAppNoChecks([]string{"apply", "-f", name}) + // require.NoError(t, err) + // assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked) + // assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked) + // assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked) + // assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked) + // assert.True(t, ds.SaveAppConfigFuncInvoked) + // expectedSetAppCfg := strings.ReplaceAll(expectedNotSetAppConfg, "enable_end_user_authentication: false", "enable_end_user_authentication: true") + // assert.YAMLEq(t, expectedSetAppCfg, runAppForTest(t, []string{"get", "config", "--yaml"})) + // ds.SaveAppConfigFuncInvoked = false + + // // disable end user auth in app config + // name = writeTmpYml(t, fmt.Sprintf(appConfigSpecEnableEndUserAuth, "false")) + // _, err = runAppNoChecks([]string{"apply", "-f", name}) + // require.NoError(t, err) + // assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked) + // assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked) + // assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked) + // assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked) + // assert.True(t, ds.SaveAppConfigFuncInvoked) + // assert.YAMLEq(t, expectedNotSetAppConfg, runAppForTest(t, []string{"get", "config", "--yaml"})) + // ds.SaveAppConfigFuncInvoked = false + + // // setup team config + // assert.False(t, ds.SaveTeamFuncInvoked) + // b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1Empty.yml")) + // require.NoError(t, err) + // expectedNotSetTeam1 := string(b) + // assert.YAMLEq(t, expectedNotSetTeam1, runAppForTest(t, []string{"get", "teams", "--yaml"})) + + // // enable end user auth in team config + // name = writeTmpYml(t, fmt.Sprintf(team1SpecEnableEndUserAuth, "true")) + // _, err = runAppNoChecks([]string{"apply", "-f", name}) + // require.NoError(t, err) + // assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked) + // assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked) + // assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked) + // assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked) + // assert.False(t, ds.SaveAppConfigFuncInvoked) + // assert.True(t, ds.SaveTeamFuncInvoked) + // expectedSetTeam1 := strings.ReplaceAll(expectedNotSetTeam1, "enable_end_user_authentication: false", "enable_end_user_authentication: true") + // expectedSetTeam1 = strings.ReplaceAll(expectedSetTeam1, "enable_host_users: false", "enable_host_users: true") + // expectedSetTeam1 = strings.ReplaceAll(expectedSetTeam1, "enable_software_inventory: false", "enable_software_inventory: true") + // assert.YAMLEq(t, expectedSetTeam1, runAppForTest(t, []string{"get", "teams", "--yaml"})) + // ds.SaveTeamFuncInvoked = false + + // // disable end user auth in team config + // name = writeTmpYml(t, fmt.Sprintf(team1SpecEnableEndUserAuth, "false")) + // _, err = runAppNoChecks([]string{"apply", "-f", name}) + // require.NoError(t, err) + // assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked) + // assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked) + // assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked) + // assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked) + // assert.False(t, ds.SaveAppConfigFuncInvoked) + // assert.True(t, ds.SaveTeamFuncInvoked) + // expectedSetTeam1 = strings.ReplaceAll(expectedSetTeam1, "enable_end_user_authentication: true", "enable_end_user_authentication: false") + // assert.YAMLEq(t, expectedSetTeam1, runAppForTest(t, []string{"get", "teams", "--yaml"})) + // ds.SaveTeamFuncInvoked = false + // }) } func TestApplySpecs(t *testing.T) { diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index 245cec6eaa..db881bedfd 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -92,6 +92,7 @@ }, "macos_setup": { "bootstrap_package": null, + "enable_end_user_authentication": false, "macos_setup_assistant": null }, "end_user_authentication": { diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 8b3a2d65dd..534f395c11 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -26,6 +26,7 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: + enable_end_user_authentication: false macos_setup_assistant: end_user_authentication: idp_name: "" diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index 64e72cf84d..e08ffb0f1a 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -50,6 +50,7 @@ }, "macos_setup": { "bootstrap_package": null, + "enable_end_user_authentication": false, "macos_setup_assistant": null }, "end_user_authentication": { diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 11d01081c3..234ddd0899 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -26,6 +26,7 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: + enable_end_user_authentication: false macos_setup_assistant: end_user_authentication: idp_name: "" diff --git a/cmd/fleetctl/testdata/expectedGetTeamsJson.json b/cmd/fleetctl/testdata/expectedGetTeamsJson.json index 3887409f71..333ae91c32 100644 --- a/cmd/fleetctl/testdata/expectedGetTeamsJson.json +++ b/cmd/fleetctl/testdata/expectedGetTeamsJson.json @@ -34,6 +34,7 @@ }, "macos_setup": { "bootstrap_package": null, + "enable_end_user_authentication": false, "macos_setup_assistant": null } }, @@ -93,6 +94,7 @@ }, "macos_setup": { "bootstrap_package": null, + "enable_end_user_authentication": false, "macos_setup_assistant": null } }, diff --git a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml index 246e315507..42ddb13426 100644 --- a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml @@ -15,6 +15,7 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: + enable_end_user_authentication: false macos_setup_assistant: name: team1 --- @@ -43,5 +44,6 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: + enable_end_user_authentication: false macos_setup_assistant: name: team2 diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 4b6eafb88c..3ae0550b95 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -23,6 +23,7 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: null + enable_end_user_authentication: false macos_setup_assistant: null macos_updates: deadline: "" diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 592f9dc776..2ba4e9f916 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -23,6 +23,7 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: %s + enable_end_user_authentication: false macos_setup_assistant: %s macos_updates: deadline: "" diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml index 9cec873e7e..5aa72cb8d8 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml @@ -12,6 +12,7 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: null + enable_end_user_authentication: false macos_setup_assistant: null macos_updates: deadline: "" diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml index db9038a72d..808b96abac 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml @@ -12,6 +12,7 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: %s + enable_end_user_authentication: false macos_setup_assistant: %s macos_updates: deadline: "" diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml index 8f4a852d3a..e807ec3396 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml @@ -12,6 +12,7 @@ spec: enable_disk_encryption: false macos_setup: bootstrap_package: null + enable_end_user_authentication: false macos_setup_assistant: null macos_updates: deadline: "" diff --git a/docs/Using-Fleet/Audit-Activities.md b/docs/Using-Fleet/Audit-Activities.md index ab4c09fdf7..bba116841f 100644 --- a/docs/Using-Fleet/Audit-Activities.md +++ b/docs/Using-Fleet/Audit-Activities.md @@ -769,6 +769,40 @@ This activity contains the following fields: } ``` +### Type `enabled_macos_setup_end_user_auth` + +Generated when a user turns on end user authentication for macOS hosts that automatically enroll to a team (or no team). + +This activity contains the following fields: +- "team_id": The ID of the team that end user authentication applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that end user authentication applies to, null if it applies to devices that are not in a team. + +#### Example + +```json +{ + "team_id": 123, + "team_name": "Workstations" +} +``` + +### Type `disabled_macos_setup_end_user_auth` + +Generated when a user turns off end user authentication for macOS hosts that automatically enroll to a team (or no team). + +This activity contains the following fields: +- "team_id": The ID of the team that end user authentication applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that end user authentication applies to, null if it applies to devices that are not in a team. + +#### Example + +```json +{ + "team_id": 123, + "team_name": "Workstations" +} +``` + \ No newline at end of file diff --git a/docs/Using-Fleet/Permissions.md b/docs/Using-Fleet/Permissions.md index dd4d049de1..4264f09d84 100644 --- a/docs/Using-Fleet/Permissions.md +++ b/docs/Using-Fleet/Permissions.md @@ -81,6 +81,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | Upload an EULA file for MDM automatic enrollment\* | | | | ✅ | | | View/download MDM macOS setup assistant\* | | | ✅ | ✅ | | | Edit/upload MDM macOS setup assistant\* | | | ✅ | ✅ | | +| Enable/disable MDM macOS setup end user authentication\* | | | ✅ | ✅ | | \* Applies only to Fleet Premium @@ -138,6 +139,7 @@ Users that are members of multiple teams can be assigned different roles for eac | Edit [team MDM settings](https://fleetdm.com/docs/using-fleet/mdm-macos-settings) | | | | ✅ | ✅ | | View/download MDM macOS setup assistant | | | ✅ | ✅ | | | Edit/upload MDM macOS setup assistant | | | ✅ | ✅ | | +| Enable/disable MDM macOS setup end user authentication | | | ✅ | ✅ | | \* Applies only to [Fleet REST API](https://fleetdm.com/docs/using-fleet/rest-api) diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index 69a5d0fc31..a0510b503d 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -859,6 +859,7 @@ None. }, "macos_setup": { "bootstrap_package": "", + "enable_end_user_authentication": false, "macos_setup_assistant": "path/to/config.json" } }, @@ -935,16 +936,6 @@ None. "integrations": { "jira": null }, - "mdm": { - "apple_bm_terms_expired": false, - "apple_bm_enabled_and_configured": false, - "enabled_and_configured": false, - "apple_bm_default_team": "", - "macos_updates": { - "minimum_version": "12.3.1", - "deadline": "2022-01-01" - } - }, "logging": { "debug": false, "json": false, @@ -1047,6 +1038,7 @@ Modifies the Fleet's configuration with the supplied information. | deadline | string | body | _mdm.macos_updates settings_. Hosts that belong to no team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. **Requires Fleet Premium license** | | custom_settings | list | body | _mdm.macos_settings settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will have those custom profiles applied. | | enable_disk_encryption | boolean | body | _mdm.macos_settings settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. **Requires Fleet Premium license** | +| enable_end_user_authentication | boolean | body | _mdm.macos_setup settings_. If set to true, end user authentication will be required during automatic MDM enrollment of new macOS devices. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup#end-user-authentication). **Requires Fleet Premium license** | | additional_queries | boolean | body | Whether or not additional queries are enabled on hosts. | | force | bool | query | Force apply the agent options even if there are validation errors. | | dry_run | bool | query | Validate the configuration and return any validation errors, but do not apply the changes. | @@ -1142,6 +1134,7 @@ Modifies the Fleet's configuration with the supplied information. }, "macos_setup": { "bootstrap_package": "", + "enable_end_user_authentication": false, "macos_setup_assistant": "path/to/config.json" } }, @@ -4293,6 +4286,38 @@ The summary can optionally be filtered by team id. } ``` +### Turn on end user authentication for macOS setup + +_Available in Fleet Premium_ + +`PATCH /api/v1/fleet/mdm/apple/setup` + +#### Parameters + +| Name | Type | In | Description | +| ------------- | ------ | ---- | -------------------------------------------------------------------------------------- | +| team_id | integer | body | The team ID to apply the settings to. Settings applied to hosts in no team if absent. | +| enable_end_user_authentication | boolean | body | Whether end user authentication should be enabled for new macOS devices that automatically enroll to the team (or no team). | + +#### Example + +`PATCH /api/v1/fleet/mdm/apple/setup` + +##### Request body + +```json +{ + "team_id": 1, + "enabled_end_user_authentication": true +} +``` + +##### Default response + +`Status: 204` + + + ### Upload an EULA file _Available in Fleet Premium_ @@ -6370,6 +6395,7 @@ _Available in Fleet Premium_ }, "macos_setup": { "bootstrap_package": "", + "enable_end_user_authentication": false, "macos_setup_assistant": "path/to/config.json" } } @@ -6482,6 +6508,8 @@ _Available in Fleet Premium_ |     deadline | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. | |   macos_settings | object | body | MacOS-specific settings. | |     enable_disk_encryption | boolean | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. | +|   macos_setup | object | body | Setup for automatic MDM enrollment of macOS devices. | +|     enable_end_user_authentication | boolean | body | If set to true, end user authentication will be required during automatic MDM enrollment of new macOS devices. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup#end-user-authentication). | #### Example (add users to a team) @@ -6547,6 +6575,7 @@ _Available in Fleet Premium_ }, "macos_setup": { "bootstrap_package": "", + "enable_end_user_authentication": false, "macos_setup_assistant": "path/to/config.json" } } diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 1ee9599e49..88f37539e8 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -154,6 +154,92 @@ func (svc *Service) MDMAppleDisableFileVaultAndEscrow(ctx context.Context, teamI return ctxerr.Wrap(ctx, err, "disabling FileVault") } +func (svc *Service) UpdateMDMAppleSetup(ctx context.Context, payload fleet.MDMAppleSetupPayload) error { + if err := svc.authz.Authorize(ctx, payload, fleet.ActionWrite); err != nil { + return err + } + + if err := svc.validateMDMAppleSetupPayload(ctx, payload); err != nil { + return err + } + + if payload.TeamID != nil && *payload.TeamID != 0 { + tm, err := svc.teamByIDOrName(ctx, payload.TeamID, nil) + if err != nil { + return err + } + return svc.updateTeamMDMAppleSetup(ctx, tm, payload) + } + return svc.updateAppConfigMDMAppleSetup(ctx, payload) +} + +func (svc *Service) updateAppConfigMDMAppleSetup(ctx context.Context, payload fleet.MDMAppleSetupPayload) error { + ac, err := svc.AppConfigObfuscated(ctx) + if err != nil { + return err + } + + var didUpdate, didUpdateMacOSEndUserAuth bool + if payload.EnableEndUserAuthentication != nil { + if ac.MDM.MacOSSetup.EnableEndUserAuthentication != *payload.EnableEndUserAuthentication { + ac.MDM.MacOSSetup.EnableEndUserAuthentication = *payload.EnableEndUserAuthentication + didUpdate = true + didUpdateMacOSEndUserAuth = true + } + } + + if didUpdate { + if err := svc.ds.SaveAppConfig(ctx, ac); err != nil { + return err + } + if didUpdateMacOSEndUserAuth { + if err := svc.updateMacOSSetupEnableEndUserAuth(ctx, ac.MDM.MacOSSetup.EnableEndUserAuthentication, nil, nil); err != nil { + return err + } + } + } + return nil +} + +func (svc *Service) updateMacOSSetupEnableEndUserAuth(ctx context.Context, enable bool, teamID *uint, teamName *string) error { + // // TODO: Call Apple Business Manager API to define new enrollment profile (depends on + // // https://github.com/fleetdm/fleet/issues/10995) + // // + // // modify the method signature of the syncer to include a team config pointer + // // and check enable_end_user_authenthication in the team config if not nil + // // otherwise check enable_end_user_authenthication the app config. + // if err := svc.mdmAppleSyncDEPProfiles(ctx); err != nil { + // return err + // } + var act fleet.ActivityDetails + if enable { + act = fleet.ActivityTypeEnabledMacosSetupEndUserAuth{TeamID: teamID, TeamName: teamName} + } else { + act = fleet.ActivityTypeDisabledMacosSetupEndUserAuth{TeamID: teamID, TeamName: teamName} + } + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for macos enable end user auth change") + } + return nil +} + +func (svc *Service) validateMDMAppleSetupPayload(ctx context.Context, payload fleet.MDMAppleSetupPayload) error { + ac, err := svc.AppConfigObfuscated(ctx) + if err != nil { + return err + } + if !ac.MDM.EnabledAndConfigured { + return &fleet.MDMNotConfiguredError{} + } + if payload.EnableEndUserAuthentication != nil && *payload.EnableEndUserAuthentication == true && ac.MDM.EndUserAuthentication.IsEmpty() { + // TODO: update this error message to include steps to resolve the issue once docs for IdP + // config are available + return fleet.NewInvalidArgumentError("enable_end_user_authentication", + `Couldn't enable macos_setup.enable_end_user_authentication because no IdP is configured for MDM features.`) + } + return nil +} + func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name string, pkg io.Reader, teamID uint) error { if err := svc.authz.Authorize(ctx, &fleet.MDMAppleBootstrapPackage{TeamID: teamID}, fleet.ActionWrite); err != nil { return err @@ -505,7 +591,6 @@ func (svc *Service) InitiateMDMAppleSSO(ctx context.Context) (string, error) { } return idpURL, nil - } func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) (string, error) { diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index e628ac4bb5..f5cf88b063 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -110,7 +110,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T return nil, err } - var macOSMinVersionUpdated, macOSDiskEncryptionUpdated bool + var macOSMinVersionUpdated, macOSDiskEncryptionUpdated, macOSEnableEndUserAuthUpdated bool if payload.MDM != nil { if payload.MDM.MacOSUpdates != nil { if err := payload.MDM.MacOSUpdates.Validate(); err != nil { @@ -128,6 +128,21 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T macOSDiskEncryptionUpdated = team.Config.MDM.MacOSSettings.EnableDiskEncryption != payload.MDM.MacOSSettings.EnableDiskEncryption team.Config.MDM.MacOSSettings.EnableDiskEncryption = payload.MDM.MacOSSettings.EnableDiskEncryption } + + if payload.MDM.MacOSSetup != nil { + if !appCfg.MDM.EnabledAndConfigured && team.Config.MDM.MacOSSetup.EnableEndUserAuthentication != payload.MDM.MacOSSetup.EnableEndUserAuthentication { + return nil, 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.`)) + } + macOSEnableEndUserAuthUpdated = team.Config.MDM.MacOSSetup.EnableEndUserAuthentication != payload.MDM.MacOSSetup.EnableEndUserAuthentication + if macOSEnableEndUserAuthUpdated && payload.MDM.MacOSSetup.EnableEndUserAuthentication && appCfg.MDM.EndUserAuthentication.IsEmpty() { + // TODO: update this error message to include steps to resolve the issue once docs for IdP + // config are available + return nil, 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.`)) + } + team.Config.MDM.MacOSSetup.EnableEndUserAuthentication = payload.MDM.MacOSSetup.EnableEndUserAuthentication + } } if payload.Integrations != nil { @@ -195,6 +210,11 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T return nil, ctxerr.Wrap(ctx, err, "create activity for team macos disk encryption") } } + if macOSEnableEndUserAuthUpdated { + if err := svc.updateMacOSSetupEnableEndUserAuth(ctx, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication, &team.ID, &team.Name); err != nil { + return nil, ctxerr.Wrap(ctx, err, "update macos setup enable end user auth") + } + } return team, err } @@ -755,6 +775,22 @@ func (svc *Service) editTeamFromSpec( } } + var didUpdateMacOSEndUserAuth bool + if spec.MDM.MacOSSetup.EnableEndUserAuthentication != oldMacOSSetup.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() { + // 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 + if len(secrets) > 0 { team.Secrets = secrets } @@ -809,6 +845,12 @@ func (svc *Service) editTeamFromSpec( } } + if didUpdateMacOSEndUserAuth { + if err := svc.updateMacOSSetupEnableEndUserAuth(ctx, spec.MDM.MacOSSetup.EnableEndUserAuthentication, &team.ID, &team.Name); err != nil { + return err + } + } + return nil } @@ -889,3 +931,26 @@ func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Te } return nil } + +func (svc *Service) updateTeamMDMAppleSetup(ctx context.Context, tm *fleet.Team, payload fleet.MDMAppleSetupPayload) error { + var didUpdate, didUpdateMacOSEndUserAuth bool + if payload.EnableEndUserAuthentication != nil { + if tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication != *payload.EnableEndUserAuthentication { + tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication = *payload.EnableEndUserAuthentication + didUpdate = true + didUpdateMacOSEndUserAuth = true + } + } + + if didUpdate { + if _, err := svc.ds.SaveTeam(ctx, tm); err != nil { + return err + } + if didUpdateMacOSEndUserAuth { + if err := svc.updateMacOSSetupEnableEndUserAuth(ctx, tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication, &tm.ID, &tm.Name); err != nil { + return err + } + } + } + return nil +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index c8105083e1..aba4086532 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -39,7 +39,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null}, \"macos_updates\": {\"deadline\": \"\", \"minimum_version\": \"\"}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": false}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"apple_bm_enabled_and_configured\": false}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"org_logo_url\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": \"\", \"minimum_version\": \"\"}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": false}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"apple_bm_enabled_and_configured\": false}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"org_logo_url\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `carve_blocks` ( diff --git a/server/fleet/activities.go b/server/fleet/activities.go index d7bb0174f4..6a35cce59b 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -63,6 +63,9 @@ var ActivityDetailsList = []ActivityDetails{ ActivityTypeAddedBootstrapPackage{}, ActivityTypeDeletedBootstrapPackage{}, + + ActivityTypeEnabledMacosSetupEndUserAuth{}, + ActivityTypeDisabledMacosSetupEndUserAuth{}, } type ActivityDetails interface { @@ -939,6 +942,44 @@ func (a ActivityTypeDeletedBootstrapPackage) Documentation() (activity, details, }` } +type ActivityTypeEnabledMacosSetupEndUserAuth struct { + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeEnabledMacosSetupEndUserAuth) ActivityName() string { + return "enabled_macos_setup_end_user_auth" +} + +func (a ActivityTypeEnabledMacosSetupEndUserAuth) Documentation() (activity, details, detailsExample string) { + return `Generated when a user turns on end user authentication for macOS hosts that automatically enroll to a team (or no team).`, + `This activity contains the following fields: +- "team_id": The ID of the team that end user authentication applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that end user authentication applies to, null if it applies to devices that are not in a team.`, `{ + "team_id": 123, + "team_name": "Workstations" +}` +} + +type ActivityTypeDisabledMacosSetupEndUserAuth struct { + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeDisabledMacosSetupEndUserAuth) ActivityName() string { + return "disabled_macos_setup_end_user_auth" +} + +func (a ActivityTypeDisabledMacosSetupEndUserAuth) Documentation() (activity, details, detailsExample string) { + return `Generated when a user turns off end user authentication for macOS hosts that automatically enroll to a team (or no team).`, + `This activity contains the following fields: +- "team_id": The ID of the team that end user authentication applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that end user authentication applies to, null if it applies to devices that are not in a team.`, `{ + "team_id": 123, + "team_name": "Workstations" +}` +} + // LogRoleChangeActivities logs activities for each role change, globally and one for each change in teams. func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, oldGlobalRole *string, oldTeamRoles []UserTeam, user *User) error { if user.GlobalRole != nil && (oldGlobalRole == nil || *oldGlobalRole != *user.GlobalRole) { diff --git a/server/fleet/app.go b/server/fleet/app.go index 43d8858b11..2d8e38e79d 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -258,8 +258,9 @@ func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, erro // MacOSSetup contains settings related to the setup of DEP enrolled devices. type MacOSSetup struct { - BootstrapPackage optjson.String `json:"bootstrap_package"` - MacOSSetupAssistant optjson.String `json:"macos_setup_assistant"` + BootstrapPackage optjson.String `json:"bootstrap_package"` + EnableEndUserAuthentication bool `json:"enable_end_user_authentication"` + MacOSSetupAssistant optjson.String `json:"macos_setup_assistant"` } // MDMEndUserAuthentication contains settings related to end user authentication diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 94b19df395..74a03d6548 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -432,6 +432,18 @@ func (p MDMAppleSettingsPayload) AuthzType() string { return "mdm_apple_settings" } +// MDMAppleSetupPayload describes the payload accepted by the endpoint to +// update specific MDM macos setup values for a team (or no team). +type MDMAppleSetupPayload struct { + TeamID *uint `json:"team_id"` + EnableEndUserAuthentication *bool `json:"enable_end_user_authentication"` +} + +// AuthzType implements authz.AuthzTyper. +func (p MDMAppleSetupPayload) AuthzType() string { + return "mdm_apple_settings" // TODO: add mdm_apple_setup to rego? +} + // NanoEnrollment represents a row in the nano_enrollments table managed by // nanomdm. It is meant to be used internally by the server, not to be returned // as part of endpoints, and as a precaution its json-encoding is explicitly diff --git a/server/fleet/service.go b/server/fleet/service.go index d19cac0f6e..fce79e4aae 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -713,6 +713,10 @@ type Service interface { // Delete the MDM Apple Setup Assistant for the provided team or no team. DeleteMDMAppleSetupAssistant(ctx context.Context, teamID *uint) error + // UpdateMDMAppleSetup updates the specified MDM Apple setup values for a + // specified team or for hosts with no team. + UpdateMDMAppleSetup(ctx context.Context, payload MDMAppleSetupPayload) error + /////////////////////////////////////////////////////////////////////////////// // CronSchedulesService diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index 8800002e9a..0c0bb573b1 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -206,9 +206,12 @@ func (d *DEPService) RegisterProfileWithAppleDEPServer(ctx context.Context, setu // always still set configuration_web_url, otherwise the request method // coming from Apple changes from GET to POST, and we want to preserve // backwards compatibility. - if appCfg.MDM.EndUserAuthentication.SSOProviderSettings.IsEmpty() { - jsonProf.ConfigurationWebURL = enrollURL - } else { + jsonProf.ConfigurationWebURL = enrollURL + if !appCfg.MDM.EndUserAuthentication.SSOProviderSettings.IsEmpty() { + // TODO: modify method signatures for this (and callers as applicable) + // to include a team config pointer and check enable_end_user_authenthication + // in the team config if not nil otherwise check enable_end_user_authenthication + // in the app config. jsonProf.ConfigurationWebURL = appCfg.ServerSettings.ServerURL + "/mdm/sso" } diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 0689a4709c..e49a5b795a 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -403,15 +403,6 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } - mdmSSOSettingsChanged := oldAppConfig.MDM.EndUserAuthentication.SSOProviderSettings != - appConfig.MDM.EndUserAuthentication.SSOProviderSettings - serverURLChanged := oldAppConfig.ServerSettings.ServerURL != appConfig.ServerSettings.ServerURL - if (mdmSSOSettingsChanged || serverURLChanged) && license.Tier == "premium" { - if err := svc.EnterpriseOverrides.MDMAppleSyncDEPProfiles(ctx); err != nil { - return nil, ctxerr.Wrap(ctx, err, "sync DEP profile") - } - } - if oldAppConfig.MDM.MacOSSetup.BootstrapPackage.Value != appConfig.MDM.MacOSSetup.BootstrapPackage.Value && appConfig.MDM.MacOSSetup.BootstrapPackage.Value == "" { // clear bootstrap package for no team - note that we cannot call @@ -480,6 +471,28 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } + mdmEnableEndUserAuthChanged := oldAppConfig.MDM.MacOSSetup.EnableEndUserAuthentication != appConfig.MDM.MacOSSetup.EnableEndUserAuthentication + if mdmEnableEndUserAuthChanged { + var act fleet.ActivityDetails + if appConfig.MDM.MacOSSetup.EnableEndUserAuthentication { + act = fleet.ActivityTypeEnabledMacosSetupEndUserAuth{} + } else { + act = fleet.ActivityTypeDisabledMacosSetupEndUserAuth{} + } + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + return nil, ctxerr.Wrap(ctx, err, "create activity for macos enable end user auth change") + } + } + + mdmSSOSettingsChanged := oldAppConfig.MDM.EndUserAuthentication.SSOProviderSettings != + appConfig.MDM.EndUserAuthentication.SSOProviderSettings + serverURLChanged := oldAppConfig.ServerSettings.ServerURL != appConfig.ServerSettings.ServerURL + if (mdmEnableEndUserAuthChanged || mdmSSOSettingsChanged || serverURLChanged) && license.Tier == "premium" { + if err := svc.EnterpriseOverrides.MDMAppleSyncDEPProfiles(ctx); err != nil { + return nil, ctxerr.Wrap(ctx, err, "sync DEP profile") + } + } + return obfuscatedAppConfig, nil } @@ -499,6 +512,9 @@ func (svc *Service) validateMDM( if 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() { + 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 @@ -522,6 +538,10 @@ func (svc *Service) validateMDM( 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 { + 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.`) + } } if name := mdm.AppleBMDefaultTeam; name != "" && name != oldMdm.AppleBMDefaultTeam { @@ -560,6 +580,16 @@ func (svc *Service) validateMDM( validateSSOProviderSettings(mdm.EndUserAuthentication.SSOProviderSettings, oldMdm.EndUserAuthentication.SSOProviderSettings, invalid) } + + // MacOSSetup validation + if mdm.MacOSSetup.EnableEndUserAuthentication { + if mdm.EndUserAuthentication.IsEmpty() { + // TODO: update this error message to include steps to resolve the issue once docs for IdP + // config are available + 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.`) + } + } } func validateSSOProviderSettings(incoming, existing fleet.SSOProviderSettings, invalid *fleet.InvalidArgumentError) { diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 355931f416..c4b9d3514b 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -1944,6 +1944,41 @@ func (svc *Service) DeleteMDMAppleSetupAssistant(ctx context.Context, teamID *ui return fleet.ErrMissingLicense } +//////////////////////////////////////////////////////////////////////////////// +// Update MDM Apple Setup +//////////////////////////////////////////////////////////////////////////////// + +type updateMDMAppleSetupRequest struct { + fleet.MDMAppleSetupPayload +} + +type updateMDMAppleSetupResponse struct { + Err error `json:"error,omitempty"` +} + +func (r updateMDMAppleSetupResponse) error() error { return r.Err } + +func (r updateMDMAppleSetupResponse) Status() int { return http.StatusNoContent } + +// This endpoint is required because the UI must allow maintainers (in addition +// to admins) to update some MDM Apple settings, while the update config/update +// team endpoints only allow write access to admins. +func updateMDMAppleSetupEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*updateMDMAppleSetupRequest) + if err := svc.UpdateMDMAppleSetup(ctx, req.MDMAppleSetupPayload); err != nil { + return updateMDMAppleSetupResponse{Err: err}, nil + } + return updateMDMAppleSetupResponse{}, nil +} + +func (svc *Service) UpdateMDMAppleSetup(ctx context.Context, payload fleet.MDMAppleSetupPayload) error { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return fleet.ErrMissingLicense +} + //////////////////////////////////////////////////////////////////////////////// // POST /mdm/sso //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index b3bcbd3924..88db591d2d 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -35,7 +35,7 @@ import ( "github.com/stretchr/testify/require" ) -func setupAppleMDMService(t *testing.T) (fleet.Service, context.Context, *mock.Store) { +func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Service, context.Context, *mock.Store) { ds := new(mock.Store) cfg := config.TestConfig() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -66,7 +66,7 @@ func setupAppleMDMService(t *testing.T) (fleet.Service, context.Context, *mock.S MDMStorage: mdmStorage, DEPStorage: depStorage, MDMPusher: pusher, - License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, + License: license, } svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, opts) @@ -175,7 +175,7 @@ func setupAppleMDMService(t *testing.T) (fleet.Service, context.Context, *mock.S } func TestAppleMDMAuthorization(t *testing.T) { - svc, ctx, ds := setupAppleMDMService(t) + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) checkAuthErr := func(t *testing.T, err error, shouldFailWithAuth bool) { t.Helper() @@ -441,7 +441,7 @@ func TestAppleMDMAuthorization(t *testing.T) { } func TestMDMAppleConfigProfileAuthz(t *testing.T) { - svc, ctx, ds := setupAppleMDMService(t) + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) testCases := []struct { name string @@ -615,7 +615,7 @@ func TestMDMAppleConfigProfileAuthz(t *testing.T) { } func TestNewMDMAppleConfigProfile(t *testing.T) { - svc, ctx, ds := setupAppleMDMService(t) + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) mcBytes := mcBytesForTest("Foo", "Bar", "UUID") @@ -665,7 +665,7 @@ func mcBytesForTest(name, identifier, uuid string) []byte { } func TestHostDetailsMDMProfiles(t *testing.T) { - svc, ctx, ds := setupAppleMDMService(t) + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) expected := []fleet.HostMDMAppleProfile{ @@ -808,7 +808,7 @@ func TestHostDetailsMDMProfiles(t *testing.T) { } func TestMDMCommandAuthz(t *testing.T) { - svc, ctx, ds := setupAppleMDMService(t) + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) { switch hostID { @@ -1221,7 +1221,7 @@ func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) { } func TestMDMBatchSetAppleProfiles(t *testing.T) { - svc, ctx, ds := setupAppleMDMService(t) + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { return &fleet.Team{ID: 1, Name: name}, nil @@ -1508,7 +1508,7 @@ func TestMDMBatchSetAppleProfiles(t *testing.T) { } func TestUpdateMDMAppleSettings(t *testing.T) { - svc, ctx, ds := setupAppleMDMService(t) + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { return &fleet.Team{ID: id, Name: "team"}, nil @@ -1661,6 +1661,163 @@ func TestUpdateMDMAppleSettings(t *testing.T) { } } +func TestUpdateMDMAppleSetup(t *testing.T) { + setupTest := func(tier string) (fleet.Service, context.Context, *mock.Store) { + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: tier}) + ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { + return &fleet.Team{ID: id, Name: "team"}, nil + } + ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { + return team, nil + } + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + return nil + } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil + } + ds.SaveAppConfigFunc = func(ctx context.Context, appConfig *fleet.AppConfig) error { + return nil + } + return svc, ctx, ds + } + + type testCase struct { + name string + user *fleet.User + teamID *uint + wantErr string + } + // TODO: Add tests for gitops and observer plus roles? (Settings endpoint test above may also need to be updated) + + t.Run("FreeTier", func(t *testing.T) { + freeTestCases := []testCase{ + { + "global admin", + &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, + nil, + "Requires Fleet Premium license", + }, + { + "global maintainer", + &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, + nil, + "Requires Fleet Premium license", + }, + { + "team id with free license", + &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, + ptr.Uint(1), + "Requires Fleet Premium license", + }, + } + svc, ctx, _ := setupTest(fleet.TierFree) + for _, tt := range freeTestCases { + t.Run(tt.name, func(t *testing.T) { + // prepare the context with the user and license + ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) + err := svc.UpdateMDMAppleSetup(ctx, fleet.MDMAppleSetupPayload{TeamID: tt.teamID}) + if tt.wantErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErr) + }) + } + }) + t.Run("PremiumTier", func(t *testing.T) { + premiumTestCases := []testCase{ + { + "global admin premium", + &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, + nil, + "", + }, + { + "global admin, team", + &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, + ptr.Uint(1), + "", + }, + { + "global maintainer premium", + &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, + nil, + "", + }, + { + "global maintainer, team", + &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, + ptr.Uint(1), + "", + }, + { + "global observer", + &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}, + nil, + authz.ForbiddenErrorMessage, + }, + { + "team admin, DOES belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}, + ptr.Uint(1), + "", + }, + { + "team admin, DOES NOT belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}}, + ptr.Uint(1), + authz.ForbiddenErrorMessage, + }, + { + "team maintainer, DOES belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}}, + ptr.Uint(1), + "", + }, + { + "team maintainer, DOES NOT belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}}, + ptr.Uint(1), + authz.ForbiddenErrorMessage, + }, + { + "team observer, DOES belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}}, + ptr.Uint(1), + authz.ForbiddenErrorMessage, + }, + { + "team observer, DOES NOT belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}}, + ptr.Uint(1), + authz.ForbiddenErrorMessage, + }, + { + "user no roles", + &fleet.User{ID: 1337}, + nil, + authz.ForbiddenErrorMessage, + }, + } + svc, ctx, _ := setupTest(fleet.TierPremium) + for _, tt := range premiumTestCases { + t.Run(tt.name, func(t *testing.T) { + // prepare the context with the user and license + ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) + err := svc.UpdateMDMAppleSetup(ctx, fleet.MDMAppleSetupPayload{TeamID: tt.teamID}) + if tt.wantErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErr) + }) + } + }) +} + func TestMDMAppleCommander(t *testing.T) { ctx := context.Background() mdmStorage := &nanomdm_mock.Storage{} @@ -2244,7 +2401,7 @@ func TestEnsureFleetdConfig(t *testing.T) { } func TestMDMAppleSetupAssistant(t *testing.T) { - svc, ctx, ds := setupAppleMDMService(t) + svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}) ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil diff --git a/server/service/handler.go b/server/service/handler.go index 722df1f7f3..b6864466a1 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -479,6 +479,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC mdm.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/wipe", deviceWipeEndpoint, deviceWipeRequest{}) mdm.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{}) + mdm.PATCH("/api/_version_/fleet/mdm/apple/setup", updateMDMAppleSetupEndpoint, updateMDMAppleSetupRequest{}) mdm.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil) mdm.POST("/api/_version_/fleet/mdm/apple/setup/eula", createMDMAppleEULAEndpoint, createMDMAppleEULARequest{}) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 08856db6d7..3112a70c37 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -3011,6 +3011,267 @@ func (s *integrationMDMTestSuite) TestEULA() { s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/apple/setup/eula/%s", eulaToken), nil, http.StatusNotFound, &deleteResp) } +func (s *integrationMDMTestSuite) TestMDMMacOSSetup() { + t := s.T() + + s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + encoder := json.NewEncoder(w) + switch r.URL.Path { + case "/session": + err := encoder.Encode(map[string]string{"auth_session_token": "xyz"}) + require.NoError(t, err) + case "/profile": + err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"}) + require.NoError(t, err) + default: + _, _ = w.Write([]byte(`{}`)) + } + })) + + // setup test data + var acResp appConfigResponse + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "end_user_authentication": { + "entity_id": "https://localhost:8080", + "issuer_uri": "http://localhost:8080/simplesaml/saml2/idp/SSOService.php", + "idp_name": "SimpleSAML", + "metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php" + } + } + }`), http.StatusOK, &acResp) + require.NotEmpty(t, acResp.MDM.EndUserAuthentication) + + tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"}) + require.NoError(t, err) + + cases := []struct { + raw string + expected bool + }{ + { + raw: `"mdm": {}`, + expected: false, + }, + { + raw: `"mdm": { + "macos_setup": {} + }`, + expected: false, + }, + { + raw: `"mdm": { + "macos_setup": { + "enable_end_user_authentication": true + } + }`, + expected: true, + }, + { + raw: `"mdm": { + "macos_setup": { + "enable_end_user_authentication": false + } + }`, + expected: false, + }, + } + + t.Run("UpdateAppConfig", func(t *testing.T) { + acResp := appConfigResponse{} + path := "/api/latest/fleet/config" + fmtJSON := func(s string) json.RawMessage { + return json.RawMessage(fmt.Sprintf(`{ + %s + }`, s)) + } + + // get the initial appconfig; enable end user authentication default is false + s.DoJSON("GET", path, nil, http.StatusOK, &acResp) + require.False(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication) + + for i, c := range cases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + acResp = appConfigResponse{} + s.DoJSON("PATCH", path, fmtJSON(c.raw), http.StatusOK, &acResp) + require.Equal(t, c.expected, acResp.MDM.MacOSSetup.EnableEndUserAuthentication) + + acResp = appConfigResponse{} + s.DoJSON("GET", path, nil, http.StatusOK, &acResp) + require.Equal(t, c.expected, acResp.MDM.MacOSSetup.EnableEndUserAuthentication) + }) + } + }) + + t.Run("UpdateTeamConfig", func(t *testing.T) { + path := fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID) + fmtJSON := `{ + "name": %q, + %s + }` + + // get the initial team config; enable end user authentication default is false + teamResp := teamResponse{} + s.DoJSON("GET", path, nil, http.StatusOK, &teamResp) + require.False(t, teamResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) + + for i, c := range cases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + teamResp = teamResponse{} + s.DoJSON("PATCH", path, json.RawMessage(fmt.Sprintf(fmtJSON, tm.Name, c.raw)), http.StatusOK, &teamResp) + require.Equal(t, c.expected, teamResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) + + teamResp = teamResponse{} + s.DoJSON("GET", path, nil, http.StatusOK, &teamResp) + require.Equal(t, c.expected, teamResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) + }) + } + }) + + t.Run("TestMDMAppleSetupEndpoint", func(t *testing.T) { + t.Run("TestNoTeam", func(t *testing.T) { + var acResp appConfigResponse + s.Do("PATCH", "/api/latest/fleet/mdm/apple/setup", + fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusNoContent) + acResp = appConfigResponse{} + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.True(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication) + lastActivityID := s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosSetupEndUserAuth{}.ActivityName(), + `{"team_id": null, "team_name": null}`, 0) + + s.Do("PATCH", "/api/latest/fleet/mdm/apple/setup", + fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusNoContent) + acResp = appConfigResponse{} + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.True(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication) + s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosSetupEndUserAuth{}.ActivityName(), + ``, lastActivityID) // no new activity + + s.Do("PATCH", "/api/latest/fleet/mdm/apple/setup", + fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(false)}, http.StatusNoContent) + acResp = appConfigResponse{} + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.False(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication) + require.Greater(t, s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledMacosSetupEndUserAuth{}.ActivityName(), + `{"team_id": null, "team_name": null}`, 0), lastActivityID) + }) + + t.Run("TestTeam", func(t *testing.T) { + tmConfigPath := fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID) + expectedActivityDetail := fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name) + var tmResp teamResponse + s.Do("PATCH", "/api/latest/fleet/mdm/apple/setup", + fleet.MDMAppleSetupPayload{TeamID: &tm.ID, EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusNoContent) + tmResp = teamResponse{} + s.DoJSON("GET", tmConfigPath, nil, http.StatusOK, &tmResp) + require.True(t, tmResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) + lastActivityID := s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosSetupEndUserAuth{}.ActivityName(), + expectedActivityDetail, 0) + + s.Do("PATCH", "/api/latest/fleet/mdm/apple/setup", + fleet.MDMAppleSetupPayload{TeamID: &tm.ID, EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusNoContent) + tmResp = teamResponse{} + s.DoJSON("GET", tmConfigPath, nil, http.StatusOK, &tmResp) + require.True(t, tmResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) + s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosSetupEndUserAuth{}.ActivityName(), + ``, lastActivityID) // no new activity + + s.Do("PATCH", "/api/latest/fleet/mdm/apple/setup", + fleet.MDMAppleSetupPayload{TeamID: &tm.ID, EnableEndUserAuthentication: ptr.Bool(false)}, http.StatusNoContent) + tmResp = teamResponse{} + s.DoJSON("GET", tmConfigPath, nil, http.StatusOK, &tmResp) + require.False(t, tmResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) + require.Greater(t, s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledMacosSetupEndUserAuth{}.ActivityName(), + expectedActivityDetail, 0), lastActivityID) + }) + }) + + t.Run("ValidateEnableEndUserAuthentication", func(t *testing.T) { + // ensure the test is setup correctly + var acResp appConfigResponse + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "end_user_authentication": { + "entity_id": "https://localhost:8080", + "issuer_uri": "http://localhost:8080/simplesaml/saml2/idp/SSOService.php", + "idp_name": "SimpleSAML", + "metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php" + }, + "macos_setup": { + "enable_end_user_authentication": true + } + } + }`), http.StatusOK, &acResp) + require.NotEmpty(t, acResp.MDM.EndUserAuthentication) + + // ok to disable end user authentication without a configured IdP + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "end_user_authentication": { + "entity_id": "", + "issuer_uri": "", + "idp_name": "", + "metadata_url": "" + }, + "macos_setup": { + "enable_end_user_authentication": false + } + } + }`), http.StatusOK, &acResp) + require.Equal(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication, false) + require.True(t, acResp.MDM.EndUserAuthentication.IsEmpty()) + + // can't enable end user authentication without a configured IdP + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "end_user_authentication": { + "entity_id": "", + "issuer_uri": "", + "idp_name": "", + "metadata_url": "" + }, + "macos_setup": { + "enable_end_user_authentication": true + } + } + }`), http.StatusUnprocessableEntity, &acResp) + + // can't use setup endpoint to enable end user authentication on no team without a configured IdP + s.Do("PATCH", "/api/latest/fleet/mdm/apple/setup", + fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusUnprocessableEntity) + + // can't enable end user authentication on team config without a configured IdP already on app config + var teamResp teamResponse + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID), json.RawMessage(fmt.Sprintf(`{ + "name": %q, + "mdm": { + "macos_setup": { + "enable_end_user_authentication": true + } + } + }`, tm.Name)), http.StatusUnprocessableEntity, &teamResp) + + // can't use setup endpoint to enable end user authentication on team without a configured IdP + s.Do("PATCH", "/api/latest/fleet/mdm/apple/setup", + fleet.MDMAppleSetupPayload{TeamID: &tm.ID, EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusUnprocessableEntity) + + // ensure IdP is empty for the rest of the tests + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "end_user_authentication": { + "entity_id": "", + "issuer_uri": "", + "idp_name": "", + "metadata_url": "" + } + } + }`), http.StatusOK, &acResp) + require.Empty(t, acResp.MDM.EndUserAuthentication) + }) +} + func (s *integrationMDMTestSuite) TestMacosSetupAssistant() { ctx := context.Background() t := s.T()