fix: show all Apple MDM profile errors in Fleet (#17070)

> Related issue: https://github.com/fleetdm/fleet/issues/16853

# Checklist for submitter

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

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Jahziel Villasana-Espinoza 2024-02-23 14:32:48 -05:00 committed by GitHub
parent 290ffd48d7
commit 2c93c21889
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 101 additions and 36 deletions

View File

@ -0,0 +1 @@
- Fixes an issue where some MDM profile installation errors would not be shown in Fleet.

View File

@ -1938,7 +1938,7 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profile *fleet.HostMDMAppleProfile) error {
if profile.OperationType == fleet.MDMOperationTypeRemove &&
profile.Status != nil &&
(*profile.Status == fleet.MDMDeliveryVerifying || *profile.Status == fleet.MDMDeliveryVerified || profile.IgnoreMDMClientError()) {
(*profile.Status == fleet.MDMDeliveryVerifying || *profile.Status == fleet.MDMDeliveryVerified) {
_, err := ds.writer(ctx).ExecContext(ctx, `
DELETE FROM host_mdm_apple_profiles
WHERE host_uuid = ? AND command_uuid = ?
@ -1946,11 +1946,17 @@ func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, prof
return err
}
detail := profile.Detail
if profile.OperationType == fleet.MDMOperationTypeRemove && profile.Status != nil && *profile.Status == fleet.MDMDeliveryFailed {
detail = fmt.Sprintf("Failed to remove: %s", detail)
}
_, err := ds.writer(ctx).ExecContext(ctx, `
UPDATE host_mdm_apple_profiles
SET status = ?, operation_type = ?, detail = ?
WHERE host_uuid = ? AND command_uuid = ?
`, profile.Status, profile.OperationType, profile.Detail, profile.HostUUID, profile.CommandUUID)
`, profile.Status, profile.OperationType, detail, profile.HostUUID, profile.CommandUUID)
return err
}

View File

@ -53,7 +53,7 @@ func TestMDMApple(t *testing.T) {
{"TestAggregateMacOSSettingsStatusWithFileVault", testAggregateMacOSSettingsStatusWithFileVault},
{"TestMDMAppleHostsProfilesStatus", testMDMAppleHostsProfilesStatus},
{"TestMDMAppleIdPAccount", testMDMAppleIdPAccount},
{"TestIgnoreMDMClientError", testIgnoreMDMClientError},
{"TestIgnoreMDMClientError", testDoNotIgnoreMDMClientError},
{"TestDeleteMDMAppleProfilesForHost", testDeleteMDMAppleProfilesForHost},
{"TestGetMDMAppleCommandResults", testGetMDMAppleCommandResults},
{"TestBulkUpsertMDMAppleConfigProfiles", testBulkUpsertMDMAppleConfigProfile},
@ -2294,7 +2294,7 @@ func testMDMAppleIdPAccount(t *testing.T, ds *Datastore) {
require.Nil(t, out)
}
func testIgnoreMDMClientError(t *testing.T, ds *Datastore) {
func testDoNotIgnoreMDMClientError(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create new record for remove pending
@ -2326,7 +2326,12 @@ func testIgnoreMDMClientError(t *testing.T, ds *Datastore) {
}))
cps, err = ds.GetHostMDMAppleProfiles(ctx, "h1")
require.NoError(t, err)
require.Len(t, cps, 0) // we ignore error code 89 and delete the pending record as well
require.Len(t, cps, 1) // we no longer ignore error code 89
require.Equal(t, "name1", cps[0].Name)
require.Equal(t, fleet.MDMOperationTypeRemove, cps[0].OperationType)
require.NotNil(t, cps[0].Status)
require.Equal(t, fleet.MDMDeliveryFailed, *cps[0].Status)
require.Equal(t, "Failed to remove: MDMClientError (89): Profile with identifier 'p1' not found.", cps[0].Detail)
// create another new record
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
@ -2362,7 +2367,7 @@ func testIgnoreMDMClientError(t *testing.T, ds *Datastore) {
require.Equal(t, fleet.MDMOperationTypeRemove, cps[0].OperationType)
require.NotNil(t, cps[0].Status)
require.Equal(t, fleet.MDMDeliveryFailed, *cps[0].Status)
require.Equal(t, "MDMClientError (96): Cannot replace profile 'p2' because it was not installed by the MDM server.", cps[0].Detail)
require.Equal(t, "Failed to remove: MDMClientError (96): Cannot replace profile 'p2' because it was not installed by the MDM server.", cps[0].Detail)
}
func testDeleteMDMAppleProfilesForHost(t *testing.T, ds *Datastore) {
@ -3751,7 +3756,7 @@ func testSetVerifiedMacOSProfiles(t *testing.T, ds *Datastore) {
InstallDate: time.Now(),
},
})))
expectedHostMDMStatus[hosts[2].ID][cp1.Identifier] = fleet.MDMDeliveryVerified //cp1 can go from pending to verified
expectedHostMDMStatus[hosts[2].ID][cp1.Identifier] = fleet.MDMDeliveryVerified // cp1 can go from pending to verified
expectedHostMDMStatus[hosts[2].ID][cp3.Identifier] = fleet.MDMDeliveryPending // first retry for cp3
expectedHostMDMStatus[hosts[2].ID][cp4.Identifier] = fleet.MDMDeliveryPending // first retry for cp4
checkHostMDMProfileStatuses()

