Allow end user authentication during automatic MDM enrollment to be enabled on a per-team basis (#11566)

This commit is contained in:
gillespi314
2023-05-10 15:22:08 -05:00
committed by GitHub
parent d367b8862a
commit a9584dc32f
29 changed files with 922 additions and 37 deletions
@@ -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.
+109
View File
@@ -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) {
@@ -92,6 +92,7 @@
},
"macos_setup": {
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null
},
"end_user_authentication": {
@@ -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: ""
@@ -50,6 +50,7 @@
},
"macos_setup": {
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null
},
"end_user_authentication": {
@@ -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: ""
+2
View File
@@ -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
}
},
+2
View File
@@ -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
@@ -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: ""
@@ -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: ""
@@ -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: ""
@@ -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: ""
@@ -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: ""
+34
View File
@@ -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"
}
```
<meta name="pageOrderInSection" value="1400">
+2
View File
@@ -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)
+39 -10
View File
@@ -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_
| &nbsp;&nbsp;&nbsp;&nbsp;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. |
| &nbsp;&nbsp;macos_settings | object | body | MacOS-specific settings. |
| &nbsp;&nbsp;&nbsp;&nbsp;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. |
| &nbsp;&nbsp;macos_setup | object | body | Setup for automatic MDM enrollment of macOS devices. |
| &nbsp;&nbsp;&nbsp;&nbsp;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"
}
}
+86 -1
View File
@@ -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) {
+66 -1
View File
@@ -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
}
+1 -1
View File
@@ -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` (
+41
View File
@@ -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) {
+3 -2
View File
@@ -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
+12
View File
@@ -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
+4
View File
@@ -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
+6 -3
View File
@@ -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"
}
+39 -9
View File
@@ -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) {
+35
View File
@@ -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
////////////////////////////////////////////////////////////////////////////////
+167 -10
View File
@@ -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
+1
View File
@@ -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{})
+261
View File
@@ -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()