LM: Fix deleted iOS/iPadOS checking in does not update lost mode status (#34250)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34203 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
This commit is contained in:
@@ -930,6 +930,7 @@ func (c *TestAppleMDMClient) AcknowledgeDeviceInformation(udid, cmdUUID, deviceN
|
||||
"OSVersion": "17.5.1",
|
||||
"ProductName": productName,
|
||||
"WiFiMAC": "ff:ff:ff:ff:ff:ff",
|
||||
"IsMDMLostModeEnabled": false,
|
||||
},
|
||||
}
|
||||
return c.sendAndDecodeCommandResponse(payload)
|
||||
|
||||
@@ -7147,3 +7147,30 @@ WHERE
|
||||
|
||||
return &idp, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetLatestAppleMDMCommandOfType(ctx context.Context, hostUUID string, commandType string) (*fleet.MDMCommand, error) {
|
||||
const stmt = `
|
||||
SELECT id as host_uuid, command_uuid, request_type FROM nano_view_queue WHERE id = ? AND request_type = ? ORDER BY created_at DESC LIMIT 1
|
||||
`
|
||||
|
||||
var cmd fleet.MDMCommand
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &cmd, stmt, hostUUID, commandType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, notFound("MDMCommand")
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "get latest apple mdm command of type")
|
||||
}
|
||||
|
||||
return &cmd, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) SetLockCommandForLostModeCheckin(ctx context.Context, hostID uint, commandUUID string) error {
|
||||
// We know we can insert here, as this is only called when processing a
|
||||
// a new iphone/ipad checkin with lost mode enabled.
|
||||
const stmt = `
|
||||
INSERT INTO host_mdm_actions (host_id, lock_ref)
|
||||
VALUES (?, ?)
|
||||
`
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, stmt, hostID, commandUUID)
|
||||
return ctxerr.Wrap(ctx, err, "set lock ref for lost mode checkin")
|
||||
}
|
||||
|
||||
@@ -104,6 +104,8 @@ func TestMDMApple(t *testing.T) {
|
||||
{"TestDeleteMDMAppleDeclarationWithPendingInstalls", testDeleteMDMAppleDeclarationWithPendingInstalls},
|
||||
{"TestUpdateNanoMDMUserEnrollmentUsername", testUpdateNanoMDMUserEnrollmentUsername},
|
||||
{"TestLockUnlockWipeIphone", testLockUnlockWipeIphone},
|
||||
{"TestGetLatestAppleMDMCommandOfType", testGetLatestAppleMDMCommandOfType},
|
||||
{"TestSetLockCommandForLostModeCheckin", testSetLockCommandForLostModeCheckin},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -9483,3 +9485,71 @@ func testUpdateNanoMDMUserEnrollmentUsername(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, nanoenroll_username, user1)
|
||||
require.Equal(t, userUUID1, fetchedUserUUID1)
|
||||
}
|
||||
|
||||
func testGetLatestAppleMDMCommandOfType(t *testing.T, ds *Datastore) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Fails if host does not have a record
|
||||
_, err := ds.GetLatestAppleMDMCommandOfType(ctx, "non-existing-uuid", "DeviceLock")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, sql.ErrNoRows))
|
||||
|
||||
// Nano enroll a single device
|
||||
realHostUUID := uuid.NewString()
|
||||
host := &fleet.Host{
|
||||
UUID: realHostUUID,
|
||||
HardwareSerial: "serial",
|
||||
Platform: "darwin",
|
||||
TeamID: nil,
|
||||
}
|
||||
nanoEnroll(t, ds, host, false)
|
||||
|
||||
// Insert one record
|
||||
deviceLockCommandUUID := uuid.NewString()
|
||||
requestType := "DeviceLock"
|
||||
insertIntoNanoViewQueue(t, ds, host.UUID, deviceLockCommandUUID, requestType)
|
||||
|
||||
// Fails if host does exist but not request type
|
||||
_, err = ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, "EnableLostMode")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, sql.ErrNoRows))
|
||||
|
||||
// Succeeds if host and request type exist
|
||||
cmd, err := ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, requestType)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, deviceLockCommandUUID, cmd.CommandUUID)
|
||||
require.Equal(t, requestType, cmd.RequestType)
|
||||
}
|
||||
|
||||
// insertIntoNanoViewQueue is a helper function that populates the entries that nano_view_queue is made up of.
|
||||
func insertIntoNanoViewQueue(t *testing.T, ds *Datastore, hostUUID, commandUUID, requestType string) {
|
||||
ctx := t.Context()
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
// Insert into nano_commands
|
||||
_, err := q.ExecContext(ctx, `INSERT INTO nano_commands (command_uuid, request_type, command, subtype) VALUES (?, ?, '<?xml', 'None')`, commandUUID, requestType)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert into nano_enrollment_queue
|
||||
_, err = q.ExecContext(ctx, `INSERT INTO nano_enrollment_queue (id, command_uuid, active, priority) VALUES (?, ?, 1, 0)`, hostUUID, commandUUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert into nano_command_results
|
||||
_, err = q.ExecContext(ctx, `INSERT INTO nano_command_results (id, command_uuid, status, result, not_now_tally) VALUES (?, ?, 'Acknowledged', '<?xml', 0)`, hostUUID, commandUUID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func testSetLockCommandForLostModeCheckin(t *testing.T, ds *Datastore) {
|
||||
ctx := t.Context()
|
||||
|
||||
hostID := uint(1)
|
||||
commandUUID := uuid.NewString()
|
||||
|
||||
// Insert successfully
|
||||
err := ds.SetLockCommandForLostModeCheckin(ctx, hostID, commandUUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fails if trying to insert on existing row
|
||||
err = ds.SetLockCommandForLostModeCheckin(ctx, hostID, commandUUID)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -2444,6 +2444,16 @@ type AndroidDatastore interface {
|
||||
// Returns a struct with the current installed software on the host (pre-mutations) plus all
|
||||
// mutations performed: what was inserted and what was removed.
|
||||
UpdateHostSoftware(ctx context.Context, hostID uint, software []Software) (*UpdateHostSoftwareDBResult, error)
|
||||
|
||||
// GetLatestAppleMDMCommandOfType retrieves the latest command of the given type for the host with the given UUID.
|
||||
// If no such command exists, not found error is returned
|
||||
//
|
||||
// Returns a subset of fields in the MDMCommand struct.
|
||||
GetLatestAppleMDMCommandOfType(ctx context.Context, hostUUID string, commandType string) (*MDMCommand, error)
|
||||
|
||||
// SetLockCommandForLostModeCheckin sets the lock reference for a lost mode check-in.
|
||||
// This is used when an iphone or ipados checks in after being deleted, with lost mode enabled.
|
||||
SetLockCommandForLostModeCheckin(ctx context.Context, hostID uint, commandUUID string) error
|
||||
}
|
||||
|
||||
// MDMAppleStore wraps nanomdm's storage and adds methods to deal with
|
||||
|
||||
@@ -398,6 +398,7 @@ func (svc *MDMAppleCommander) DeviceInformation(ctx context.Context, hostUUIDs [
|
||||
<string>OSVersion</string>
|
||||
<string>WiFiMAC</string>
|
||||
<string>ProductName</string>
|
||||
<string>IsMDMLostModeEnabled</string>
|
||||
</array>
|
||||
<key>RequestType</key>
|
||||
<string>DeviceInformation</string>
|
||||
|
||||
@@ -1453,6 +1453,10 @@ type ListHostMDMAndroidProfilesPendingInstallWithVersionFunc func(ctx context.Co
|
||||
|
||||
type GetAndroidPolicyRequestByUUIDFunc func(ctx context.Context, requestUUID string) (*fleet.MDMAndroidPolicyRequest, error)
|
||||
|
||||
type GetLatestAppleMDMCommandOfTypeFunc func(ctx context.Context, hostUUID string, commandType string) (*fleet.MDMCommand, error)
|
||||
|
||||
type SetLockCommandForLostModeCheckinFunc func(ctx context.Context, hostID uint, commandUUID string) error
|
||||
|
||||
type NewMDMAndroidConfigProfileFunc func(ctx context.Context, cp fleet.MDMAndroidConfigProfile) (*fleet.MDMAndroidConfigProfile, error)
|
||||
|
||||
type GetMDMAndroidConfigProfileFunc func(ctx context.Context, profileUUID string) (*fleet.MDMAndroidConfigProfile, error)
|
||||
@@ -3695,6 +3699,12 @@ type DataStore struct {
|
||||
GetAndroidPolicyRequestByUUIDFunc GetAndroidPolicyRequestByUUIDFunc
|
||||
GetAndroidPolicyRequestByUUIDFuncInvoked bool
|
||||
|
||||
GetLatestAppleMDMCommandOfTypeFunc GetLatestAppleMDMCommandOfTypeFunc
|
||||
GetLatestAppleMDMCommandOfTypeFuncInvoked bool
|
||||
|
||||
SetLockCommandForLostModeCheckinFunc SetLockCommandForLostModeCheckinFunc
|
||||
SetLockCommandForLostModeCheckinFuncInvoked bool
|
||||
|
||||
NewMDMAndroidConfigProfileFunc NewMDMAndroidConfigProfileFunc
|
||||
NewMDMAndroidConfigProfileFuncInvoked bool
|
||||
|
||||
@@ -8847,6 +8857,20 @@ func (s *DataStore) GetAndroidPolicyRequestByUUID(ctx context.Context, requestUU
|
||||
return s.GetAndroidPolicyRequestByUUIDFunc(ctx, requestUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetLatestAppleMDMCommandOfType(ctx context.Context, hostUUID string, commandType string) (*fleet.MDMCommand, error) {
|
||||
s.mu.Lock()
|
||||
s.GetLatestAppleMDMCommandOfTypeFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetLatestAppleMDMCommandOfTypeFunc(ctx, hostUUID, commandType)
|
||||
}
|
||||
|
||||
func (s *DataStore) SetLockCommandForLostModeCheckin(ctx context.Context, hostID uint, commandUUID string) error {
|
||||
s.mu.Lock()
|
||||
s.SetLockCommandForLostModeCheckinFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.SetLockCommandForLostModeCheckinFunc(ctx, hostID, commandUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) NewMDMAndroidConfigProfile(ctx context.Context, cp fleet.MDMAndroidConfigProfile) (*fleet.MDMAndroidConfigProfile, error) {
|
||||
s.mu.Lock()
|
||||
s.NewMDMAndroidConfigProfileFuncInvoked = true
|
||||
|
||||
@@ -4079,6 +4079,7 @@ func (svc *MDMAppleCheckinAndCommandService) handleRefetchDeviceResults(ctx cont
|
||||
wifiMac = wifiMacVal.(string)
|
||||
}
|
||||
productName := deviceInformationResponse.QueryResponses["ProductName"].(string)
|
||||
isLostModeEnabled := deviceInformationResponse.QueryResponses["IsMDMLostModeEnabled"].(bool)
|
||||
host.ComputerName = deviceName
|
||||
host.Hostname = deviceName
|
||||
host.GigsDiskSpaceAvailable = availableDeviceCapacity
|
||||
@@ -4120,7 +4121,28 @@ func (svc *MDMAppleCheckinAndCommandService) handleRefetchDeviceResults(ctx cont
|
||||
if err := svc.ds.UpdateMDMData(ctx, host.ID, true); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "failed to update MDM data")
|
||||
}
|
||||
|
||||
// We run this check here as we only want to run it on re-check ins for deleted hosts.
|
||||
if (platform == "ios" || platform == "ipados") && isLostModeEnabled {
|
||||
fmt.Println("===lost mode enabled on iPhone/iPad, checking for lock command record", host.UUID)
|
||||
cmd, err := svc.ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, "EnableLostMode")
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return nil, ctxerr.Wrap(ctx, err, "check for existing EnableLostMode command")
|
||||
}
|
||||
if fleet.IsNotFound(err) {
|
||||
// Device is in lost mode, but we do not have a lock command record for it.
|
||||
// Lost mode was enabled outside of Fleet?
|
||||
return nil, ctxerr.NewWithData(ctx, "device is in lost mode but no EnableLostMode command record found", map[string]interface{}{"host_uuid": host.UUID})
|
||||
}
|
||||
|
||||
level.Debug(svc.logger).Log("msg", "device is in lost mode and EnableLostMode command record found, updating host lock/wipe status", "host_uuid", host.UUID)
|
||||
err = svc.ds.SetLockCommandForLostModeCheckin(ctx, host.ID, cmd.CommandUUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "update host lost mode status on refetch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4479,14 +4479,18 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
|
||||
hostID := uint(42)
|
||||
hostUUID := "ABC-DEF-GHI"
|
||||
commandUUID := fleet.RefetchDeviceCommandUUIDPrefix + "UUID"
|
||||
lostModeCommandUUID := uuid.NewString()
|
||||
|
||||
ds := new(mock.Store)
|
||||
svc := MDMAppleCheckinAndCommandService{ds: ds}
|
||||
svc := MDMAppleCheckinAndCommandService{ds: ds, logger: kitlog.NewNopLogger()}
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
return &fleet.Host{
|
||||
ID: hostID,
|
||||
UUID: hostUUID,
|
||||
MDM: fleet.MDMHostData{
|
||||
EnrollmentStatus: ptr.String("Pending"), // We check it in as a new device, to trigger lost mode flow
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
||||
@@ -4498,15 +4502,15 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
|
||||
require.WithinDuration(t, time.Now(), host.DetailUpdatedAt, 1*time.Minute)
|
||||
return nil
|
||||
}
|
||||
ds.SetOrUpdateHostDisksSpaceFunc = func(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error {
|
||||
require.Equal(t, hostID, hostID)
|
||||
ds.SetOrUpdateHostDisksSpaceFunc = func(ctx context.Context, incomingHostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error {
|
||||
require.Equal(t, hostID, incomingHostID)
|
||||
require.NotZero(t, 51, int64(gigsAvailable))
|
||||
require.NotZero(t, 79, int64(percentAvailable))
|
||||
require.NotZero(t, 64, int64(gigsTotal))
|
||||
return nil
|
||||
}
|
||||
ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error {
|
||||
require.Equal(t, hostID, hostID)
|
||||
ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, incomingHostID uint, hostOS fleet.OperatingSystem) error {
|
||||
require.Equal(t, hostID, incomingHostID)
|
||||
require.Equal(t, "iPadOS", hostOS.Name)
|
||||
require.Equal(t, "17.5.1", hostOS.Version)
|
||||
require.Equal(t, "ipados", hostOS.Platform)
|
||||
@@ -4517,6 +4521,22 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
|
||||
assert.Equal(t, fleet.RefetchDeviceCommandUUIDPrefix, command.CommandType)
|
||||
return nil
|
||||
}
|
||||
ds.UpdateMDMDataFunc = func(ctx context.Context, incomingHostID uint, enrolled bool) error {
|
||||
require.Equal(t, hostID, incomingHostID)
|
||||
return nil
|
||||
}
|
||||
ds.GetLatestAppleMDMCommandOfTypeFunc = func(ctx context.Context, incomingHostUUID, commandType string) (*fleet.MDMCommand, error) {
|
||||
require.Equal(t, hostUUID, incomingHostUUID)
|
||||
require.Equal(t, "EnableLostMode", commandType)
|
||||
return &fleet.MDMCommand{
|
||||
CommandUUID: lostModeCommandUUID,
|
||||
}, nil
|
||||
}
|
||||
ds.SetLockCommandForLostModeCheckinFunc = func(ctx context.Context, incomingHostUUID uint, commandUUID string) error {
|
||||
require.Equal(t, hostID, incomingHostUUID)
|
||||
require.Equal(t, lostModeCommandUUID, commandUUID)
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := svc.CommandAndReportResults(
|
||||
&mdm.Request{Context: ctx},
|
||||
@@ -4543,6 +4563,8 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
|
||||
<string>iPad13,18</string>
|
||||
<key>WiFiMAC</key>
|
||||
<string>ff:ff:ff:ff:ff:ff</string>
|
||||
<key>IsMDMLostModeEnabled</key>
|
||||
<true />
|
||||
</dict>
|
||||
<key>Status</key>
|
||||
<string>Acknowledged</string>
|
||||
@@ -4559,6 +4581,9 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
|
||||
require.True(t, ds.SetOrUpdateHostDisksSpaceFuncInvoked)
|
||||
require.True(t, ds.UpdateHostOperatingSystemFuncInvoked)
|
||||
assert.True(t, ds.RemoveHostMDMCommandFuncInvoked)
|
||||
require.True(t, ds.UpdateMDMDataFuncInvoked)
|
||||
require.True(t, ds.GetLatestAppleMDMCommandOfTypeFuncInvoked)
|
||||
require.True(t, ds.SetLockCommandForLostModeCheckinFuncInvoked)
|
||||
}
|
||||
|
||||
func TestUnmarshalAppList(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user