accept 89 error on RemoveProfile as valid (#43172)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Magnus Jensen 2026-04-07 15:23:37 -05:00 committed by GitHub
parent 82b6614b2b
commit 3371b48373
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 89 additions and 2 deletions

View file

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

View file

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

View file

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

View file

@ -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":

View file

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