diff --git a/changes/23200-ade-enroll b/changes/23200-ade-enroll new file mode 100644 index 0000000000..6a6c597bf4 --- /dev/null +++ b/changes/23200-ade-enroll @@ -0,0 +1,2 @@ +- Fixes a bug where a device that was removed from ABM and then added back wouldn't properly + re-enroll in Fleet MDM \ No newline at end of file diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 4a9af648f6..d259e91409 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -5155,6 +5155,42 @@ func (ds *Datastore) GetMatchingHostSerials(ctx context.Context, serials []strin return result, nil } +func (ds *Datastore) GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error) { + result := map[string]struct{}{} + if len(serials) == 0 { + return result, nil + } + + stmt := ` +SELECT + hardware_serial +FROM + hosts h + JOIN host_dep_assignments hdep ON hdep.host_id = h.id +WHERE + h.hardware_serial IN (?) AND hdep.deleted_at IS NOT NULL; + ` + + var args []interface{} + for _, serial := range serials { + args = append(args, serial) + } + stmt, args, err := sqlx.In(stmt, args) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "building IN statement for matching hosts") + } + var matchingSerials []string + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &matchingSerials, stmt, args...); err != nil { + return nil, err + } + + for _, serial := range matchingSerials { + result[serial] = struct{}{} + } + + return result, nil +} + func (ds *Datastore) GetHostHealth(ctx context.Context, id uint) (*fleet.HostHealth, error) { sqlStmt := ` SELECT h.os_version, h.updated_at, h.platform, h.team_id, hd.encrypted as disk_encryption_enabled FROM hosts h diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index bb82622b6a..fd41dc45e8 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -170,6 +170,7 @@ func TestHosts(t *testing.T) { {"UpdateHostIssues", testUpdateHostIssues}, {"ListUpcomingHostMaintenanceWindows", testListUpcomingHostMaintenanceWindows}, {"GetHostEmails", testGetHostEmails}, + {"TestGetMatchingHostSerialsMarkedDeleted", testGetMatchingHostSerialsMarkedDeleted}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -9751,3 +9752,83 @@ func testGetHostEmails(t *testing.T, ds *Datastore) { require.NoError(t, err) assert.ElementsMatch(t, []string{"foo@example.com", "bar@example.com"}, emails) } + +func testGetMatchingHostSerialsMarkedDeleted(t *testing.T, ds *Datastore) { + ctx := context.Background() + serials := []string{"foo", "bar", "baz"} + team, err := ds.NewTeam(context.Background(), &fleet.Team{ + Name: "team1", + }) + require.NoError(t, err) + abmTok, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: t.Name(), EncryptedToken: []byte("token")}) + require.NoError(t, err) + var hosts []fleet.Host + for i, serial := range serials { + var tmID *uint + if serial == "bar" { + tmID = &team.ID + } + h, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String(fmt.Sprint(i)), + UUID: fmt.Sprint(i), + OsqueryHostID: ptr.String(fmt.Sprint(i)), + Hostname: "foo.local", + PrimaryIP: "192.168.1.1", + PrimaryMac: "30-65-EC-6F-C4-58", + HardwareSerial: serial, + TeamID: tmID, + ID: uint(i), + }) + require.NoError(t, err) + require.NotNil(t, h) + + // Only "foo" and "baz" are + if i%2 == 0 { + hosts = append(hosts, *h) + } + } + + require.NoError(t, ds.UpsertMDMAppleHostDEPAssignments(ctx, hosts, abmTok.ID)) + require.NoError(t, ds.DeleteHostDEPAssignments(ctx, abmTok.ID, serials)) + + cases := []struct { + name string + in []string + want map[string]struct{} + err string + }{ + {"no serials provided", []string{}, map[string]struct{}{}, ""}, + {"no matching serials", []string{"oof", "rab", "bar"}, map[string]struct{}{}, ""}, + { + "partial matches", + []string{"foo", "rab", "bar"}, + map[string]struct{}{"foo": {}}, + "", + }, + { + "all matching", + []string{"foo", "baz"}, + map[string]struct{}{ + "foo": {}, + "baz": {}, + }, + "", + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got, err := ds.GetMatchingHostSerialsMarkedDeleted(ctx, tt.in) + if tt.err == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.err) + } + require.Equal(t, tt.want, got) + }) + } +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index c05e5ac4fc..c962d4119c 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1298,6 +1298,11 @@ type Datastore interface { // a map that only contains the serials that have a matching row in the `hosts` table. GetMatchingHostSerials(ctx context.Context, serials []string) (map[string]*Host, error) + // GetMatchingHostSerialsMarkedDeleted takes a list of device serial numbers and returns a map + // of only the ones that were found in the `hosts` table AND have a row in + // `host_dep_assignments` that is marked as deleted. + GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error) + // DeleteHostDEPAssignmentsFromAnotherABM makes as deleted any DEP entry that matches one of the provided serials only if the entry is NOT associated to the provided ABM token. DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index 49e1125f24..5ea71f3aa2 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -660,6 +660,18 @@ func (d *DEPService) processDeviceResponse( for _, device := range addedDevicesSlice { addedSerials = append(addedSerials, device.SerialNumber) } + + // Check if any of the "added" or "modified" hosts are hosts that we've recently removed from + // Fleet in ABM. A host in this state will have a row in `host_dep_assignments` where the + // `deleted_at ` col is NOT NULL. Down below we skip assigning the profile to devices that we + // think are still enrolled; doing this check here allows us to avoid skipping devices that + // _seem_ like they're still enrolled but were actually removed and should get the profile. + // See https://github.com/fleetdm/fleet/issues/23200 for more context. + existingDeletedSerials, err := d.ds.GetMatchingHostSerialsMarkedDeleted(ctx, addedSerials) + if err != nil { + return ctxerr.Wrap(ctx, err, "get matching deleted host serials") + } + err = d.ds.DeleteHostDEPAssignmentsFromAnotherABM(ctx, abmTokenID, addedSerials) if err != nil { return ctxerr.Wrap(ctx, err, "deleting dep assignments from another abm") @@ -682,7 +694,7 @@ func (d *DEPService) processDeviceResponse( } level.Debug(kitlog.With(d.logger)).Log("msg", "devices to assign DEP profiles", "to_add", len(addedDevicesSlice), "to_remove", - deletedSerials, "to_modify", modifiedSerials) + strings.Join(deletedSerials, ", "), "to_modify", strings.Join(modifiedSerials, ", ")) // at this point, the hosts rows are created for the devices, with the // correct team_id, so we know what team-specific profile needs to be applied. @@ -754,7 +766,8 @@ func (d *DEPService) processDeviceResponse( for profUUID, devices := range profileToDevices { var serials []string for _, device := range devices { - if device.ProfileUUID == profUUID { + _, ok := existingDeletedSerials[device.SerialNumber] + if device.ProfileUUID == profUUID && !ok { skippedSerials = append(skippedSerials, device.SerialNumber) continue } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 48cc6d6076..ad49d7a6aa 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -859,6 +859,8 @@ type GetMDMAppleDefaultSetupAssistantFunc func(ctx context.Context, teamID *uint type GetMatchingHostSerialsFunc func(ctx context.Context, serials []string) (map[string]*fleet.Host, error) +type GetMatchingHostSerialsMarkedDeletedFunc func(ctx context.Context, serials []string) (map[string]struct{}, error) + type DeleteHostDEPAssignmentsFromAnotherABMFunc func(ctx context.Context, abmTokenID uint, serials []string) error type DeleteHostDEPAssignmentsFunc func(ctx context.Context, abmTokenID uint, serials []string) error @@ -2405,6 +2407,9 @@ type DataStore struct { GetMatchingHostSerialsFunc GetMatchingHostSerialsFunc GetMatchingHostSerialsFuncInvoked bool + GetMatchingHostSerialsMarkedDeletedFunc GetMatchingHostSerialsMarkedDeletedFunc + GetMatchingHostSerialsMarkedDeletedFuncInvoked bool + DeleteHostDEPAssignmentsFromAnotherABMFunc DeleteHostDEPAssignmentsFromAnotherABMFunc DeleteHostDEPAssignmentsFromAnotherABMFuncInvoked bool @@ -5773,6 +5778,13 @@ func (s *DataStore) GetMatchingHostSerials(ctx context.Context, serials []string return s.GetMatchingHostSerialsFunc(ctx, serials) } +func (s *DataStore) GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error) { + s.mu.Lock() + s.GetMatchingHostSerialsMarkedDeletedFuncInvoked = true + s.mu.Unlock() + return s.GetMatchingHostSerialsMarkedDeletedFunc(ctx, serials) +} + func (s *DataStore) DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error { s.mu.Lock() s.DeleteHostDEPAssignmentsFromAnotherABMFuncInvoked = true diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 50ec5112cc..d2a32c8e33 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -679,18 +679,19 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { } type hostDEPRow struct { - HostID uint `db:"host_id"` - ProfileUUID string `db:"profile_uuid"` - AssignProfileResponse string `db:"assign_profile_response"` - ResponseUpdatedAt time.Time `db:"response_updated_at"` - RetryJobID uint `db:"retry_job_id"` + HostID uint `db:"host_id"` + ProfileUUID string `db:"profile_uuid"` + AssignProfileResponse string `db:"assign_profile_response"` + ResponseUpdatedAt time.Time `db:"response_updated_at"` + RetryJobID uint `db:"retry_job_id"` + DeletedAt *time.Time `db:"deleted_at"` } checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow { bySerial := make(map[string]hostDEPRow, len(deviceSerials)) for _, deviceSerial := range deviceSerials { mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { var dest hostDEPRow - err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial) + err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id, deleted_at FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial) require.NoError(t, err) require.Equal(t, string(expectedStatus), dest.AssignProfileResponse) bySerial[deviceSerial] = dest @@ -1046,14 +1047,22 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { deletedSerial = devices[1].SerialNumber devices = []godep.Device{ {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now()}, - {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified", - OpDate: time.Now().Add(time.Second)}, - {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", - OpDate: time.Now().Add(2 * time.Second)}, - {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", - OpDate: time.Now().Add(3 * time.Second)}, - {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", - OpDate: time.Now().Add(4 * time.Second)}, + { + SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified", + OpDate: time.Now().Add(time.Second), + }, + { + SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", + OpDate: time.Now().Add(2 * time.Second), + }, + { + SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", + OpDate: time.Now().Add(3 * time.Second), + }, + { + SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", + OpDate: time.Now().Add(4 * time.Second), + }, {SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()}, {SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now().Add(time.Second)}, @@ -1436,7 +1445,8 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() { RetryJobID uint `db:"retry_job_id"` } checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, - expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow { + expectedStatus fleet.DEPAssignProfileResponseStatus, + ) map[string]hostDEPRow { bySerial := make(map[string]hostDEPRow, len(deviceSerials)) for _, deviceSerial := range deviceSerials { mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { @@ -1627,14 +1637,18 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() { devices = []godep.Device{ {SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "added", OpDate: time.Now()}, {SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", OpDate: time.Now()}, - {SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added", - OpDate: time.Now().Add(time.Microsecond)}, + { + SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added", + OpDate: time.Now().Add(time.Microsecond), + }, } defaultOrgDevices = []godep.Device{ {SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "added", OpDate: time.Now()}, {SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", OpDate: time.Now()}, - {SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added", - OpDate: time.Now().Add(time.Microsecond)}, + { + SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added", + OpDate: time.Now().Add(time.Microsecond), + }, } // trigger a profile sync @@ -1663,13 +1677,17 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() { // Delete the devices devices = []godep.Device{ {SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "modified", OpDate: time.Now()}, - {SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", - OpDate: time.Now().Add(time.Microsecond)}, + { + SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", + OpDate: time.Now().Add(time.Microsecond), + }, } defaultOrgDevices = []godep.Device{ {SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "modified", OpDate: time.Now()}, - {SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", - OpDate: time.Now().Add(time.Microsecond)}, + { + SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", + OpDate: time.Now().Add(time.Microsecond), + }, } // trigger a profile sync @@ -1694,7 +1712,6 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() { checkHostDEPAssignProfileResponses(defaultSerials, defaultProfileUUIDs[len(defaultProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess) checkHostDEPAssignProfileResponses(teamSerials, teamProfileUUIDs[len(teamProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess) - } func (s *integrationMDMTestSuite) TestDeprecatedDefaultAppleBMTeam() { @@ -2342,3 +2359,206 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo require.Equal(t, 1, deviceConfiguredCount) require.Equal(t, 0, otherCount) } + +func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM() { + t := s.T() + s.enableABM(t.Name()) + ctx := context.Background() + + checkPostEnrollmentCommands := func(mdmDevice *mdmtest.TestAppleMDMClient, shouldReceive bool) { + // run the worker to process the DEP enroll request + s.runWorker() + // run the worker to assign configuration profiles + s.awaitTriggerProfileSchedule(t) + + var fleetdCmd, installProfileCmd *micromdm.CommandPayload + cmd, err := mdmDevice.Idle() + require.NoError(t, err) + for cmd != nil { + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + if fullCmd.Command.RequestType == "InstallEnterpriseApplication" && + fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil && + strings.Contains(*fullCmd.Command.InstallEnterpriseApplication.ManifestURL, fleetdbase.GetPKGManifestURL()) { + fleetdCmd = &fullCmd + } else if cmd.Command.RequestType == "InstallProfile" { + installProfileCmd = &fullCmd + } + cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) + require.NoError(t, err) + } + + if shouldReceive { + // received request to install fleetd + require.NotNil(t, fleetdCmd, "host didn't get a command to install fleetd") + require.NotNil(t, fleetdCmd.Command, "host didn't get a command to install fleetd") + + // received request to install the global configuration profile + require.NotNil(t, installProfileCmd, "host didn't get a command to install profiles") + require.NotNil(t, installProfileCmd.Command, "host didn't get a command to install profiles") + } else { + require.Nil(t, fleetdCmd, "host got a command to install fleetd") + require.Nil(t, installProfileCmd, "host got a command to install profiles") + } + } + + devices := []godep.Device{ + {SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"}, + } + + profileAssignmentReqs := []profileAssignmentReq{} + + type hostDEPRow struct { + HostID uint `db:"host_id"` + ProfileUUID string `db:"profile_uuid"` + AssignProfileResponse string `db:"assign_profile_response"` + ResponseUpdatedAt time.Time `db:"response_updated_at"` + RetryJobID uint `db:"retry_job_id"` + DeletedAt *time.Time `db:"deleted_at"` + } + checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow { + bySerial := make(map[string]hostDEPRow, len(deviceSerials)) + for _, deviceSerial := range deviceSerials { + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var dest hostDEPRow + err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id, deleted_at FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial) + require.NoError(t, err) + require.Equal(t, string(expectedStatus), dest.AssignProfileResponse) + bySerial[deviceSerial] = dest + return nil + }) + } + return bySerial + } + + s.mockDEPResponse(t.Name(), 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: uuid.New().String()}) + require.NoError(t, err) + case "/server/devices": + // This endpoint is used to get an initial list of + // devices, return a single device + err := encoder.Encode(godep.DeviceResponse{Devices: devices[:1]}) + require.NoError(t, err) + case "/devices/sync": + // This endpoint is polled over time to sync devices from + // ABM, send a repeated serial and a new one + err := encoder.Encode(godep.DeviceResponse{Devices: devices, Cursor: "foo"}) + require.NoError(t, err) + case "/profile/devices": + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + var prof profileAssignmentReq + require.NoError(t, json.Unmarshal(b, &prof)) + profileAssignmentReqs = append(profileAssignmentReqs, prof) + var resp godep.ProfileResponse + resp.ProfileUUID = prof.ProfileUUID + resp.Devices = make(map[string]string, len(prof.Devices)) + for _, device := range prof.Devices { + resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess) + } + err = encoder.Encode(resp) + require.NoError(t, err) + default: + _, _ = w.Write([]byte(`{}`)) + } + })) + + s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) { + return map[string]*push.Response{}, nil + } + + // Enroll the host via ADE + depURLToken := loadEnrollmentProfileDEPToken(t, s.ds) + mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken) + mdmDevice.SerialNumber = devices[0].SerialNumber + err := mdmDevice.Enroll() + require.NoError(t, err) + + // Simulate an osquery enrollment too + // set an enroll secret + var applyResp applyEnrollSecretSpecResponse + s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{ + Spec: &fleet.EnrollSecretSpec{ + Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}}, + }, + }, http.StatusOK, &applyResp) + + // simulate a matching host enrolling via osquery + j, err := json.Marshal(&enrollAgentRequest{ + EnrollSecret: t.Name(), + HostIdentifier: mdmDevice.UUID, + }) + require.NoError(t, err) + var enrollResp enrollAgentResponse + hres := s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusOK) + defer hres.Body.Close() + require.NoError(t, json.NewDecoder(hres.Body).Decode(&enrollResp)) + require.NotEmpty(t, enrollResp.NodeKey) + + listHostsRes := listHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes) + require.Len(t, listHostsRes.Hosts, 1) + h := listHostsRes.Hosts[0] + + s.runDEPSchedule() + + // make sure the host gets post enrollment requests + checkPostEnrollmentCommands(mdmDevice, true) + + var hostResp getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp) + // 1 profile with fleetd configuration + 1 root CA config + require.Len(t, *hostResp.Host.MDM.Profiles, 2) + + // Turn MDM off in the host + s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", h.ID), nil, http.StatusOK) + + // profiles are removed and the host is no longer enrolled + hostResp = getHostResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp) + require.Nil(t, hostResp.Host.MDM.Profiles) + require.Equal(t, "", hostResp.Host.MDM.Name) + + err = mdmDevice.Checkout() + require.NoError(t, err) + + // Simulate the device getting unassigned from Fleet in ABM + devices = []godep.Device{ + {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()}, + } + + t.Log("RUN AFTER DELETED") + s.runDEPSchedule() + + a := checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) + require.NotZero(t, a[mdmDevice.SerialNumber].DeletedAt) + + // Now we add the device back into ABM + profileAssignmentReqs = []profileAssignmentReq{} + + devices = []godep.Device{ + // In https://github.com/fleetdm/fleet/issues/23200, we saw a profileUUID being sent back on + // the godep.Device in the response from ABM. We're not 100% sure why, but the fact that + // this field is set was the source of the bug, which is why we're including it here. + {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now(), ProfileUUID: a[mdmDevice.SerialNumber].ProfileUUID}, + } + + t.Log("RUN AFTER RE-ADDED") + s.runDEPSchedule() + + a = checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) + require.Nil(t, a[mdmDevice.SerialNumber].DeletedAt) + + err = mdmDevice.Enroll() + require.NoError(t, err) + + // make sure the host gets post enrollment requests + checkPostEnrollmentCommands(mdmDevice, true) +} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 0e51ef7cc1..2d98542328 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11914,7 +11914,7 @@ func (s *integrationMDMTestSuite) TestSetupExperience() { require.True(t, vppFound, "vpp app not found in status results") require.True(t, softwareFound, "software installer app not found in status results") - x, err := s.ds.GetHostAwaitingConfiguration(ctx, fleetHost.UUID) + awaitingConfig, err := s.ds.GetHostAwaitingConfiguration(ctx, fleetHost.UUID) require.NoError(t, err) - require.True(t, x) + require.True(t, awaitingConfig) }