From eea90e5632e081901ccac70a88e183a9aa8d21d3 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 27 Nov 2024 12:11:08 -0500 Subject: [PATCH] Proposal fix/plan for 24024 (#24207) --- .../24024-bypass-setup-experience-if-empty | 2 + server/mdm/lifecycle/lifecycle.go | 17 ++-- server/service/apple_mdm.go | 13 +-- server/service/integration_mdm_dep_test.go | 83 +++++++++++++++++-- server/service/integration_mdm_test.go | 8 ++ server/service/orbit.go | 16 ++-- server/worker/apple_mdm.go | 45 +++++----- server/worker/apple_mdm_test.go | 58 ++++++++++--- 8 files changed, 181 insertions(+), 61 deletions(-) create mode 100644 changes/24024-bypass-setup-experience-if-empty diff --git a/changes/24024-bypass-setup-experience-if-empty b/changes/24024-bypass-setup-experience-if-empty new file mode 100644 index 0000000000..319df88c1c --- /dev/null +++ b/changes/24024-bypass-setup-experience-if-empty @@ -0,0 +1,2 @@ +* Bypass the setup experience UI if there is no setup experience item to process (no software to install, no script to execute), so that releasing the device is done without going through that window. +* Fixed releasing a DEP-enrolled macOS device if mTLS is configured for `fleetd`. diff --git a/server/mdm/lifecycle/lifecycle.go b/server/mdm/lifecycle/lifecycle.go index 33658a2367..fd96454274 100644 --- a/server/mdm/lifecycle/lifecycle.go +++ b/server/mdm/lifecycle/lifecycle.go @@ -32,13 +32,14 @@ const ( // Not all options are required for all actions, each individual action should // validate that it receives the required information. type HostOptions struct { - Action HostAction - Platform string - UUID string - HardwareSerial string - HardwareModel string - EnrollReference string - Host *fleet.Host + Action HostAction + Platform string + UUID string + HardwareSerial string + HardwareModel string + EnrollReference string + Host *fleet.Host + HasSetupExperienceItems bool } // HostLifecycle manages MDM host lifecycle actions @@ -174,6 +175,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro opts.Platform, tmID, opts.EnrollReference, + !opts.HasSetupExperienceItems, ) return ctxerr.Wrap(ctx, err, "queue DEP post-enroll task") } @@ -189,6 +191,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro opts.Platform, tmID, opts.EnrollReference, + false, ); err != nil { return ctxerr.Wrap(ctx, err, "queue manual post-enroll task") } diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 04ea557deb..984e4df684 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2778,20 +2778,21 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm. return ctxerr.Wrap(r.Context, err, "cleaning SCEP refs") } + var hasSetupExpItems bool if m.AwaitingConfiguration { // Enqueue setup experience items and mark the host as being in setup experience - _, err := svc.ds.EnqueueSetupExperienceItems(r.Context, r.ID, info.TeamID) + hasSetupExpItems, err = svc.ds.EnqueueSetupExperienceItems(r.Context, r.ID, info.TeamID) if err != nil { return ctxerr.Wrap(r.Context, err, "queueing setup experience tasks") } - } return svc.mdmLifecycle.Do(r.Context, mdmlifecycle.HostOptions{ - Action: mdmlifecycle.HostActionTurnOn, - Platform: info.Platform, - UUID: r.ID, - EnrollReference: r.Params[mobileconfig.FleetEnrollReferenceKey], + Action: mdmlifecycle.HostActionTurnOn, + Platform: info.Platform, + UUID: r.ID, + EnrollReference: r.Params[mobileconfig.FleetEnrollReferenceKey], + HasSetupExperienceItems: hasSetupExpItems, }) } diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 249dddb185..bf46168b94 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -121,12 +121,33 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceGlobal() { s.enableABM("fleet_ade_test") + // add a setup experience script to run for no team + extraArgs := make(map[string][]string) + body, headers := generateNewScriptMultipartRequest(t, + "script.sh", []byte(`echo "hello"`), s.token, extraArgs) + s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers) + + // test manual and automatic release with the new setup experience flow + for _, enableReleaseManually := range []bool{false, true} { + t.Run(fmt.Sprintf("enableReleaseManually=%t;new_flow", enableReleaseManually), func(t *testing.T) { + s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false) + }) + } // test manual and automatic release with the old worker flow for _, enableReleaseManually := range []bool{false, true} { - t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) { + t.Run(fmt.Sprintf("enableReleaseManually=%t;old_flow", enableReleaseManually), func(t *testing.T) { s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", true) }) } + + // remove the setup experience script, run the new setup experience flow when + // there is no setup experience item to process (so it is bypassed) + s.Do("DELETE", "/api/latest/fleet/setup_experience/script", nil, http.StatusOK) + for _, enableReleaseManually := range []bool{false, true} { + t.Run(fmt.Sprintf("enableReleaseManually=%t;bypass_flow", enableReleaseManually), func(t *testing.T) { + s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false) + }) + } } func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() { @@ -211,12 +232,35 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() { // enable FileVault s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings", json.RawMessage([]byte(fmt.Sprintf(`{"enable_disk_encryption":true,"team_id":%d}`, tm.ID))), http.StatusNoContent) + // add a setup experience script to run for this team + extraArgs := map[string][]string{ + "team_id": {fmt.Sprintf("%d", tm.ID)}, + } + body, headers := generateNewScriptMultipartRequest(t, + "script.sh", []byte(`echo "hello"`), s.token, extraArgs) + s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers) + + // test manual and automatic release with the new setup experience flow + for _, enableReleaseManually := range []bool{false, true} { + t.Run(fmt.Sprintf("enableReleaseManually=%t;new_flow", enableReleaseManually), func(t *testing.T) { + s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false) + }) + } // test manual and automatic release with the old worker flow for _, enableReleaseManually := range []bool{false, true} { - t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) { + t.Run(fmt.Sprintf("enableReleaseManually=%t;old_flow", enableReleaseManually), func(t *testing.T) { s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", true) }) } + + // remove the setup experience script, run the new setup experience flow when + // there is no setup experience item to process (so it is bypassed) + s.Do("DELETE", "/api/latest/fleet/setup_experience/script", nil, http.StatusOK, "team_id", fmt.Sprint(tm.ID)) + for _, enableReleaseManually := range []bool{false, true} { + t.Run(fmt.Sprintf("enableReleaseManually=%t;bypass_flow", enableReleaseManually), func(t *testing.T) { + s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false) + }) + } } func (s *integrationMDMTestSuite) TestDEPEnrollReleaseIphoneTeam() { @@ -286,6 +330,11 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseIphoneTeam() { func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, device godep.Device, enableReleaseManually bool, teamID *uint, customProfileIdent string, useOldFleetdFlow bool) { ctx := context.Background() + var isIphone bool + if device.DeviceFamily == "iPhone" { + isIphone = true + } + // set the enable release device manually option payload := map[string]any{ "enable_release_device_manually": enableReleaseManually, @@ -359,15 +408,22 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de // enroll the host depURLToken := loadEnrollmentProfileDEPToken(t, s.ds) mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken) - var isIphone bool - if device.DeviceFamily == "iPhone" { + if isIphone { mdmDevice.Model = "iPhone 14,6" - isIphone = true } mdmDevice.SerialNumber = device.SerialNumber err := mdmDevice.Enroll() require.NoError(t, err) + // check if it has setup experience items or not + hasSetupExpItems := true + _, err = s.ds.GetHostAwaitingConfiguration(ctx, mdmDevice.UUID) + if fleet.IsNotFound(err) { + hasSetupExpItems = false + } else if err != nil { + require.NoError(t, err) + } + // run the worker to process the DEP enroll request s.runWorker() // run the cron to assign configuration profiles @@ -525,8 +581,13 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de b, err := io.ReadAll(res.Body) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, &orbitConfigResp)) - // should be notified of the setup experience flow - require.False(t, orbitConfigResp.Notifications.RunSetupExperience) + if hasSetupExpItems { + // should be notified of the setup experience flow + require.True(t, orbitConfigResp.Notifications.RunSetupExperience) + } else { + // should bypass the setup experience flow + require.False(t, orbitConfigResp.Notifications.RunSetupExperience) + } if enableReleaseManually { // get the worker's pending job from the future, there should not be any @@ -537,7 +598,7 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de return } - if useOldFleetdFlow { + if useOldFleetdFlow || !hasSetupExpItems { // there should be a Release Device pending job pending, err := s.ds.GetQueuedJobs(ctx, 2, time.Now().UTC().Add(time.Minute)) require.NoError(t, err) @@ -574,6 +635,12 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de require.NoError(t, err) require.Len(t, pending, 0) + // mark the setup experience script as done + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `UPDATE setup_experience_status_results SET status = 'success' WHERE host_uuid = ?`, mdmDevice.UUID) + return err + }) + // call the /status endpoint to automatically release the host var statusResp getOrbitSetupExperienceStatusResponse s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index d8d2f6390d..a32b66b742 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -677,6 +677,14 @@ func (s *integrationMDMTestSuite) TearDownTest() { _, err := tx.ExecContext(ctx, "DELETE FROM vpp_tokens;") return err }) + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_status_results;") + return err + }) + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_scripts;") + return err + }) } func (s *integrationMDMTestSuite) mockDEPResponse(orgName string, handler http.Handler) { diff --git a/server/service/orbit.go b/server/service/orbit.go index df75ce21c5..e8e622a327 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -249,15 +249,13 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro notifs.RunSetupExperience = true } - if inSetupAssistant || fleet.IsNotFound(err) { - // If the client is running a fleetd that doesn't support setup experience, or if no - // software/script has been configured for setup experience, then we should fall back to - // the "old way" of releasing the device. We do an additional check for - // !inSetupAssistant to prevent enqueuing a new job every time the /config - // endpoint is hit. + if inSetupAssistant { + // If the client is running a fleetd that doesn't support setup + // experience, then we should fall back to the "old way" of releasing + // the device. mp, ok := capabilities.FromContext(ctx) - if !ok || !mp.Has(fleet.CapabilitySetupExperience) || !inSetupAssistant { - level.Debug(svc.logger).Log("msg", "host doesn't support setup experience or no setup experience configured, falling back to worker-based device release", "host_uuid", host.UUID) + if !ok || !mp.Has(fleet.CapabilitySetupExperience) { + level.Debug(svc.logger).Log("msg", "host doesn't support setup experience, falling back to worker-based device release", "host_uuid", host.UUID) if err := svc.processReleaseDeviceForOldFleetd(ctx, host); err != nil { return fleet.OrbitConfig{}, err } @@ -521,7 +519,7 @@ func (svc *Service) processReleaseDeviceForOldFleetd(ctx context.Context, host * // Enroll reference arg is not used in the release device task, passing empty string. if err := worker.QueueAppleMDMJob(ctx, svc.ds, svc.logger, worker.AppleMDMPostDEPReleaseDeviceTask, - host.UUID, host.Platform, host.TeamID, "", bootstrapCmdUUID, acctConfigCmdUUID); err != nil { + host.UUID, host.Platform, host.TeamID, "", false, bootstrapCmdUUID, acctConfigCmdUUID); err != nil { return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job") } } diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go index 01ac59ea79..235a3a7333 100644 --- a/server/worker/apple_mdm.go +++ b/server/worker/apple_mdm.go @@ -50,12 +50,13 @@ func (a *AppleMDM) Name() string { // appleMDMArgs is the payload for the Apple MDM job. type appleMDMArgs struct { - Task AppleMDMTask `json:"task"` - HostUUID string `json:"host_uuid"` - TeamID *uint `json:"team_id,omitempty"` - EnrollReference string `json:"enroll_reference,omitempty"` - EnrollmentCommands []string `json:"enrollment_commands,omitempty"` - Platform string `json:"platform,omitempty"` + Task AppleMDMTask `json:"task"` + HostUUID string `json:"host_uuid"` + TeamID *uint `json:"team_id,omitempty"` + EnrollReference string `json:"enroll_reference,omitempty"` + EnrollmentCommands []string `json:"enrollment_commands,omitempty"` + Platform string `json:"platform,omitempty"` + UseWorkerDeviceRelease bool `json:"use_worker_device_release,omitempty"` } // Run executes the apple_mdm job. @@ -163,9 +164,10 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) } } - // proceed to release the device only if it is not a macos, as those are - // released via the setup experience flow. - if !isMacOS(args.Platform) { + // proceed to release the device if it is not a macos, as those are released + // via the setup experience flow, or if we were told to use the worker based + // release. + if !isMacOS(args.Platform) || args.UseWorkerDeviceRelease { var manualRelease bool if args.TeamID == nil { ac, err := a.Datastore.AppConfig(ctx) @@ -187,7 +189,7 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) // be final and same for MDM profiles of that host; it means the DEP // enrollment process is done and the device can be released. if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask, - args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil { + args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, false, awaitCmdUUIDs...); err != nil { return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job") } } @@ -198,10 +200,11 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) // This job is deprecated for macos because releasing devices is now done via // the orbit endpoint /setup_experience/status that is polled by a swift dialog -// UI window during the setup process, and automatically releases the device -// once all pending setup tasks are done. However, it must remain implemented -// for iOS and iPadOS and in case there are such jobs to process after a Fleet -// migration to a new version. +// UI window during the setup process (unless there are no setup experience +// items, in which case this worker job is used), and automatically releases +// the device once all pending setup tasks are done. However, it must remain +// implemented for iOS and iPadOS and in case there are such jobs to process +// after a Fleet migration to a new version. func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArgs) error { // Edge cases: // - if the device goes offline for a long time, should we go ahead and @@ -355,6 +358,7 @@ func QueueAppleMDMJob( platform string, teamID *uint, enrollReference string, + useWorkerDeviceRelease bool, enrollmentCommandUUIDs ...string, ) error { attrs := []interface{}{ @@ -373,12 +377,13 @@ func QueueAppleMDMJob( level.Info(logger).Log(attrs...) args := &appleMDMArgs{ - Task: task, - HostUUID: hostUUID, - TeamID: teamID, - EnrollReference: enrollReference, - EnrollmentCommands: enrollmentCommandUUIDs, - Platform: platform, + Task: task, + HostUUID: hostUUID, + TeamID: teamID, + EnrollReference: enrollReference, + EnrollmentCommands: enrollmentCommandUUIDs, + Platform: platform, + UseWorkerDeviceRelease: useWorkerDeviceRelease, } // the release device task is always added with a delay diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go index 8b497379ab..f27aa32bf1 100644 --- a/server/worker/apple_mdm_test.go +++ b/server/worker/apple_mdm_test.go @@ -141,7 +141,7 @@ func TestAppleMDM(t *testing.T) { // create a host and enqueue the job h := createEnrolledHost(t, 1, nil, true) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should mark the job as done @@ -171,7 +171,7 @@ func TestAppleMDM(t *testing.T) { // create a host and enqueue the job h := createEnrolledHost(t, 1, nil, true) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should mark the job as failed @@ -204,7 +204,7 @@ func TestAppleMDM(t *testing.T) { w.Register(mdmWorker) // use "" instead of "darwin" as platform to test a queued job after the upgrade to iOS/iPadOS support. - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "", false) require.NoError(t, err) // run the worker, should succeed @@ -239,7 +239,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should succeed @@ -281,7 +281,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should succeed @@ -330,7 +330,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false) require.NoError(t, err) // run the worker, should succeed @@ -380,7 +380,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false) require.NoError(t, err) // run the worker, should succeed @@ -418,7 +418,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd", false) require.NoError(t, err) // run the worker, should succeed @@ -461,7 +461,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID) + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID, false) require.NoError(t, err) // run the worker, should succeed @@ -514,7 +514,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID) + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID, false) require.NoError(t, err) // run the worker, should succeed @@ -548,7 +548,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should succeed @@ -564,4 +564,40 @@ func TestAppleMDM(t *testing.T) { require.Empty(t, jobs) require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t)) }) + + t.Run("use worker for automatic release", func(t *testing.T) { + mysql.SetTestABMAssets(t, ds, testOrgName) + defer mysql.TruncateTables(t, ds) + + h := createEnrolledHost(t, 1, nil, true) + + mdmWorker := &AppleMDM{ + Datastore: ds, + Log: nopLog, + Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}), + } + w := NewWorker(ds, nopLog) + w.Register(mdmWorker) + + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true) + require.NoError(t, err) + + // run the worker, should succeed + err = w.ProcessJobs(ctx) + require.NoError(t, err) + + // ensure the job's not_before allows it to be returned if it were to run + // again + time.Sleep(time.Second) + + require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t)) + + // the release device job got enqueued + jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().Add(time.Minute)) // release job is always added with a delay + require.NoError(t, err) + require.Len(t, jobs, 1) + require.Equal(t, fleet.JobStateQueued, jobs[0].State) + require.Equal(t, appleMDMJobName, jobs[0].Name) + require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask) + }) }