diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index bdbc354f9e..f302644ee0 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -1060,6 +1060,14 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload return err } +func (ds *Datastore) DeleteMDMAppleProfilesForHost(ctx context.Context, hostUUID string) error { + _, err := ds.writer.ExecContext(ctx, ` + DELETE FROM host_mdm_apple_profiles + WHERE host_uuid = ? + `, hostUUID) + return err +} + func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profile *fleet.HostMDMAppleProfile) error { if profile.OperationType == fleet.MDMAppleOperationTypeRemove && profile.Status != nil && (*profile.Status == fleet.MDMAppleDeliveryApplied || profile.IgnoreMDMClientError()) { diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index d71d433c5e..01cf12e840 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -37,6 +37,7 @@ func TestMDMAppleConfigProfile(t *testing.T) { {"TestMDMAppleHostsProfilesStatus", testMDMAppleHostsProfilesStatus}, {"TestMDMAppleInsertIdPAccount", testMDMAppleInsertIdPAccount}, {"TestIgnoreMDMClientError", testIgnoreMDMClientError}, + {"TestDeleteMDMAppleProfilesForHost", testDeleteMDMAppleProfilesForHost}, } for _, c := range cases { @@ -1577,3 +1578,38 @@ func testIgnoreMDMClientError(t *testing.T, ds *Datastore) { require.Equal(t, fleet.MDMAppleDeliveryFailed, *cps[0].Status) require.Equal(t, "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) { + ctx := context.Background() + h, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: ptr.String("host0-osquery-id"), + NodeKey: ptr.String("host0-node-key"), + UUID: "host0-test-mdm-profiles", + Hostname: "hostname0", + }) + require.NoError(t, err) + + require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{ + ProfileID: uint(1), + ProfileIdentifier: "p1", + ProfileName: "name1", + HostUUID: h.UUID, + CommandUUID: "c1", + OperationType: fleet.MDMAppleOperationTypeRemove, + Status: &fleet.MDMAppleDeliveryPending, + }})) + + gotProfs, err := ds.GetHostMDMProfiles(ctx, h.UUID) + require.NoError(t, err) + require.Len(t, gotProfs, 1) + + err = ds.DeleteMDMAppleProfilesForHost(ctx, h.UUID) + require.NoError(t, err) + gotProfs, err = ds.GetHostMDMProfiles(ctx, h.UUID) + require.NoError(t, err) + require.Nil(t, gotProfs) +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index ca659554f0..9c5c5dc6e6 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -817,6 +817,9 @@ type Datastore interface { // and the status is "applied" (i.e. successfully removed). UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profile *HostMDMAppleProfile) error + // DeleteMDMAppleProfilesForHost deletes all MDM profiles for a host + DeleteMDMAppleProfilesForHost(ctx context.Context, hostUUID string) error + // GetMDMAppleCommandRequest type returns the request type for the given command GetMDMAppleCommandRequestType(ctx context.Context, commandUUID string) (string, error) diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index fb1b975224..9cb643f6c7 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -566,6 +566,8 @@ type GetMDMAppleProfilesContentsFunc func(ctx context.Context, profileIDs []uint type UpdateOrDeleteHostMDMAppleProfileFunc func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error +type DeleteMDMAppleProfilesForHostFunc func(ctx context.Context, hostUUID string) error + type GetMDMAppleCommandRequestTypeFunc func(ctx context.Context, commandUUID string) (string, error) type GetMDMAppleHostsProfilesSummaryFunc func(ctx context.Context, teamID *uint) (*fleet.MDMAppleHostsProfilesSummary, error) @@ -1398,6 +1400,9 @@ type DataStore struct { UpdateOrDeleteHostMDMAppleProfileFunc UpdateOrDeleteHostMDMAppleProfileFunc UpdateOrDeleteHostMDMAppleProfileFuncInvoked bool + DeleteMDMAppleProfilesForHostFunc DeleteMDMAppleProfilesForHostFunc + DeleteMDMAppleProfilesForHostFuncInvoked bool + GetMDMAppleCommandRequestTypeFunc GetMDMAppleCommandRequestTypeFunc GetMDMAppleCommandRequestTypeFuncInvoked bool @@ -3335,6 +3340,13 @@ func (s *DataStore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profi return s.UpdateOrDeleteHostMDMAppleProfileFunc(ctx, profile) } +func (s *DataStore) DeleteMDMAppleProfilesForHost(ctx context.Context, hostUUID string) error { + s.mu.Lock() + s.DeleteMDMAppleProfilesForHostFuncInvoked = true + s.mu.Unlock() + return s.DeleteMDMAppleProfilesForHostFunc(ctx, hostUUID) +} + func (s *DataStore) GetMDMAppleCommandRequestType(ctx context.Context, commandUUID string) (string, error) { s.mu.Lock() s.GetMDMAppleCommandRequestTypeFuncInvoked = true diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 8a0dcfdbc1..add4516e56 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -1103,6 +1103,14 @@ func (svc *Service) EnqueueMDMAppleCommandRemoveEnrollmentProfile(ctx context.Co return ctxerr.Wrap(ctx, err, "enqueuing mdm apple remove profile command") } + // Since the host is unenrolled, delete all profiles assigned to the + // host manually, the device won't Acknowledge any more requests (eg: + // to delete profiles) and profiles are automatically removed on + // unenrollment. + if err := svc.ds.DeleteMDMAppleProfilesForHost(ctx, h.UUID); err != nil { + return ctxerr.Wrap(ctx, err, "removing all profiles from host") + } + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeMDMUnenrolled{ HostSerial: h.HardwareSerial, HostDisplayName: h.DisplayName(), diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index b9bedb9262..f24f8d028c 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -662,6 +662,10 @@ func TestMDMCommandAuthz(t *testing.T) { return nil } + ds.DeleteMDMAppleProfilesForHostFunc = func(ctx context.Context, hostUUID string) error { + return nil + } + var mdmEnabled atomic.Bool ds.GetNanoMDMEnrollmentStatusFunc = func(ctx context.Context, hostUUID string) (bool, error) { // This function is called twice during EnqueueMDMAppleCommandRemoveEnrollmentProfile. diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index d466253f85..2787cc7b35 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -778,6 +778,21 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() { require.Len(t, listHostsRes.Hosts, 1) h := listHostsRes.Hosts[0] + // assign profiles to the host + s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{ + mobileconfigForTest("N1", "I1"), + mobileconfigForTest("N2", "I2"), + mobileconfigForTest("N3", "I3"), + }}, http.StatusNoContent) + + // trigger a sync and verify that there are profiles assigned to the host + _, err = s.profileSchedule.Trigger() + require.NoError(t, err) + + var hostResp getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp) + require.Len(t, *hostResp.Host.MDM.Profiles, 3) + // try to unenroll the host, fails since the host doesn't respond s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusGatewayTimeout) @@ -815,6 +830,12 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() { return res, err } s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusOK) + + // profiles are removed and the host is no longer enrolled + hostResp = getHostResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp) + require.Nil(t, hostResp.Host.MDM.Profiles) + require.Equal(t, "", hostResp.Host.MDM.Name) } func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() {