diff --git a/pkg/mdm/mdmtest/apple.go b/pkg/mdm/mdmtest/apple.go index beda8cd9f6..57e20f7c49 100644 --- a/pkg/mdm/mdmtest/apple.go +++ b/pkg/mdm/mdmtest/apple.go @@ -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) diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 604d6fa1db..916a9efefe 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -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") +} diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index a0c64bae2c..f14616f941 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -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 (?, ?, 'OSVersion WiFiMAC ProductName + IsMDMLostModeEnabled RequestType DeviceInformation diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index eacb9ef4f4..5b2c74c4e2 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -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 diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 96e9261ef3..f75bca354d 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -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 } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index e81ddc9bf6..4caf6a2646 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -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) { iPad13,18 WiFiMAC ff:ff:ff:ff:ff:ff + IsMDMLostModeEnabled + Status Acknowledged @@ -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) {