mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
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:
parent
4eb8cefba9
commit
e27e916f74
8 changed files with 397 additions and 28 deletions
2
changes/23200-ade-enroll
Normal file
2
changes/23200-ade-enroll
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue