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:
Magnus Jensen
2025-10-15 17:24:40 -03:00
committed by GitHub
parent 3e7fde5fef
commit 8c4b5f9371
8 changed files with 185 additions and 5 deletions
+1
View File
@@ -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)
+27
View File
@@ -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")
}
+70
View File
@@ -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)
}
+10
View File
@@ -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
+1
View File
@@ -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>
+24
View File
@@ -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
+22
View File
@@ -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
}
+30 -5
View File
@@ -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) {