View File

@ -277,17 +277,6 @@ func (p HostMDMAppleProfile) ToHostMDMProfile() HostMDMProfile {
}
}
func (p HostMDMAppleProfile) IgnoreMDMClientError() bool {
switch p.OperationType {
case MDMOperationTypeRemove:
switch {
case strings.Contains(p.Detail, "MDMClientError (89)"):
return true
}
}
return false
}
type HostMDMProfileDetail string
const (

View File

@ -318,24 +318,6 @@ func mcPayloadContentForTest(refs []string) string {
return formatted
}
func TestHostMDMAppleProfileIgnoreClientError(t *testing.T) {
require.True(t, HostMDMAppleProfile{
CommandUUID: "c1",
HostUUID: "h1",
Status: &MDMDeliveryFailed,
Detail: "MDMClientError (89): Profile with identifier 'p1' not found.",
OperationType: MDMOperationTypeRemove,
}.IgnoreMDMClientError())
require.False(t, HostMDMAppleProfile{
CommandUUID: "c1",
HostUUID: "h1",
Status: &MDMDeliveryFailed,
Detail: "MDMClientError (96): Cannot replace profile 'p2' because it was not installed by the MDM server.",
OperationType: MDMOperationTypeRemove,
}.IgnoreMDMClientError())
}
func TestHostDEPAssignment(t *testing.T) {
cases := []struct {
testName string

View File

@ -11333,6 +11333,88 @@ func (s *integrationMDMTestSuite) TestGetManualEnrollmentProfile() {
s.downloadAndVerifyEnrollmentProfile("/api/latest/fleet/mdm/manual_enrollment_profile")
}
func (s *integrationMDMTestSuite) TestDontIgnoreAnyProfileErrors() {
t := s.T()
ctx := context.Background()
// Create a host and a couple of profiles
host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
globalProfiles := [][]byte{
mobileconfigForTest("N1", "I1"),
mobileconfigForTest("N2", "I2"),
}
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
s.awaitTriggerProfileSchedule(t)
// The profiles should be associated with the host we made + the standard fleet config
profs, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
require.NoError(t, err)
require.Len(t, profs, 3)
// Acknowledge the profiles so we can mark them as verified
cmd, err := mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
}
require.NoError(t, apple_mdm.VerifyHostMDMProfiles(context.Background(), s.ds, host, map[string]*fleet.HostMacOSProfile{
"I1": {Identifier: "I1", DisplayName: "I1", InstallDate: time.Now()},
"I2": {Identifier: "I2", DisplayName: "I2", InstallDate: time.Now()},
mobileconfig.FleetdConfigPayloadIdentifier: {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, DisplayName: "I2", InstallDate: time.Now()},
}))
// Check that the profile is marked as verified when fetching the host
getHostResp := getHostResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host.MDM.Profiles)
for _, hm := range *getHostResp.Host.MDM.Profiles {
require.Equal(t, fleet.MDMDeliveryVerified, *hm.Status)
}
// remove the profiles
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{}, http.StatusNoContent)
s.awaitTriggerProfileSchedule(t)
// On the host side, return errors for the two profile removal actions
cmd, err = mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
if cmd.Command.RequestType == "RemoveProfile" {
var errChain []mdm.ErrorChain
if cmd.Command.RemoveProfile.Identifier == "I1" {
errChain = append(errChain, mdm.ErrorChain{ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'I1' not found."})
} else {
errChain = append(errChain, mdm.ErrorChain{ErrorCode: 96, ErrorDomain: "MDMClientError", USEnglishDescription: "Cannot replace profile 'I2' because it was not installed by the MDM server."})
}
cmd, err = mdmDevice.Err(cmd.CommandUUID, errChain)
require.NoError(t, err)
continue
}
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
}
// get that host - it should report "failed" for the profiles and include the error message detail
expectedErrs := map[string]string{
"N1": "Failed to remove: MDMClientError (89): Profile with identifier 'I1' not found.\n",
"N2": "Failed to remove: MDMClientError (96): Cannot replace profile 'I2' because it was not installed by the MDM server.\n",
}
getHostResp = getHostResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
for _, hm := range *getHostResp.Host.MDM.Profiles {
if wantErr, ok := expectedErrs[hm.Name]; ok {
require.Equal(t, fleet.MDMDeliveryFailed, *hm.Status)
require.Equal(t, wantErr, hm.Detail)
continue
}
require.Equal(t, fleet.MDMDeliveryVerified, *hm.Status)
}
}
func (s *integrationMDMTestSuite) TestSCEPCertExpiration() {
t := s.T()
ctx := context.Background()