fix: re-enroll devices that are removed from ABM and then added back (#23757)

> Related issue: #23200

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [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/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] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Jahziel Villasana-Espinoza 2024-11-14 18:31:12 -05:00 committed by GitHub
parent 4eb8cefba9
commit e27e916f74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 397 additions and 28 deletions

2
changes/23200-ade-enroll Normal file
View file

@ -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

View file

@ -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

View file

@ -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)
})
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}