Check for device token inactive error in refetcher and turn off MDM (#33027)

fixes: #29650

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [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/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests

- [ ] QA'd all new/changed functionality manually (Not possible for me
to get it into the same state, I just never get a response from my
device, but never the device token is inactive, I think that might be a
very long time process?, but verified that if the error comes back it
successfully turns off MDM)
This commit is contained in:
Magnus Jensen 2025-09-18 09:58:31 +03:00 committed by GitHub
parent 4909907edd
commit 0b87656438
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 146 additions and 3 deletions

View file

@ -0,0 +1 @@
- Turn off MDM for iOS and iPadOS devices when refetcher returns device token is inactive

View file

@ -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 {

View file

@ -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)
}