diff --git a/changes/29650-turn-off-mdm-on-device-token-inactive-refetcher b/changes/29650-turn-off-mdm-on-device-token-inactive-refetcher new file mode 100644 index 0000000000..5848d41e93 --- /dev/null +++ b/changes/29650-turn-off-mdm-on-device-token-inactive-refetcher @@ -0,0 +1 @@ +- Turn off MDM for iOS and iPadOS devices when refetcher returns device token is inactive \ No newline at end of file diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index a4e5715148..2680b1f113 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -1414,7 +1414,11 @@ func IOSiPadOSRefetch(ctx context.Context, ds fleet.Datastore, commander *MDMApp } if len(installedAppsUUIDs) > 0 { err = commander.InstalledApplicationList(ctx, installedAppsUUIDs, fleet.RefetchAppsCommandUUIDPrefix+commandUUID, false) - if err != nil { + turnedOff, turnedOffError := turnOffMDMIfAPNSFailed(ctx, ds, err, logger) + if turnedOffError != nil { + return turnedOffError + } + if err != nil && !turnedOff { return ctxerr.Wrap(ctx, err, "send InstalledApplicationList commands to ios and ipados devices") } } @@ -1431,7 +1435,11 @@ func IOSiPadOSRefetch(ctx context.Context, ds fleet.Datastore, commander *MDMApp } if len(certsListUUIDs) > 0 { err = commander.CertificateList(ctx, certsListUUIDs, fleet.RefetchCertsCommandUUIDPrefix+commandUUID) - if err != nil { + turnedOff, turnedOffError := turnOffMDMIfAPNSFailed(ctx, ds, err, logger) + if turnedOffError != nil { + return turnedOffError + } + if err != nil && !turnedOff { return ctxerr.Wrap(ctx, err, "send CertificateList commands to ios and ipados devices") } } @@ -1448,7 +1456,12 @@ func IOSiPadOSRefetch(ctx context.Context, ds fleet.Datastore, commander *MDMApp } } if len(deviceInfoUUIDs) > 0 { - if err := commander.DeviceInformation(ctx, deviceInfoUUIDs, fleet.RefetchDeviceCommandUUIDPrefix+commandUUID); err != nil { + err := commander.DeviceInformation(ctx, deviceInfoUUIDs, fleet.RefetchDeviceCommandUUIDPrefix+commandUUID) + turnedOff, turnedOffError := turnOffMDMIfAPNSFailed(ctx, ds, err, logger) + if turnedOffError != nil { + return turnedOffError + } + if err != nil && !turnedOff { return ctxerr.Wrap(ctx, err, "send DeviceInformation commands to ios and ipados devices") } } @@ -1461,6 +1474,25 @@ func IOSiPadOSRefetch(ctx context.Context, ds fleet.Datastore, commander *MDMApp return nil } +// turnOffMDMIfAPNSFailed checks if the error is an APNSDeliveryError and turns off MDM for the failed devices. +// Returns a boolean value to indicate whether or not MDM was turned off. +func turnOffMDMIfAPNSFailed(ctx context.Context, ds fleet.Datastore, err error, logger kitlog.Logger) (bool, error) { + var e *APNSDeliveryError + if !errors.As(err, &e) { + return false, nil + } + + for uuid, err := range e.errorsByUUID { + if strings.Contains(err.Error(), "device token is inactive") { + level.Info(logger).Log("msg", "turning off MDM for device with inactive device token", "uuid", uuid) + if err := ds.MDMTurnOff(ctx, uuid); err != nil { + return false, ctxerr.Wrap(ctx, err, "turn off mdm for failed device") + } + } + } + return true, nil +} + func GenerateOTAEnrollmentProfileMobileconfig(orgName, fleetURL, enrollSecret, idpUUID string) ([]byte, error) { path, err := url.JoinPath(fleetURL, "/api/v1/fleet/ota_enrollment") if err != nil { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 12b29137fe..bd1f8f3f66 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -17799,3 +17799,113 @@ func (s *integrationMDMTestSuite) TestBYODEnrollmentWithIdPEnabled() { require.NotEmpty(t, location) require.True(t, strings.HasPrefix(location, "http://localhost:9080/simplesaml/")) } + +func (s *integrationMDMTestSuite) TestIOSiPadOSRefetch() { + ctx := s.T().Context() + + successfulPushUUID := "successful-uuid" + failedPushUUID := "failed-uuid" + + // Set up test data + + // Perform the MDM enrollment. + mdmEnrollInfo := mdmtest.AppleEnrollInfo{ + SCEPChallenge: s.scepChallenge, + SCEPURL: s.server.URL + apple_mdm.SCEPPath, + MDMURL: s.server.URL + apple_mdm.MDMPath, + } + model := "iPhone14,6" + + successfulHost, err := s.ds.NewHost(ctx, &fleet.Host{ + HardwareSerial: mdmtest.RandSerialNumber(), + UUID: successfulPushUUID, + Platform: string(fleet.IOSPlatform), + DetailUpdatedAt: time.Now().Add(-2 * time.Hour), // ensure refetch is needed + }) + require.NoError(s.T(), err) + + successfulMdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model) + successfulMdmDevice.SerialNumber = successfulHost.HardwareSerial + err = successfulMdmDevice.Enroll() + require.NoError(s.T(), err) + + failedHost, err := s.ds.NewHost(ctx, &fleet.Host{ + HardwareSerial: mdmtest.RandSerialNumber(), + UUID: failedPushUUID, + Platform: string(fleet.IOSPlatform), + DetailUpdatedAt: time.Now().Add(-2 * time.Hour), // ensure refetch is needed + }) + require.NoError(s.T(), err) + + failedMdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model) + failedMdmDevice.SerialNumber = failedHost.HardwareSerial + err = failedMdmDevice.Enroll() + require.NoError(s.T(), err) + + failedHostTokenInactive, err := s.ds.NewHost(ctx, &fleet.Host{ + HardwareSerial: mdmtest.RandSerialNumber(), + UUID: failedPushUUID, + Platform: string(fleet.IOSPlatform), + DetailUpdatedAt: time.Now().Add(-2 * time.Hour), // ensure refetch is needed + }) + require.NoError(s.T(), err) + + failedMdmDeviceTokenInactive := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model) + failedMdmDeviceTokenInactive.SerialNumber = failedHostTokenInactive.HardwareSerial + err = failedMdmDeviceTokenInactive.Enroll() + require.NoError(s.T(), err) + + originalPushFunc := s.pushProvider.PushFunc + s.T().Cleanup(func() { + s.pushProvider.PushFunc = originalPushFunc + }) + s.pushProvider.PushFunc = func(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) { + require.Len(s.T(), pushes, 1) + pushObject := pushes[0] + switch pushObject.PushMagic { + case "pushmagic" + successfulMdmDevice.SerialNumber: + return map[string]*push.Response{ + pushObject.Token.String(): { + Id: successfulPushUUID, + Err: nil, + }, + }, nil + case "pushmagic" + failedMdmDeviceTokenInactive.SerialNumber: + return map[string]*push.Response{ + pushObject.Token.String(): { + Id: failedPushUUID, + Err: errors.New("device token is inactive"), + }, + }, nil + case "pushmagic" + failedMdmDevice.SerialNumber: + return map[string]*push.Response{ + pushObject.Token.String(): { + Id: failedPushUUID, + Err: errors.New("random apns error"), + }, + }, nil + } + return nil, errors.New("unknown device") + } + + err = apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger) + require.NoError(s.T(), err) // Verify it not longer throws an error + + // Verify successful is still enrolled + successfulHostMDM, err := s.ds.GetHostMDM(ctx, successfulHost.ID) + require.NoError(s.T(), err) + require.NotNil(s.T(), successfulHostMDM) + require.True(s.T(), successfulHostMDM.Enrolled) + + // Verify random APNS error host is still enrolled + failedHostMDM, err := s.ds.GetHostMDM(ctx, failedHost.ID) + require.NoError(s.T(), err) + require.NotNil(s.T(), failedHostMDM) + require.True(s.T(), failedHostMDM.Enrolled) + + // Verify device token inactive host is no longer enrolled + failedHostMDMTokenInactive, err := s.ds.GetHostMDM(ctx, failedHostTokenInactive.ID) + require.NoError(s.T(), err) + require.NotNil(s.T(), failedHostMDMTokenInactive) + require.False(s.T(), failedHostMDMTokenInactive.Enrolled) +}