Don't queue profiles for non host_mdm.enrolled Apple hosts (#49611)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #48845 

# 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**
* Apple MDM reconciliation no longer queues profiles for deleted or
non–MDM-enrolled hosts.
* Apple MDM reconciliation batching/snapshots now include only hosts
with confirmed active MDM enrollment, reducing incorrect or stale
reconciliation candidates.
* **Tests**
* Added MySQL datastore coverage to validate reconcile snapshot
selection and reconcile host lookup behavior.
* Improved Apple MDM and related test setups to explicitly ensure
required MDM server configuration exists before reconciliation
assertions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Magnus Jensen
2026-07-21 21:33:29 +02:00
committed by GitHub
parent 647ab1863c
commit 966838b159
5 changed files with 115 additions and 0 deletions
@@ -0,0 +1 @@
- Fixed an issue where the Apple reconciler would queue profiles for deleted hosts, that was pending in Fleet via Apple Business.
@@ -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 ?
`
@@ -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)
}
+6
View File
@@ -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,
@@ -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")