diff --git a/ee/server/service/service.go b/ee/server/service/service.go index fb66f21136..e5a28036ef 100644 --- a/ee/server/service/service.go +++ b/ee/server/service/service.go @@ -86,6 +86,7 @@ func NewService( MDMWindowsEnableOSUpdates: eeservice.mdmWindowsEnableOSUpdates, MDMWindowsDisableOSUpdates: eeservice.mdmWindowsDisableOSUpdates, MDMAppleEditedAppleOSUpdates: eeservice.mdmAppleEditedAppleOSUpdates, + SetupExperienceNextStep: eeservice.SetupExperienceNextStep, }) return eeservice, nil diff --git a/server/fleet/service.go b/server/fleet/service.go index c25391173d..e7f70e8072 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -35,6 +35,7 @@ type EnterpriseOverrides struct { MDMWindowsEnableOSUpdates func(ctx context.Context, teamID *uint, updates WindowsUpdates) error MDMWindowsDisableOSUpdates func(ctx context.Context, teamID *uint) error MDMAppleEditedAppleOSUpdates func(ctx context.Context, teamID *uint, appleDevice AppleDevice, updates AppleOSUpdateSettings) error + SetupExperienceNextStep func(ctx context.Context, hostUUID string) (bool, error) } type OsqueryService interface { diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 34a0b3ac93..aee886c1e0 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -1478,6 +1478,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu // simulate fleetd being installed and the host being orbit-enrolled now enrolledHost.OsqueryHostID = ptr.String(mdmDevice.UUID) + enrolledHost.UUID = mdmDevice.UUID orbitKey := setOrbitEnrollment(t, enrolledHost, s.ds) enrolledHost.OrbitNodeKey = &orbitKey @@ -1516,74 +1517,127 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu require.NoError(t, err) require.Nil(t, cmd) - // TODO(mna): here we should call the "state machine" to trigger creation of - // the software install and script execution requests, but that is not - // implemented yet. + statusResp = getOrbitSetupExperienceStatusResponse{} + s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) + // Software is now running, script is still pending + require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) + require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Software[0].Status) + require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID) + require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID) - // TODO(mna): when callback of software/script results are implemented, this will - // automatically update the /status responses, and once everything has run it will - // automatically release the device. + require.NotNil(t, statusResp.Results.Script) + require.Equal(t, "script.sh", statusResp.Results.Script.Name) + require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status) + + // The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull + // it out manually + results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID) + require.Len(t, results, 2) + require.NoError(t, err) + var installUUID string + for _, r := range results { + if r.HostSoftwareInstallsExecutionID != nil { + installUUID = *r.HostSoftwareInstallsExecutionID + } + } + + require.NotEmpty(t, installUUID) // record a result for software installation - /* - var installResp orbitPostSoftwareInstallResultResponse - s.DoJSON("POST", "/api/fleet/orbit/software_install/result", - json.RawMessage(fmt.Sprintf(`{ + s.Do("POST", "/api/fleet/orbit/software_install/result", + json.RawMessage(fmt.Sprintf(`{ "orbit_node_key": %q, "install_uuid": %q, "install_script_exit_code": 0, "install_script_output": "ok" - }`, *enrolledHost.OrbitNodeKey, installUUID)), http.StatusOK, &installResp) + }`, *enrolledHost.OrbitNodeKey, installUUID)), http.StatusNoContent) - // status still shows script as pending - statusResp = getOrbitSetupExperienceStatusResponse{} - s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) - require.Nil(t, statusResp.Results.BootstrapPackage) // no bootstrap package involved - require.Nil(t, statusResp.Results.AccountConfiguration) // no SSO involved - require.Len(t, statusResp.Results.ConfigurationProfiles, 3) // fleetd config, root CA, custom profile - require.NotNil(t, statusResp.Results.Script) - require.Equal(t, "script.sh", statusResp.Results.Script.Name) - require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status) - require.Len(t, statusResp.Results.Software, 1) - require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) - require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status) + // status still shows script as pending + statusResp = getOrbitSetupExperienceStatusResponse{} + s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) + require.Nil(t, statusResp.Results.BootstrapPackage) // no bootstrap package involved + require.Nil(t, statusResp.Results.AccountConfiguration) // no SSO involved + require.Len(t, statusResp.Results.ConfigurationProfiles, 3) // fleetd config, root CA, custom profile + require.NotNil(t, statusResp.Results.Script) + require.Equal(t, "script.sh", statusResp.Results.Script.Name) + require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status) + require.Len(t, statusResp.Results.Software, 1) + require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) + require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status) - // no MDM command got enqueued due to the /status call (device not released yet) - cmd, err = mdmDevice.Idle() - require.NoError(t, err) - require.Nil(t, cmd) + // no MDM command got enqueued due to the /status call (device not released yet) + cmd, err = mdmDevice.Idle() + require.NoError(t, err) + require.Nil(t, cmd) - // record a result for script execution - var scriptResp orbitPostScriptResultResponse - s.DoJSON("POST", "/api/fleet/orbit/scripts/result", - json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *enrolledHost.OrbitNodeKey, execID)), - http.StatusOK, &scriptResp) + // Software is installed, now we should run the script + statusResp = getOrbitSetupExperienceStatusResponse{} + s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) + // Software is now running, script is still pending + require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) + require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status) + require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID) + require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID) - // check that the host received the device configured command automatically - cmd, err = mdmDevice.Idle() - require.NoError(t, err) - cmds = cmds[:0] - for cmd != nil { - var fullCmd micromdm.CommandPayload - require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) - cmds = append(cmds, &fullCmd) - cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) - require.NoError(t, err) + require.NotNil(t, statusResp.Results.Script) + require.Equal(t, "script.sh", statusResp.Results.Script.Name) + require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Script.Status) + + // Get script exec ID + results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID) + require.Len(t, results, 2) + require.NoError(t, err) + var execID string + for _, r := range results { + if r.ScriptExecutionID != nil { + execID = *r.ScriptExecutionID } + } - require.Len(t, cmds, 1) - var deviceConfiguredCount int - for _, cmd := range cmds { - switch cmd.Command.RequestType { - case "DeviceConfigured": - deviceConfiguredCount++ - default: - otherCount++ - } + // record a result for script execution + var scriptResp orbitPostScriptResultResponse + s.DoJSON("POST", "/api/fleet/orbit/scripts/result", + json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *enrolledHost.OrbitNodeKey, execID)), + http.StatusOK, &scriptResp) + + // Get status again, now the script should be complete. This should also trigger the automatic + // release of the device, as all setup experience steps are now complete. + statusResp = getOrbitSetupExperienceStatusResponse{} + s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) + // Software is now running, script is still pending + require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) + require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status) + require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID) + require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID) + + require.NotNil(t, statusResp.Results.Script) + require.Equal(t, "script.sh", statusResp.Results.Script.Name) + require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Script.Status) + + // check that the host received the device configured command automatically + cmd, err = mdmDevice.Idle() + require.NoError(t, err) + cmds = cmds[:0] + for cmd != nil { + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + cmds = append(cmds, &fullCmd) + cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) + require.NoError(t, err) + } + + require.Len(t, cmds, 1) + var deviceConfiguredCount int + for _, cmd := range cmds { + switch cmd.Command.RequestType { + case "DeviceConfigured": + deviceConfiguredCount++ + default: + otherCount++ } - require.Equal(t, 1, deviceConfiguredCount) - require.Equal(t, 0, otherCount) - */ + } + require.Equal(t, 1, deviceConfiguredCount) + require.Equal(t, 0, otherCount) } func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptForceRelease() { diff --git a/server/service/orbit.go b/server/service/orbit.go index 140700f467..f42a22e7c2 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -712,7 +712,7 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host return ctxerr.Wrap(ctx, err, "update setup experience status") } else if updated { level.Debug(svc.logger).Log("msg", "setup experience script result updated", "host_uuid", host.UUID, "execution_id", result.ExecutionID) - _, err := svc.SetupExperienceNextStep(ctx, host.UUID) + _, err := svc.EnterpriseOverrides.SetupExperienceNextStep(ctx, host.UUID) if err != nil { return ctxerr.Wrap(ctx, err, "getting next step for host setup experience") }