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:
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user