From a1e5c500c05b1a3349a385f8935bd2e1f03ae63e Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Thu, 29 Jan 2026 15:31:28 -0500 Subject: [PATCH] Update server-proto version to 9, implement THROTTLED w/ 24h cooldown (#38920) **Related issue:** Resolves #37072 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually --- changes/37072-dep-sync | 1 + server/datastore/mysql/apple_mdm.go | 27 ++++-- server/datastore/mysql/apple_mdm_test.go | 4 +- server/datastore/mysql/hosts.go | 2 +- server/fleet/apple_mdm.go | 9 +- server/fleet/hosts_test.go | 9 ++ server/mdm/apple/apple_mdm.go | 2 +- server/mdm/apple/apple_mdm_external_test.go | 100 ++++++++++++++++++++ server/mdm/nanodep/client/transport.go | 2 +- server/service/devices_test.go | 19 ++++ server/service/integration_mdm_dep_test.go | 66 ++++++++++++- server/service/integration_mdm_test.go | 16 ++++ 12 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 changes/37072-dep-sync diff --git a/changes/37072-dep-sync b/changes/37072-dep-sync new file mode 100644 index 0000000000..c8cf7365af --- /dev/null +++ b/changes/37072-dep-sync @@ -0,0 +1 @@ +* Updated DEP syncing code to use server-protocol-version 9 and handle THROTTLED responses diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 302ce6851c..259e7a73be 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -4633,6 +4633,7 @@ func (ds *Datastore) updateHostDEPAssignProfileResponses(ctx context.Context, pa var ( notAccessible []string failed []string + throttled []string ) for serial, status := range payload.Devices { @@ -4643,6 +4644,8 @@ func (ds *Datastore) updateHostDEPAssignProfileResponses(ctx context.Context, pa notAccessible = append(notAccessible, serial) case string(fleet.DEPAssignProfileResponseFailed): failed = append(failed, serial) + case string(fleet.DEPAssignProfileResponseThrottled): + throttled = append(throttled, serial) default: // this should never happen unless Apple changes the response format, so we log it for // future debugging @@ -4663,6 +4666,10 @@ func (ds *Datastore) updateHostDEPAssignProfileResponses(ctx context.Context, pa string(fleet.DEPAssignProfileResponseFailed), abmTokenID); err != nil { return err } + if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, throttled, + string(fleet.DEPAssignProfileResponseThrottled), abmTokenID); err != nil { + return err + } return nil }) } @@ -4714,8 +4721,11 @@ WHERE return nil } -// depCooldownPeriod is the waiting period following a failed DEP assign profile request for a host. -const depCooldownPeriod = 1 * time.Hour // TODO: Make this a test config option? +// depFailedCooldownPeriod is the waiting period following a failed DEP assign profile request for a host. +const ( + depFailedCooldownPeriod = 1 * time.Hour // TODO: Make this a test config option? + depThrottledCooldownPeriod = 24 * time.Hour +) func (ds *Datastore) ScreenDEPAssignProfileSerialsForCooldown(ctx context.Context, serials []string) (skipSerialsByOrgName map[string][]string, serialsByOrgName map[string][]string, err error) { if len(serials) == 0 { @@ -4724,7 +4734,8 @@ func (ds *Datastore) ScreenDEPAssignProfileSerialsForCooldown(ctx context.Contex stmt := ` SELECT - CASE WHEN assign_profile_response = ? AND (response_updated_at > DATE_SUB(NOW(), INTERVAL ? SECOND) OR retry_job_id != 0) THEN + CASE WHEN (assign_profile_response = ? AND (response_updated_at > DATE_SUB(NOW(), INTERVAL ? SECOND) OR retry_job_id != 0)) OR + (assign_profile_response = ? AND (response_updated_at > DATE_SUB(NOW(), INTERVAL ? SECOND) OR retry_job_id != 0)) THEN 'skip' ELSE 'assign' @@ -4739,7 +4750,7 @@ WHERE h.hardware_serial IN (?) ` - stmt, args, err := sqlx.In(stmt, string(fleet.DEPAssignProfileResponseFailed), depCooldownPeriod.Seconds(), serials) + stmt, args, err := sqlx.In(stmt, string(fleet.DEPAssignProfileResponseFailed), depFailedCooldownPeriod.Seconds(), string(fleet.DEPAssignProfileResponseThrottled), depThrottledCooldownPeriod.Seconds(), serials) if err != nil { return nil, nil, ctxerr.Wrap(ctx, err, "screen dep serials: prepare statement arguments") } @@ -4791,10 +4802,14 @@ FROM JOIN hosts h ON h.id = host_id LEFT JOIN jobs j ON j.id = retry_job_id WHERE - assign_profile_response = ? + (assign_profile_response = ? AND(retry_job_id = 0 OR j.state = ?) AND(response_updated_at IS NULL OR response_updated_at <= DATE_SUB(NOW(), INTERVAL ? SECOND)) +) OR (assign_profile_response = ? + AND(retry_job_id = 0 OR j.state = ?) + AND(response_updated_at IS NULL + OR response_updated_at <= DATE_SUB(NOW(), INTERVAL ? SECOND))) ORDER BY response_updated_at ASC LIMIT ?` @@ -4802,7 +4817,7 @@ LIMIT ?` TeamID uint `db:"team_id"` HardwareSerial string `db:"hardware_serial"` } - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, string(fleet.DEPAssignProfileResponseFailed), string(fleet.JobStateFailure), depCooldownPeriod.Seconds(), apple_mdm.DEPSyncLimit); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, string(fleet.DEPAssignProfileResponseFailed), string(fleet.JobStateFailure), depFailedCooldownPeriod.Seconds(), string(fleet.DEPAssignProfileResponseThrottled), string(fleet.JobStateFailure), depThrottledCooldownPeriod.Seconds(), apple_mdm.DEPSyncLimit); err != nil { return nil, ctxerr.Wrap(ctx, err, "get host dep assign profile expired cooldowns") } diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 2c8f9d559f..8893335c9b 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -9714,7 +9714,7 @@ func testGetDEPAssignProfileExpiredCooldowns(t *testing.T, ds *Datastore) { // Set response_updated_at to be dep cooldown + 10 seconds to avoid timing issues ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { - _, err := q.ExecContext(ctx, `UPDATE host_dep_assignments SET response_updated_at = DATE_SUB(NOW(), INTERVAL ? SECOND) WHERE host_id = ?`, depCooldownPeriod.Seconds()+10, host.ID) + _, err := q.ExecContext(ctx, `UPDATE host_dep_assignments SET response_updated_at = DATE_SUB(NOW(), INTERVAL ? SECOND) WHERE host_id = ?`, depFailedCooldownPeriod.Seconds()+10, host.ID) return err }) cooldowns, err = ds.GetDEPAssignProfileExpiredCooldowns(ctx) @@ -9725,7 +9725,7 @@ func testGetDEPAssignProfileExpiredCooldowns(t *testing.T, ds *Datastore) { for i := range 200 { h := newTestHostWithPlatform(t, ds, fmt.Sprintf("host-%d", i), "macos", nil) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { - _, err := q.ExecContext(ctx, `INSERT INTO host_dep_assignments (host_id, profile_uuid, assign_profile_response, response_updated_at, retry_job_id) VALUES (?, ?, ?, DATE_SUB(NOW(), INTERVAL ? SECOND), 0)`, h.ID, uuid.NewString(), fleet.DEPAssignProfileResponseFailed, depCooldownPeriod.Seconds()+10) + _, err := q.ExecContext(ctx, `INSERT INTO host_dep_assignments (host_id, profile_uuid, assign_profile_response, response_updated_at, retry_job_id) VALUES (?, ?, ?, DATE_SUB(NOW(), INTERVAL ? SECOND), 0)`, h.ID, uuid.NewString(), fleet.DEPAssignProfileResponseFailed, depFailedCooldownPeriod.Seconds()+10) return err }) } diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 1bf80b6498..894b8d11bd 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -927,7 +927,7 @@ const hostMDMSelect = `, 'enrollment_status', hmdm.enrollment_status, 'dep_profile_error', CASE - WHEN hdep.assign_profile_response = '` + string(fleet.DEPAssignProfileResponseFailed) + `' THEN CAST(TRUE AS JSON) + WHEN hdep.assign_profile_response IN ('` + string(fleet.DEPAssignProfileResponseFailed) + `', '` + string(fleet.DEPAssignProfileResponseThrottled) + `') THEN CAST(TRUE AS JSON) ELSE CAST(FALSE AS JSON) END, 'server_url', diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index a0bb7164d4..991d8461b0 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -554,6 +554,7 @@ const ( DEPAssignProfileResponseSuccess DEPAssignProfileResponseStatus = "SUCCESS" DEPAssignProfileResponseNotAccessible DEPAssignProfileResponseStatus = "NOT_ACCESSIBLE" DEPAssignProfileResponseFailed DEPAssignProfileResponseStatus = "FAILED" + DEPAssignProfileResponseThrottled DEPAssignProfileResponseStatus = "THROTTLED" ) // NanoEnrollment represents a row in the nano_enrollments table managed by @@ -1118,9 +1119,11 @@ type AppleMDMVPPInstaller interface { InstallVPPAppPostValidation(ctx context.Context, host *Host, vppApp *VPPApp, token string, opts HostSoftwareInstallOptions) (string, error) } -const DeviceLocationCmdName = "DeviceLocation" -const EnableLostModeCmdName = "EnableLostMode" -const DisableLostModeCmdName = "DisableLostMode" +const ( + DeviceLocationCmdName = "DeviceLocation" + EnableLostModeCmdName = "EnableLostMode" + DisableLostModeCmdName = "DisableLostMode" +) type HostLocationData struct { HostID uint `db:"host_id"` diff --git a/server/fleet/hosts_test.go b/server/fleet/hosts_test.go index 9597939d14..9e2d74dc63 100644 --- a/server/fleet/hosts_test.go +++ b/server/fleet/hosts_test.go @@ -297,6 +297,15 @@ func TestIsEligibleForDEPMigration(t *testing.T) { expected: false, expectedManual: false, }, + { + name: "Not eligible - DEP assigned and DEP profile throttled", + osqueryHostID: ptr.String("some-id"), + depAssignedToFleet: ptr.Bool(true), + depProfileResponse: DEPAssignProfileResponseThrottled, + enrolledInThirdPartyMDM: true, + expected: false, + expectedManual: false, + }, { name: "Not eligible - DEP assigned but not response yet", osqueryHostID: ptr.String("some-id"), diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index 1078f0e993..778f750605 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -950,7 +950,7 @@ func (d *DEPService) getProfileUUIDForTeam(ctx context.Context, tmID *uint, abmT // logCountsForResults tries to aggregate the result types and log the counts. func logCountsForResults(deviceResults map[string]string) (out []interface{}) { - results := map[string]int{"success": 0, "not_accessible": 0, "failed": 0, "other": 0} + results := map[string]int{"success": 0, "not_accessible": 0, "failed": 0, "throttled": 0, "other": 0} for _, result := range deviceResults { l := strings.ToLower(result) if _, ok := results[l]; !ok { diff --git a/server/mdm/apple/apple_mdm_external_test.go b/server/mdm/apple/apple_mdm_external_test.go index fbe41405e1..0b8ab516b6 100644 --- a/server/mdm/apple/apple_mdm_external_test.go +++ b/server/mdm/apple/apple_mdm_external_test.go @@ -428,4 +428,104 @@ func TestDEPService_RunAssigner(t *testing.T) { return nil }) }) + + t.Run("assign returns throttled for one device", func(t *testing.T) { + start := time.Now().Truncate(time.Second) + + devices := []godep.Device{ + {SerialNumber: "a", OpType: "added"}, + {SerialNumber: "b", OpType: "ignore"}, + {SerialNumber: "c", OpType: ""}, + } + + var assignCalled bool + svc := setupTest(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + encoder := json.NewEncoder(w) + switch r.URL.Path { + case "/session": + _, _ = w.Write([]byte(`{"auth_session_token": "session123"}`)) + case "/account": + _, _ = w.Write(fmt.Appendf(nil, `{"admin_id": "admin123", "org_name": "%s"}`, abmTokenOrgName)) + case "/profile": + err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "profile123"}) + require.NoError(t, err) + case "/server/devices": + err := encoder.Encode(godep.DeviceResponse{}) + require.NoError(t, err) + case "/devices/sync": + err := encoder.Encode(godep.DeviceResponse{Devices: devices}) + require.NoError(t, err) + case "/profile/devices": + assignCalled = true + + reqBody, err := io.ReadAll(r.Body) + require.NoError(t, err) + + var assignReq godep.Profile + err = json.Unmarshal(reqBody, &assignReq) + require.NoError(t, err) + require.Equal(t, assignReq.ProfileUUID, "profile123") + require.ElementsMatch(t, []string{"a", "c"}, assignReq.Devices) + apiResp := godep.ProfileResponse{ + ProfileUUID: "profile123", + Devices: map[string]string{ + "a": string(fleet.DEPAssignProfileResponseSuccess), + "c": string(fleet.DEPAssignProfileResponseThrottled), + }, + } + respBytes, err := json.Marshal(&apiResp) + require.NoError(t, err) + + _, err = w.Write(respBytes) + require.NoError(t, err) + default: + t.Errorf("unexpected request to %s", r.URL.Path) + } + }) + err := svc.RunAssigner(ctx) + require.NoError(t, err) + require.True(t, assignCalled) + + // the default profile was created + defProf, err := ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic) + require.NoError(t, err) + require.NotNil(t, defProf) + require.NotEmpty(t, defProf.Token) + + // a profile UUID was assigned to no-team + profUUID, modTime, err := ds.GetMDMAppleDefaultSetupAssistant(ctx, nil, abmTokenOrgName) + require.NoError(t, err) + require.Equal(t, "profile123", profUUID) + require.False(t, modTime.Before(start)) + + // a couple hosts were created (except the op_type ignored) + hosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}) + require.NoError(t, err) + require.Len(t, hosts, 2) + serials := make([]string, len(hosts)) + for i, h := range hosts { + serials[i] = h.HardwareSerial + require.Nil(t, h.TeamID, h.HardwareSerial) + } + require.ElementsMatch(t, []string{"a", "c"}, serials) + + // Verify that the one host has an assignment marked throttled + mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + stmt := "SELECT COUNT(*) FROM host_dep_assignments WHERE host_id = ? AND assign_profile_response = ?" + var result int + require.NoError(t, sqlx.GetContext(ctx, q, &result, stmt, hosts[1].ID, fleet.DEPAssignProfileResponseThrottled)) + require.Equal(t, 1, result, "expected one throttled assignment for serial c") + return nil + }) + + // And the other is marked success + mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + stmt := "SELECT COUNT(*) FROM host_dep_assignments WHERE host_id = ? AND assign_profile_response = ?" + var result int + require.NoError(t, sqlx.GetContext(ctx, q, &result, stmt, hosts[0].ID, fleet.DEPAssignProfileResponseSuccess)) + require.Equal(t, 1, result, "expected one success assignment for serial a") + return nil + }) + }) } diff --git a/server/mdm/nanodep/client/transport.go b/server/mdm/nanodep/client/transport.go index 436b036f00..b6842fd21d 100644 --- a/server/mdm/nanodep/client/transport.go +++ b/server/mdm/nanodep/client/transport.go @@ -16,7 +16,7 @@ const ( ADMAuthSession = "X-ADM-Auth-Session" ServerProtocolVersion = "X-Server-Protocol-Version" - DefaultServerProtocolVersion = "8" + DefaultServerProtocolVersion = "9" SessionEndpoint = "/session" diff --git a/server/service/devices_test.go b/server/service/devices_test.go index fa58cd2962..b750fbb995 100644 --- a/server/service/devices_test.go +++ b/server/service/devices_test.go @@ -417,6 +417,25 @@ func TestGetFleetDesktopSummary(t *testing.T) { RenewEnrollmentProfile: false, }, }, + { + name: "throttled ADE assignment status", + host: &fleet.Host{ + DEPAssignedToFleet: ptr.Bool(true), + OsqueryHostID: ptr.String("test"), + }, + hostMDM: &fleet.HostMDM{ + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMIntune, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseThrottled)), + }, + err: nil, + out: fleet.DesktopNotifications{ + NeedsMDMMigration: false, + RenewEnrollmentProfile: false, + }, + }, { name: "not accessible ADE assignment status", host: &fleet.Host{ diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 163f772955..58c6aa98c0 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -1014,6 +1014,7 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { } expectAssignProfileResponseFailed := "" // set to device serial when testing the failed profile assignment flow + expectAssignProfileResponseThrottled := "" // set to device serial when testing the throttled profile assignment flow expectAssignProfileResponseNotAccessible := "" // set to device serial when testing the not accessible profile assignment flow s.enableABM(t.Name()) @@ -1052,6 +1053,8 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { resp.Devices[device] = string(fleet.DEPAssignProfileResponseNotAccessible) case expectAssignProfileResponseFailed: resp.Devices[device] = string(fleet.DEPAssignProfileResponseFailed) + case expectAssignProfileResponseThrottled: + resp.Devices[device] = string(fleet.DEPAssignProfileResponseThrottled) default: resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess) } @@ -1651,6 +1654,66 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { checkHostCooldown(serial, expectProfileUUID, fleet.DEPAssignProfileResponseNotAccessible, &failedAt, expectNoJobID) // no change checkNoJobsPending() + expectAssignProfileResponseNotAccessible = "" + + // ingest new device via DEP but the profile assignment fails + serial = uuid.NewString() + devices = []godep.Device{ + {SerialNumber: serial, Model: "MacBook Pro", OS: "osx", OpType: "added"}, + } + expectAssignProfileResponseThrottled = serial + profileAssignmentReqs = []profileAssignmentReq{} + s.runDEPSchedule() + checkAssignProfileRequests(serial, nil) + profUUID = profileAssignmentReqs[0].ProfileUUID + d = checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseThrottled, nil, expectNoJobID) + require.NotZero(t, d.ResponseUpdatedAt) + failedAt = d.ResponseUpdatedAt + checkNoJobsPending() + h = checkListHostDEPError(serial, "Pending", true) // list hosts shows device pending and dep profile error + + // transfer to team, no profile assignment request is made during the cooldown period + profileAssignmentReqs = []profileAssignmentReq{} + s.Do("POST", "/api/v1/fleet/hosts/transfer", + addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{h.ID}}, http.StatusOK) + checkPendingMacOSSetupAssistantJob("hosts_transferred", &team.ID, []string{serial}, 0) + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // screened by cooldown + checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseThrottled, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // run the integrations schedule and expect no changes + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) + checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseThrottled, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // simulate expired cooldown + failedAt = failedAt.Add(-25 * time.Hour) + setAssignProfileResponseUpdatedAt(serial, failedAt) + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // assign profile request will be made when the retry job is processed on the next worker run + d = checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseThrottled, &failedAt, nil) + require.NotZero(t, d.RetryJobID) // retry job created + jobID = d.RetryJobID + checkPendingMacOSSetupAssistantJob("hosts_cooldown", &team.ID, []string{serial}, jobID) + + // run the inregration schedule and expect success + expectAssignProfileResponseThrottled = "" + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + checkAssignProfileRequests(serial, nil) + require.NotEqual(t, profUUID, profileAssignmentReqs[0].ProfileUUID) // retry job will use the current team profile instead + profUUID = profileAssignmentReqs[0].ProfileUUID + d = checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseSuccess, nil, expectNoJobID) // retry job cleared + require.True(t, d.ResponseUpdatedAt.After(failedAt)) + checkNoJobsPending() + // list hosts shows pending (because MDM detail query hasn't been reported) but dep profile + // error has been cleared + checkListHostDEPError(serial, "Pending", false) + // run with devices that already have valid and invalid profiles // assigned, we shouldn't re-assign the valid ones. devices = []godep.Device{ @@ -1659,9 +1722,8 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { {SerialNumber: uuid.NewString(), Model: "MacBook Pro", OS: "osx", OpType: "added", ProfileUUID: "bar"}, // doesn't match an existing profile {SerialNumber: uuid.NewString(), Model: "MacBook Mini", OS: "osx", OpType: "modified", ProfileUUID: "foo"}, // doesn't match an existing profile {SerialNumber: addedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", ProfileUUID: defaultProfileUUID}, // matches existing profile, but will be assigned since it is "added" - {SerialNumber: serial, Model: "MacBook Mini", OS: "osx", OpType: "modified", ProfileUUID: defaultProfileUUID}, // matches existing profile + {SerialNumber: serial, Model: "MacBook Mini", OS: "osx", OpType: "modified", ProfileUUID: profUUID}, // matches existing profile } - expectAssignProfileResponseNotAccessible = "" profileAssignmentReqs = []profileAssignmentReq{} s.runDEPSchedule() require.NotEmpty(t, profileAssignmentReqs) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index b6e9b17924..2a8b686f1f 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -7041,6 +7041,22 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial})) cleanAssignmentStatus() + // simulate a "THROTTLED" JSON profile assignment + profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseThrottled + s.runDEPSchedule() + getDesktopResp = fleetDesktopResponse{} + res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK) + require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp)) + require.NoError(t, res.Body.Close()) + require.False(t, getDesktopResp.Notifications.NeedsMDMMigration) + require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + orbitConfigResp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp) + require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration) + require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial})) + cleanAssignmentStatus() + // simulate a "NOT_ACCESSIBLE" JSON profile assignment profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseNotAccessible s.runDEPSchedule()