diff --git a/changes/48845-dont-reconcile-profiles-for-non-mdm-enrolled-hosts b/changes/48845-dont-reconcile-profiles-for-non-mdm-enrolled-hosts new file mode 100644 index 0000000000..8248c8a5d3 --- /dev/null +++ b/changes/48845-dont-reconcile-profiles-for-non-mdm-enrolled-hosts @@ -0,0 +1 @@ +- Fixed an issue where the Apple reconciler would queue profiles for deleted hosts, that was pending in Fleet via Apple Business. \ No newline at end of file diff --git a/server/datastore/mysql/apple_mdm_batched.go b/server/datastore/mysql/apple_mdm_batched.go index 7f4132b169..2b801b5211 100644 --- a/server/datastore/mysql/apple_mdm_batched.go +++ b/server/datastore/mysql/apple_mdm_batched.go @@ -46,6 +46,9 @@ func (ds *Datastore) listAppleMDMHostsForReconcileBatchTransaction( WHERE (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND h.uuid > ? + AND EXISTS ( + SELECT 1 FROM host_mdm hmdm WHERE hmdm.enrolled = 1 AND hmdm.host_id = h.id + ) ORDER BY h.uuid, h.id DESC LIMIT ? ` diff --git a/server/datastore/mysql/apple_mdm_batched_test.go b/server/datastore/mysql/apple_mdm_batched_test.go new file mode 100644 index 0000000000..fa86022a53 --- /dev/null +++ b/server/datastore/mysql/apple_mdm_batched_test.go @@ -0,0 +1,103 @@ +package mysql + +import ( + "testing" + "time" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/stretchr/testify/require" +) + +func TestGetAppleProfileReconcileSnapshotChecksMDMStatus(t *testing.T) { + ds := CreateMySQLDS(t) + ctx := t.Context() + + // Both hosts are darwin and fully nano-enrolled (Device enrollment + + // nano_devices row), so the only thing that differs between them is their + // host_mdm.enrolled flag. This isolates the EXISTS(host_mdm ... enrolled = 1) + // filter in the reconcile query. + newEnrolledHost := func(suffix string, enrolled bool) *fleet.Host { + h, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: new("osquery-" + suffix), + NodeKey: new("nodekey-" + suffix), + UUID: "uuid-" + suffix, + Hostname: "hostname-" + suffix, + HardwareSerial: "serial-" + suffix, + Platform: "darwin", + }) + require.NoError(t, err) + + nanoEnroll(t, ds, h, false) + err = ds.SetOrUpdateMDMData(ctx, h.ID, false, enrolled, "https://example.com", true, fleet.WellKnownMDMFleet, "", false) + require.NoError(t, err) + + return h + } + + // host_mdm.enrolled = 1 -> should be returned. + enrolledHost := newEnrolledHost("enrolled", true) + // host_mdm.enrolled = 0 -> should NOT be returned. + notEnrolledHost := newEnrolledHost("not-enrolled", false) + + hosts, _, _, _, err := ds.GetAppleProfileReconcileSnapshot(ctx, "", 100) + require.NoError(t, err) + + gotUUIDs := make(map[string]struct{}, len(hosts)) + for _, h := range hosts { + gotUUIDs[h.UUID] = struct{}{} + } + + require.Contains(t, gotUUIDs, enrolledHost.UUID, "host with host_mdm.enrolled = 1 should be returned") + require.NotContains(t, gotUUIDs, notEnrolledHost.UUID, "host with host_mdm.enrolled = 0 should not be returned") +} + +func TestGetAppleMDMHostForReconcileIgnoresHostMDMStatus(t *testing.T) { + ds := CreateMySQLDS(t) + ctx := t.Context() + + // Both hosts are darwin and fully nano-enrolled (Device enrollment + + // nano_devices row), so the only thing that differs between them is their + // host_mdm.enrolled flag. + newEnrolledHost := func(suffix string, enrolled bool) *fleet.Host { + h, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: new("osquery-" + suffix), + NodeKey: new("nodekey-" + suffix), + UUID: "uuid-" + suffix, + Hostname: "hostname-" + suffix, + HardwareSerial: "serial-" + suffix, + Platform: "darwin", + }) + require.NoError(t, err) + + nanoEnroll(t, ds, h, false) + err = ds.SetOrUpdateMDMData(ctx, h.ID, false, enrolled, "https://example.com", true, fleet.WellKnownMDMFleet, "", false) + require.NoError(t, err) + + return h + } + + // host_mdm.enrolled = 1 -> should be returned. + enrolledHost := newEnrolledHost("enrolled", true) + + info, err := ds.GetAppleMDMHostForReconcile(ctx, enrolledHost.UUID) + require.NoError(t, err) + + require.NotNil(t, info) + require.Equal(t, enrolledHost.UUID, info.UUID) + + // host_mdm.enrolled = 0 -> should also be returned for enrolling hosts + notEnrolledHost := newEnrolledHost("not-enrolled", false) + info, err = ds.GetAppleMDMHostForReconcile(ctx, notEnrolledHost.UUID) + require.NoError(t, err) + + require.NotNil(t, info) + require.Equal(t, notEnrolledHost.UUID, info.UUID) +} diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index c6522bea0c..c17f94cb87 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -6641,6 +6641,8 @@ func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) { Platform: "darwin", }) require.NoError(t, err) + err = ds.SetOrUpdateMDMData(ctx, host1.ID, false, true, "https://example.com", true, fleet.WellKnownMDMFleet, "", false) + require.NoError(t, err) nanoEnroll(t, ds, host1, true) require.NoError(t, service.ReconcileAppleDeclarationsBatched(ctx, ds, commander, ds.logger)) @@ -6985,6 +6987,8 @@ func testDeleteMDMAppleDeclarationWithPendingInstalls(t *testing.T, ds *Datastor Platform: "darwin", }) require.NoError(t, err) + err = ds.SetOrUpdateMDMData(ctx, host.ID, false, true, "https://example.com", true, fleet.WellKnownMDMFleet, "", false) + require.NoError(t, err) nanoEnroll(t, ds, host, true) commander, _ := createMDMAppleCommanderAndStorage(t, ds) @@ -8259,6 +8263,8 @@ func testReconcileAppleProfilesDuplicateHostUUID(t *testing.T, ds *Datastore) { hLow := test.NewHost(t, ds, "dup-low", "1.1.1.1", "dup-key-low", sharedUUID, now) hHigh := test.NewHost(t, ds, "dup-high", "1.1.1.2", "dup-key-high", sharedUUID, now) require.Greater(t, hHigh.ID, hLow.ID) + err := ds.SetOrUpdateMDMData(ctx, hHigh.ID, false, true, "https://example.com", true, fleet.WellKnownMDMFleet, "", false) + require.NoError(t, err) nanoEnroll(t, ds, hHigh, false) // Source dedup: the reconcile snapshot must surface the UUID exactly once, diff --git a/server/datastore/mysql/custom_host_vitals_test.go b/server/datastore/mysql/custom_host_vitals_test.go index 76141fb8ce..11def1450a 100644 --- a/server/datastore/mysql/custom_host_vitals_test.go +++ b/server/datastore/mysql/custom_host_vitals_test.go @@ -602,6 +602,8 @@ func testReconcileSnapshotMarksVitalDeclarations(t *testing.T, ds *Datastore) { // A macOS host must be MDM-enrolled to enter the reconcile window; otherwise // the snapshot skips loading declarations entirely. host := test.NewHost(t, ds, "macos-1", "1", "macos-1-key", "macos-1-uuid", time.Now()) + err := ds.SetOrUpdateMDMData(ctx, host.ID, false, true, "https://example.com", true, fleet.WellKnownMDMFleet, "", false) + require.NoError(t, err) nanoEnroll(t, ds, host, false) vitalID := createCustomHostVital(t, ds, "FUNCTION")