From 3371b48373f306c4c9e141c90aa63bbe35bd18bd Mon Sep 17 00:00:00 2001 From: Magnus Jensen Date: Tue, 7 Apr 2026 15:23:37 -0500 Subject: [PATCH] accept 89 error on RemoveProfile as valid (#43172) **Related issue:** Resolves #42103 # 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. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## Summary by CodeRabbit * **Bug Fixes** * Improved profile removal handling: Fleet now successfully removes host OS setting entries even when the removal command encounters a "profile not found" error from the device. --- changes/42103-accept-89-on-profile-removal | 1 + server/mdm/apple/util.go | 15 +++++++ server/mdm/apple/util_test.go | 50 ++++++++++++++++++++++ server/service/apple_mdm.go | 13 +++++- server/service/apple_mdm_test.go | 12 ++++++ 5 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 changes/42103-accept-89-on-profile-removal diff --git a/changes/42103-accept-89-on-profile-removal b/changes/42103-accept-89-on-profile-removal new file mode 100644 index 0000000000..263ca36df6 --- /dev/null +++ b/changes/42103-accept-89-on-profile-removal @@ -0,0 +1 @@ +* Fixed an issue, where Fleet would not remove the host OS setting entry if a RemoveProfile command failed with error code 89 (Profile not found on device). \ No newline at end of file diff --git a/server/mdm/apple/util.go b/server/mdm/apple/util.go index 8d9eb58634..47ff83e7cc 100644 --- a/server/mdm/apple/util.go +++ b/server/mdm/apple/util.go @@ -91,6 +91,21 @@ func IsRecoveryLockPasswordMismatchError(chain []mdm.ErrorChain) bool { return false } +// IsProfileNotFoundError checks if the error chain indicates that a profile +// was not found on the device. When this error occurs during a RemoveProfile +// command, it means the profile is already absent — the desired outcome. +// +// Known error signature: +// - MDMClientError (89): "Profile with identifier '...' not found." +func IsProfileNotFoundError(chain []mdm.ErrorChain) bool { + for _, e := range chain { + if e.ErrorDomain == "MDMClientError" && e.ErrorCode == 89 { + return true + } + } + return false +} + // FmtDDMError formats a DDM error message func FmtDDMError(reasons []fleet.MDMAppleDDMStatusErrorReason) string { var errMsg strings.Builder diff --git a/server/mdm/apple/util_test.go b/server/mdm/apple/util_test.go index ae9f58f04f..cb7257ed9b 100644 --- a/server/mdm/apple/util_test.go +++ b/server/mdm/apple/util_test.go @@ -46,6 +46,56 @@ func TestGenerateRandomPin(t *testing.T) { } } +func TestIsProfileNotFoundError(t *testing.T) { + cases := []struct { + name string + chain []mdm.ErrorChain + expected bool + }{ + { + name: "empty chain", + chain: nil, + expected: false, + }, + { + name: "MDMClientError 89 - profile not found", + chain: []mdm.ErrorChain{ + {ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'com.example' not found."}, + }, + expected: true, + }, + { + name: "different MDMClientError code", + chain: []mdm.ErrorChain{ + {ErrorCode: 90, ErrorDomain: "MDMClientError", USEnglishDescription: "Some other error"}, + }, + expected: false, + }, + { + name: "different error domain with code 89", + chain: []mdm.ErrorChain{ + {ErrorCode: 89, ErrorDomain: "SomeOtherDomain", USEnglishDescription: "Some error"}, + }, + expected: false, + }, + { + name: "profile not found in chain with other errors", + chain: []mdm.ErrorChain{ + {ErrorCode: 100, ErrorDomain: "SomeOtherDomain", USEnglishDescription: "First error"}, + {ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'com.example' not found."}, + }, + expected: true, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + result := IsProfileNotFoundError(tt.chain) + require.Equal(t, tt.expected, result) + }) + } +} + func TestIsRecoveryLockPasswordMismatchError(t *testing.T) { cases := []struct { name string diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 74b3ace296..73f4149894 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -3862,11 +3862,20 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ apple_mdm.FmtErrorChain(cmdResult.ErrorChain), ) case "RemoveProfile": + status := mdmAppleDeliveryStatusFromCommandStatus(cmdResult.Status) + detail := apple_mdm.FmtErrorChain(cmdResult.ErrorChain) + // MDMClientError 89 means "Profile not found" — for a removal, this + // is the desired outcome, so treat it as successful. + if status != nil && *status == fleet.MDMDeliveryFailed && + apple_mdm.IsProfileNotFoundError(cmdResult.ErrorChain) { + status = &fleet.MDMDeliveryVerifying + detail = "" + } return nil, svc.ds.UpdateOrDeleteHostMDMAppleProfile(r.Context, &fleet.HostMDMAppleProfile{ CommandUUID: cmdResult.CommandUUID, HostUUID: cmdResult.Identifier(), - Status: mdmAppleDeliveryStatusFromCommandStatus(cmdResult.Status), - Detail: apple_mdm.FmtErrorChain(cmdResult.ErrorChain), + Status: status, + Detail: detail, OperationType: fleet.MDMOperationTypeRemove, }) case "DeviceLock", "EraseDevice": diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index b27a3a219d..741f490713 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -2069,6 +2069,18 @@ func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) { OperationType: fleet.MDMOperationTypeRemove, }, }, + { + status: "Error", + requestType: "RemoveProfile", + errors: []mdm.ErrorChain{ + {ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'com.example' not found."}, + }, + want: &fleet.HostMDMAppleProfile{ + Status: &fleet.MDMDeliveryVerifying, + Detail: "", + OperationType: fleet.MDMOperationTypeRemove, + }, + }, } for i, c := range cases {