45178 cpie reconciler query updates (#46889)
**Related issue:** Resolves #45178 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually. _Note: Only windows and Mac OS. Android required by someone with a device._ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved MDM profile applicability on Android, Windows, and Apple to handle combined include-all, include-any, and exclude-any label rules, yielding more accurate install/remove decisions. * **Tests** * Added cross-platform tests covering combined include/exclude label scenarios to verify correct profile selection, installation, and removal behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
632089ee55
commit
244cdd5558
@@ -1052,9 +1052,8 @@ const androidApplicableProfilesQuery = `
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based profiles where the host is a member of all the labels (include-all).
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
-- include-all only (no exclude labels): host must be a member of every include label.
|
||||
-- broken include labels disqualify the profile.
|
||||
SELECT
|
||||
macp.profile_uuid,
|
||||
macp.name,
|
||||
@@ -1077,6 +1076,10 @@ const androidApplicableProfilesQuery = `
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
h.platform = 'android' AND
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE android_profile_uuid = macp.profile_uuid AND exclude = 1
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
macp.profile_uuid, macp.name, h.uuid, h.id
|
||||
@@ -1085,11 +1088,8 @@ const androidApplicableProfilesQuery = `
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based entities where the host is NOT a member of any of the labels (exclude-any).
|
||||
-- explicitly ignore profiles with broken excluded labels so that they are never applied,
|
||||
-- and ignore profiles that depend on labels created _after_ the label_updated_at timestamp
|
||||
-- of the host (because we don't have results for that label yet, the host may or may not be
|
||||
-- a member).
|
||||
-- exclude-any only (no include labels): host must NOT be a member of any exclude label.
|
||||
-- broken or not-yet-scanned dynamic exclude labels disqualify the profile.
|
||||
SELECT
|
||||
macp.profile_uuid,
|
||||
macp.name,
|
||||
@@ -1099,10 +1099,6 @@ const androidApplicableProfilesQuery = `
|
||||
COUNT(*) as count_profile_labels,
|
||||
COUNT(mcpl.label_id) as count_non_broken_labels,
|
||||
COUNT(lm.label_id) as count_host_labels,
|
||||
-- this helps avoid the case where the host is not a member of a label
|
||||
-- just because it hasn't reported results for that label yet. But we
|
||||
-- only need consider this for dynamic labels - manual(type=1) can be
|
||||
-- considered at any time
|
||||
SUM(
|
||||
CASE WHEN lbl.label_membership_type <> 1 AND lbl.created_at IS NOT NULL AND h.label_updated_at >= lbl.created_at THEN 1
|
||||
WHEN lbl.label_membership_type = 1 AND lbl.created_at IS NOT NULL THEN 1
|
||||
@@ -1121,20 +1117,21 @@ const androidApplicableProfilesQuery = `
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
h.platform = 'android' AND
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE android_profile_uuid = macp.profile_uuid AND exclude = 0
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
macp.profile_uuid, macp.name, h.uuid, h.id
|
||||
HAVING
|
||||
-- considers only the profiles with labels, without any broken label, with results reported after all labels were
|
||||
-- created and with the host not in any label
|
||||
count_profile_labels > 0 AND count_profile_labels = count_non_broken_labels AND
|
||||
count_profile_labels = count_host_updated_after_labels AND count_host_labels = 0
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based profiles where the host is a member of any of the labels (include-any).
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
-- include-any only (no exclude labels): host must be a member of at least one include label.
|
||||
-- broken include labels are skipped (host can't be a member of a deleted label).
|
||||
SELECT
|
||||
macp.profile_uuid,
|
||||
macp.name,
|
||||
@@ -1157,11 +1154,115 @@ const androidApplicableProfilesQuery = `
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
h.platform = 'android' AND
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE android_profile_uuid = macp.profile_uuid AND exclude = 1
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
macp.profile_uuid, macp.name, h.uuid, h.id
|
||||
HAVING
|
||||
count_profile_labels > 0 AND count_host_labels >= 1
|
||||
|
||||
UNION
|
||||
|
||||
-- include-all + exclude-any: host must be in ALL include labels AND NOT in ANY exclude label.
|
||||
-- broken include labels or broken/not-yet-scanned dynamic exclude labels disqualify the profile.
|
||||
SELECT
|
||||
macp.profile_uuid,
|
||||
macp.name,
|
||||
macp.checksum,
|
||||
h.uuid as host_uuid,
|
||||
h.id as host_id,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 THEN 1 ELSE 0 END) as count_profile_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 AND mcpl.label_id IS NOT NULL THEN 1 ELSE 0 END) as count_non_broken_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 AND lm_inc.label_id IS NOT NULL THEN 1 ELSE 0 END) as count_host_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 1 AND lm_exc.label_id IS NOT NULL THEN 1
|
||||
WHEN mcpl.exclude = 1 AND (lbl.label_membership_type = 0 AND lbl.created_at IS NOT NULL AND h.label_updated_at < lbl.created_at) THEN 1
|
||||
WHEN mcpl.exclude = 1 AND mcpl.label_id IS NULL THEN 1
|
||||
ELSE 0 END) as count_host_updated_after_labels
|
||||
FROM
|
||||
mdm_android_configuration_profiles macp
|
||||
JOIN hosts h
|
||||
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
|
||||
JOIN android_devices ad
|
||||
ON ad.host_id = h.id
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.android_profile_uuid = macp.profile_uuid
|
||||
LEFT OUTER JOIN labels lbl
|
||||
ON lbl.id = mcpl.label_id
|
||||
LEFT OUTER JOIN label_membership lm_inc
|
||||
ON lm_inc.label_id = mcpl.label_id AND lm_inc.host_id = h.id AND mcpl.exclude = 0
|
||||
LEFT OUTER JOIN label_membership lm_exc
|
||||
ON lm_exc.label_id = mcpl.label_id AND lm_exc.host_id = h.id AND mcpl.exclude = 1
|
||||
WHERE
|
||||
h.platform = 'android' AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE android_profile_uuid = macp.profile_uuid AND exclude = 0 AND require_all = 1
|
||||
) AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE android_profile_uuid = macp.profile_uuid AND exclude = 1
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
macp.profile_uuid, macp.name, h.uuid, h.id
|
||||
HAVING
|
||||
-- include gate: host in all include labels (no broken include labels)
|
||||
count_profile_labels > 0 AND count_non_broken_labels = count_profile_labels AND count_host_labels = count_profile_labels AND
|
||||
-- exclude gate: host not in any exclude label, no broken/unscanned exclude labels
|
||||
count_host_updated_after_labels = 0
|
||||
|
||||
UNION
|
||||
|
||||
-- include-any + exclude-any: host must be in AT LEAST ONE include label AND NOT in ANY exclude label.
|
||||
-- broken/not-yet-scanned dynamic exclude labels disqualify the profile.
|
||||
SELECT
|
||||
macp.profile_uuid,
|
||||
macp.name,
|
||||
macp.checksum,
|
||||
h.uuid as host_uuid,
|
||||
h.id as host_id,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 THEN 1 ELSE 0 END) as count_profile_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 AND mcpl.label_id IS NOT NULL THEN 1 ELSE 0 END) as count_non_broken_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 AND lm_inc.label_id IS NOT NULL THEN 1 ELSE 0 END) as count_host_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 1 AND lm_exc.label_id IS NOT NULL THEN 1
|
||||
WHEN mcpl.exclude = 1 AND (lbl.label_membership_type = 0 AND lbl.created_at IS NOT NULL AND h.label_updated_at < lbl.created_at) THEN 1
|
||||
WHEN mcpl.exclude = 1 AND mcpl.label_id IS NULL THEN 1
|
||||
ELSE 0 END) as count_host_updated_after_labels
|
||||
FROM
|
||||
mdm_android_configuration_profiles macp
|
||||
JOIN hosts h
|
||||
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
|
||||
JOIN android_devices ad
|
||||
ON ad.host_id = h.id
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.android_profile_uuid = macp.profile_uuid
|
||||
LEFT OUTER JOIN labels lbl
|
||||
ON lbl.id = mcpl.label_id
|
||||
LEFT OUTER JOIN label_membership lm_inc
|
||||
ON lm_inc.label_id = mcpl.label_id AND lm_inc.host_id = h.id AND mcpl.exclude = 0
|
||||
LEFT OUTER JOIN label_membership lm_exc
|
||||
ON lm_exc.label_id = mcpl.label_id AND lm_exc.host_id = h.id AND mcpl.exclude = 1
|
||||
WHERE
|
||||
h.platform = 'android' AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE android_profile_uuid = macp.profile_uuid AND exclude = 0 AND require_all = 0
|
||||
) AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE android_profile_uuid = macp.profile_uuid AND exclude = 1
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
macp.profile_uuid, macp.name, h.uuid, h.id
|
||||
HAVING
|
||||
-- include gate: host in at least one include label
|
||||
count_host_labels >= 1 AND
|
||||
-- exclude gate: host not in any exclude label, no broken/unscanned exclude labels
|
||||
count_host_updated_after_labels = 0
|
||||
`
|
||||
|
||||
// ListMDMAndroidProfilesToSend is the android platform equivalent to
|
||||
@@ -1233,7 +1334,7 @@ func (ds *Datastore) ListMDMAndroidProfilesToSend(ctx context.Context) ([]*fleet
|
||||
ds.host_uuid IS NULL AND
|
||||
-- and it is not in pending remove status (in which case it was processed)
|
||||
( hmap.operation_type != ? OR COALESCE(hmap.status, '') <> ? )
|
||||
`, fmt.Sprintf(androidApplicableProfilesQuery, "TRUE", "TRUE", "TRUE", "TRUE"))
|
||||
`, fmt.Sprintf(androidApplicableProfilesQuery, "TRUE", "TRUE", "TRUE", "TRUE", "TRUE", "TRUE"))
|
||||
|
||||
// NOTE: we explicitly don't "ignore" profiles to remove based on broken labels,
|
||||
// because of how Android profiles are applied vs other platforms (ignoring
|
||||
@@ -1271,9 +1372,9 @@ func (ds *Datastore) ListMDMAndroidProfilesToSend(ctx context.Context) ([]*fleet
|
||||
ON hmap.host_uuid = ds.host_uuid AND hmap.profile_uuid = ds.profile_uuid
|
||||
LEFT OUTER JOIN android_policy_requests apr
|
||||
ON apr.request_uuid = hmap.policy_request_uuid
|
||||
`, fmt.Sprintf(androidApplicableProfilesQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
`, fmt.Sprintf(androidApplicableProfilesQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
|
||||
query, args, err := sqlx.In(listToInstallProfilesStmt, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs)
|
||||
query, args, err := sqlx.In(listToInstallProfilesStmt, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "building list android host applicable profiles query")
|
||||
}
|
||||
@@ -1297,9 +1398,9 @@ func (ds *Datastore) ListMDMAndroidProfilesToSend(ctx context.Context) ([]*fleet
|
||||
WHERE
|
||||
hmap.host_uuid IN (?) AND
|
||||
ds.host_uuid IS NULL
|
||||
`, fmt.Sprintf(androidApplicableProfilesQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
`, fmt.Sprintf(androidApplicableProfilesQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
|
||||
|
||||
query, args, err = sqlx.In(listToRemoveProfilesStmt, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs)
|
||||
query, args, err = sqlx.In(listToRemoveProfilesStmt, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "building list android host to remove profiles query")
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func TestAndroid(t *testing.T) {
|
||||
{"GetMDMAndroidProfilesSummary", testMDMAndroidProfilesSummary},
|
||||
{"ListMDMAndroidProfilesToSend", testListMDMAndroidProfilesToSend},
|
||||
{"ListMDMAndroidProfilesToSend_WithExcludeAny", testListMDMAndroidProfilesToSendWithExcludeAny},
|
||||
{"ListMDMAndroidProfilesToSend_WithCombinedLabels", testListMDMAndroidProfilesToSendWithCombinedLabels},
|
||||
{"GetMDMAndroidProfilesContents", testGetMDMAndroidProfilesContents},
|
||||
{"BulkUpsertMDMAndroidHostProfiles", testBulkUpsertMDMAndroidHostProfiles},
|
||||
{"BulkUpsertMDMAndroidHostProfiles", testBulkUpsertMDMAndroidHostProfiles2},
|
||||
@@ -1752,6 +1753,91 @@ func testListMDMAndroidProfilesToSendWithExcludeAny(t *testing.T, ds *Datastore)
|
||||
}, profs)
|
||||
}
|
||||
|
||||
func testListMDMAndroidProfilesToSendWithCombinedLabels(t *testing.T, ds *Datastore) {
|
||||
test.AddBuiltinLabels(t, ds)
|
||||
ctx := t.Context()
|
||||
|
||||
host := createAndroidHost("enterprise-id-combined")
|
||||
newHost, err := ds.NewAndroidHost(ctx, host, false)
|
||||
require.NoError(t, err)
|
||||
h := newHost.Host
|
||||
|
||||
// advance label_updated_at so dynamic labels are immediately evaluated
|
||||
h.LabelUpdatedAt = time.Now().UTC().Add(time.Second)
|
||||
h.PolicyUpdatedAt = time.Now().UTC()
|
||||
err = ds.UpdateHost(ctx, h)
|
||||
require.NoError(t, err)
|
||||
|
||||
inclAllLbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "incl-all-1", LabelMembershipType: fleet.LabelMembershipTypeManual})
|
||||
require.NoError(t, err)
|
||||
inclAllLbl2, err := ds.NewLabel(ctx, &fleet.Label{Name: "incl-all-2", LabelMembershipType: fleet.LabelMembershipTypeManual})
|
||||
require.NoError(t, err)
|
||||
inclAnyLbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "inclany-any-1", LabelMembershipType: fleet.LabelMembershipTypeManual})
|
||||
require.NoError(t, err)
|
||||
exclLbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-1", LabelMembershipType: fleet.LabelMembershipTypeManual})
|
||||
require.NoError(t, err)
|
||||
|
||||
// include-all + exclude-any profile (requires both incl-all-1 and incl-all-2)
|
||||
pCombinedAll, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("combined-incl-all", inclAllLbl, inclAllLbl2, exclLbl))
|
||||
require.NoError(t, err)
|
||||
// include-any + exclude-any profile
|
||||
pCombinedAny, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("combined-incl-any", inclAnyLbl, exclLbl))
|
||||
require.NoError(t, err)
|
||||
|
||||
profChecksum := getAndroidProfileChecksum(t, ds, pCombinedAll.ProfileUUID)
|
||||
|
||||
// host is not a member of any label → neither profile applies
|
||||
profs, toRemove, err := ds.ListMDMAndroidProfilesToSend(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, toRemove)
|
||||
require.Empty(t, profs)
|
||||
|
||||
// host joins include labels but not exclude → both profiles apply
|
||||
err = ds.AddLabelsToHost(ctx, h.ID, []uint{inclAllLbl.ID, inclAllLbl2.ID, inclAnyLbl.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
profs, toRemove, err = ds.ListMDMAndroidProfilesToSend(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, toRemove)
|
||||
require.Len(t, profs, 2)
|
||||
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
|
||||
{ProfileUUID: pCombinedAll.ProfileUUID, HostUUID: h.UUID, ProfileName: pCombinedAll.Name, Checksum: profChecksum},
|
||||
{ProfileUUID: pCombinedAny.ProfileUUID, HostUUID: h.UUID, ProfileName: pCombinedAny.Name, Checksum: profChecksum},
|
||||
}, profs)
|
||||
|
||||
// host also joins exclude label → neither profile applies
|
||||
err = ds.AddLabelsToHost(ctx, h.ID, []uint{exclLbl.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
profs, toRemove, err = ds.ListMDMAndroidProfilesToSend(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, toRemove)
|
||||
require.Empty(t, profs)
|
||||
|
||||
// host leaves exclude label → both profiles apply again
|
||||
err = ds.RemoveLabelsFromHost(ctx, h.ID, []uint{exclLbl.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
profs, toRemove, err = ds.ListMDMAndroidProfilesToSend(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, toRemove)
|
||||
require.Len(t, profs, 2)
|
||||
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
|
||||
{ProfileUUID: pCombinedAll.ProfileUUID, HostUUID: h.UUID, ProfileName: pCombinedAll.Name, Checksum: profChecksum},
|
||||
{ProfileUUID: pCombinedAny.ProfileUUID, HostUUID: h.UUID, ProfileName: pCombinedAny.Name, Checksum: profChecksum},
|
||||
}, profs)
|
||||
|
||||
// remove host from one include-all label → include-all profile no longer applies, include-any still does
|
||||
err = ds.RemoveLabelsFromHost(ctx, h.ID, []uint{inclAllLbl2.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
profs, toRemove, err = ds.ListMDMAndroidProfilesToSend(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, toRemove)
|
||||
require.Len(t, profs, 1)
|
||||
require.Equal(t, pCombinedAny.ProfileUUID, profs[0].ProfileUUID)
|
||||
}
|
||||
|
||||
func testGetMDMAndroidProfilesContents(t *testing.T, ds *Datastore) {
|
||||
ctx := t.Context()
|
||||
p1 := androidProfileForTest("p1")
|
||||
|
||||
@@ -2464,9 +2464,8 @@ const windowsMDMProfilesDesiredStateQuery = `
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based profiles where the host is a member of all the labels (include-all).
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
-- include-all only (no exclude labels): host must be a member of every include label.
|
||||
-- broken include labels disqualify the profile.
|
||||
SELECT
|
||||
mwcp.profile_uuid,
|
||||
mwcp.name,
|
||||
@@ -2491,6 +2490,10 @@ const windowsMDMProfilesDesiredStateQuery = `
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
h.platform = 'windows' AND
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE windows_profile_uuid = mwcp.profile_uuid AND exclude = 1
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
mwcp.profile_uuid, mwcp.name, h.uuid
|
||||
@@ -2499,11 +2502,8 @@ const windowsMDMProfilesDesiredStateQuery = `
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based entities where the host is NOT a member of any of the labels (exclude-any).
|
||||
-- explicitly ignore profiles with broken excluded labels so that they are never applied,
|
||||
-- and ignore profiles that depend on labels created _after_ the label_updated_at timestamp
|
||||
-- of the host (because we don't have results for that label yet, the host may or may not be
|
||||
-- a member).
|
||||
-- exclude-any only (no include labels): host must NOT be a member of any exclude label.
|
||||
-- broken or not-yet-scanned dynamic exclude labels disqualify the profile.
|
||||
SELECT
|
||||
mwcp.profile_uuid,
|
||||
mwcp.name,
|
||||
@@ -2513,10 +2513,6 @@ const windowsMDMProfilesDesiredStateQuery = `
|
||||
COUNT(*) as count_profile_labels,
|
||||
COUNT(mcpl.label_id) as count_non_broken_labels,
|
||||
COUNT(lm.label_id) as count_host_labels,
|
||||
-- this helps avoid the case where the host is not a member of a label
|
||||
-- just because it hasn't reported results for that label yet. But we
|
||||
-- only need consider this for dynamic labels - manual(type=1) can be
|
||||
-- considered at any time
|
||||
SUM(
|
||||
CASE WHEN lbl.label_membership_type <> 1 AND lbl.created_at IS NOT NULL AND h.label_updated_at >= lbl.created_at THEN 1
|
||||
WHEN lbl.label_membership_type = 1 AND lbl.created_at IS NOT NULL THEN 1
|
||||
@@ -2537,18 +2533,20 @@ const windowsMDMProfilesDesiredStateQuery = `
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
h.platform = 'windows' AND
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE windows_profile_uuid = mwcp.profile_uuid AND exclude = 0
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
mwcp.profile_uuid, mwcp.name, h.uuid
|
||||
HAVING
|
||||
-- considers only the profiles with labels, without any broken label, with results reported after all labels were created and with the host not in any label
|
||||
count_profile_labels > 0 AND count_profile_labels = count_non_broken_labels AND count_profile_labels = count_host_updated_after_labels AND count_host_labels = 0
|
||||
|
||||
UNION
|
||||
|
||||
-- label-based profiles where the host is a member of any of the labels (include-any).
|
||||
-- by design, "include" labels cannot match if they are broken (the host cannot be
|
||||
-- a member of a deleted label).
|
||||
-- include-any only (no exclude labels): host must be a member of at least one include label.
|
||||
-- broken include labels are skipped (host can't be a member of a deleted label).
|
||||
SELECT
|
||||
mwcp.profile_uuid,
|
||||
mwcp.name,
|
||||
@@ -2573,11 +2571,120 @@ const windowsMDMProfilesDesiredStateQuery = `
|
||||
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
|
||||
WHERE
|
||||
h.platform = 'windows' AND
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE windows_profile_uuid = mwcp.profile_uuid AND exclude = 1
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
mwcp.profile_uuid, mwcp.name, h.uuid
|
||||
HAVING
|
||||
count_profile_labels > 0 AND count_host_labels >= 1
|
||||
|
||||
UNION
|
||||
|
||||
-- include-all + exclude-any: host must be in ALL include labels AND NOT in ANY exclude label.
|
||||
-- the include and exclude rows are counted separately via conditional aggregation.
|
||||
-- broken include labels or broken/not-yet-scanned dynamic exclude labels disqualify the profile.
|
||||
SELECT
|
||||
mwcp.profile_uuid,
|
||||
mwcp.name,
|
||||
mwcp.checksum,
|
||||
mwcp.secrets_updated_at,
|
||||
h.uuid as host_uuid,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 THEN 1 ELSE 0 END) as count_profile_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 AND mcpl.label_id IS NOT NULL THEN 1 ELSE 0 END) as count_non_broken_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 AND lm_inc.label_id IS NOT NULL THEN 1 ELSE 0 END) as count_host_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 1 AND lm_exc.label_id IS NOT NULL THEN 1
|
||||
WHEN mcpl.exclude = 1 AND (lbl.label_membership_type = 0 AND lbl.created_at IS NOT NULL AND h.label_updated_at < lbl.created_at) THEN 1
|
||||
WHEN mcpl.exclude = 1 AND mcpl.label_id IS NULL THEN 1
|
||||
ELSE 0 END) as count_host_updated_after_labels
|
||||
FROM
|
||||
mdm_windows_configuration_profiles mwcp
|
||||
JOIN hosts h
|
||||
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
||||
JOIN mdm_windows_enrollments mwe
|
||||
ON mwe.host_uuid = h.uuid
|
||||
JOIN host_mdm hmdm
|
||||
ON hmdm.host_id = h.id AND hmdm.enrolled = 1
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid
|
||||
LEFT OUTER JOIN labels lbl
|
||||
ON lbl.id = mcpl.label_id
|
||||
LEFT OUTER JOIN label_membership lm_inc
|
||||
ON lm_inc.label_id = mcpl.label_id AND lm_inc.host_id = h.id AND mcpl.exclude = 0
|
||||
LEFT OUTER JOIN label_membership lm_exc
|
||||
ON lm_exc.label_id = mcpl.label_id AND lm_exc.host_id = h.id AND mcpl.exclude = 1
|
||||
WHERE
|
||||
h.platform = 'windows' AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE windows_profile_uuid = mwcp.profile_uuid AND exclude = 0 AND require_all = 1
|
||||
) AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE windows_profile_uuid = mwcp.profile_uuid AND exclude = 1
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
mwcp.profile_uuid, mwcp.name, h.uuid
|
||||
HAVING
|
||||
-- include gate: host in all include labels (no broken include labels)
|
||||
count_profile_labels > 0 AND count_non_broken_labels = count_profile_labels AND count_host_labels = count_profile_labels AND
|
||||
-- exclude gate: host not in any exclude label, no broken/unscanned exclude labels (reusing count_host_updated_after_labels)
|
||||
count_host_updated_after_labels = 0
|
||||
|
||||
UNION
|
||||
|
||||
-- include-any + exclude-any: host must be in AT LEAST ONE include label AND NOT in ANY exclude label.
|
||||
-- broken/not-yet-scanned dynamic exclude labels disqualify the profile.
|
||||
SELECT
|
||||
mwcp.profile_uuid,
|
||||
mwcp.name,
|
||||
mwcp.checksum,
|
||||
mwcp.secrets_updated_at,
|
||||
h.uuid as host_uuid,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 THEN 1 ELSE 0 END) as count_profile_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 AND mcpl.label_id IS NOT NULL THEN 1 ELSE 0 END) as count_non_broken_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 0 AND lm_inc.label_id IS NOT NULL THEN 1 ELSE 0 END) as count_host_labels,
|
||||
SUM(CASE WHEN mcpl.exclude = 1 AND lm_exc.label_id IS NOT NULL THEN 1
|
||||
WHEN mcpl.exclude = 1 AND (lbl.label_membership_type = 0 AND lbl.created_at IS NOT NULL AND h.label_updated_at < lbl.created_at) THEN 1
|
||||
WHEN mcpl.exclude = 1 AND mcpl.label_id IS NULL THEN 1
|
||||
ELSE 0 END) as count_host_updated_after_labels
|
||||
FROM
|
||||
mdm_windows_configuration_profiles mwcp
|
||||
JOIN hosts h
|
||||
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
|
||||
JOIN mdm_windows_enrollments mwe
|
||||
ON mwe.host_uuid = h.uuid
|
||||
JOIN host_mdm hmdm
|
||||
ON hmdm.host_id = h.id AND hmdm.enrolled = 1
|
||||
JOIN mdm_configuration_profile_labels mcpl
|
||||
ON mcpl.windows_profile_uuid = mwcp.profile_uuid
|
||||
LEFT OUTER JOIN labels lbl
|
||||
ON lbl.id = mcpl.label_id
|
||||
LEFT OUTER JOIN label_membership lm_inc
|
||||
ON lm_inc.label_id = mcpl.label_id AND lm_inc.host_id = h.id AND mcpl.exclude = 0
|
||||
LEFT OUTER JOIN label_membership lm_exc
|
||||
ON lm_exc.label_id = mcpl.label_id AND lm_exc.host_id = h.id AND mcpl.exclude = 1
|
||||
WHERE
|
||||
h.platform = 'windows' AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE windows_profile_uuid = mwcp.profile_uuid AND exclude = 0 AND require_all = 0
|
||||
) AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM mdm_configuration_profile_labels
|
||||
WHERE windows_profile_uuid = mwcp.profile_uuid AND exclude = 1
|
||||
) AND
|
||||
( %s )
|
||||
GROUP BY
|
||||
mwcp.profile_uuid, mwcp.name, h.uuid
|
||||
HAVING
|
||||
-- include gate: host in at least one include label
|
||||
count_host_labels >= 1 AND
|
||||
-- exclude gate: host not in any exclude label, no broken/unscanned exclude labels
|
||||
count_host_updated_after_labels = 0
|
||||
`
|
||||
|
||||
func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||
@@ -2640,7 +2747,7 @@ const windowsProfilesToInstallQuery = `
|
||||
|
||||
func (ds *Datastore) listAllMDMWindowsProfilesToInstallDB(ctx context.Context, tx sqlx.ExtContext) ([]*fleet.MDMWindowsProfilePayload, error) {
|
||||
var profiles []*fleet.MDMWindowsProfilePayload
|
||||
err := sqlx.SelectContext(ctx, tx, &profiles, fmt.Sprintf(windowsProfilesToInstallQuery, "TRUE", "TRUE", "TRUE", "TRUE"), fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove)
|
||||
err := sqlx.SelectContext(ctx, tx, &profiles, fmt.Sprintf(windowsProfilesToInstallQuery, "TRUE", "TRUE", "TRUE", "TRUE", "TRUE", "TRUE"), fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrapf(ctx, err, "selecting windows MDM profiles to install")
|
||||
}
|
||||
@@ -2663,7 +2770,7 @@ func (ds *Datastore) listMDMWindowsProfilesToInstallDB(
|
||||
hostFilter = "mwcp.profile_uuid IN (?) AND h.uuid IN (?)"
|
||||
}
|
||||
|
||||
toInstallQuery := fmt.Sprintf(windowsProfilesToInstallQuery, hostFilter, hostFilter, hostFilter, hostFilter)
|
||||
toInstallQuery := fmt.Sprintf(windowsProfilesToInstallQuery, hostFilter, hostFilter, hostFilter, hostFilter, hostFilter, hostFilter)
|
||||
|
||||
// use a 10k host batch size to match what we do on the macOS side.
|
||||
selectProfilesBatchSize := 10_000
|
||||
@@ -2687,10 +2794,12 @@ func (ds *Datastore) listMDMWindowsProfilesToInstallDB(
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove,
|
||||
)
|
||||
} else {
|
||||
stmt, args, err = sqlx.In(toInstallQuery, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove)
|
||||
stmt, args, err = sqlx.In(toInstallQuery, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrapf(ctx, err, "building sqlx.In for list MDM windows profiles to install, batch %d of %d", i, selectProfilesTotalBatches)
|
||||
@@ -2758,8 +2867,8 @@ func (ds *Datastore) ListNextPendingMDMWindowsHostUUIDs(ctx context.Context, aft
|
||||
// UNION arms so the optimizer filters early. The remove query also
|
||||
// gets the cursor in its 5th slot (outer WHERE on hmwp.host_uuid)
|
||||
// for a clean PK range scan on host_mdm_windows_profiles.
|
||||
toInstall := fmt.Sprintf(windowsProfilesToInstallQuery, "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?")
|
||||
toRemove := fmt.Sprintf(windowsProfilesToRemoveQuery, "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "hmwp.host_uuid > ?")
|
||||
toInstall := fmt.Sprintf(windowsProfilesToInstallQuery, "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?")
|
||||
toRemove := fmt.Sprintf(windowsProfilesToRemoveQuery, "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "hmwp.host_uuid > ?")
|
||||
|
||||
stmt := fmt.Sprintf(`
|
||||
SELECT host_uuid FROM (
|
||||
@@ -2772,13 +2881,13 @@ func (ds *Datastore) ListNextPendingMDMWindowsHostUUIDs(ctx context.Context, aft
|
||||
`, toInstall, toRemove, batchSize)
|
||||
|
||||
// Placeholder order in stmt:
|
||||
// install branches: 4 cursor (h.uuid > ?), 2 op-type (install, remove)
|
||||
// remove branches: 4 cursor (h.uuid > ?) for desired-state arms, 1 cursor (hmwp.host_uuid > ?) for outer WHERE
|
||||
// install branches: 6 cursor (h.uuid > ?), 2 op-type (install, remove)
|
||||
// remove branches: 6 cursor (h.uuid > ?) for desired-state arms, 1 cursor (hmwp.host_uuid > ?) for outer WHERE
|
||||
var hostUUIDs []string
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hostUUIDs, stmt,
|
||||
afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID,
|
||||
afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID,
|
||||
fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove,
|
||||
afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID,
|
||||
afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID,
|
||||
afterHostUUID,
|
||||
); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "listing next pending MDM windows host UUIDs")
|
||||
@@ -2863,7 +2972,7 @@ const windowsProfilesToRemoveQuery = `
|
||||
`
|
||||
|
||||
func (ds *Datastore) listAllMDMWindowsProfilesToRemoveDB(ctx context.Context, tx sqlx.ExtContext) (profiles []*fleet.MDMWindowsProfilePayload, err error) {
|
||||
err = sqlx.SelectContext(ctx, tx, &profiles, fmt.Sprintf(windowsProfilesToRemoveQuery, "TRUE", "TRUE", "TRUE", "TRUE", "TRUE"))
|
||||
err = sqlx.SelectContext(ctx, tx, &profiles, fmt.Sprintf(windowsProfilesToRemoveQuery, "TRUE", "TRUE", "TRUE", "TRUE", "TRUE", "TRUE", "TRUE"))
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrapf(ctx, err, "selecting windows MDM profiles to remove")
|
||||
}
|
||||
@@ -2889,7 +2998,7 @@ func (ds *Datastore) listMDMWindowsProfilesToRemoveDB(
|
||||
}
|
||||
|
||||
toRemoveQuery := fmt.Sprintf(windowsProfilesToRemoveQuery,
|
||||
desiredStateFilter, desiredStateFilter, desiredStateFilter, desiredStateFilter,
|
||||
desiredStateFilter, desiredStateFilter, desiredStateFilter, desiredStateFilter, desiredStateFilter, desiredStateFilter,
|
||||
outerFilter,
|
||||
)
|
||||
|
||||
@@ -2916,6 +3025,8 @@ func (ds *Datastore) listMDMWindowsProfilesToRemoveDB(
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
onlyProfileUUIDs, batchUUIDs,
|
||||
)
|
||||
} else {
|
||||
stmt, args, err = sqlx.In(toRemoveQuery,
|
||||
@@ -2924,6 +3035,8 @@ func (ds *Datastore) listMDMWindowsProfilesToRemoveDB(
|
||||
batchUUIDs,
|
||||
batchUUIDs,
|
||||
batchUUIDs,
|
||||
batchUUIDs,
|
||||
batchUUIDs,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -56,6 +56,7 @@ func TestMDMWindows(t *testing.T) {
|
||||
{"TestBatchSetMDMWindowsProfiles", testBatchSetMDMWindowsProfiles},
|
||||
{"TestMDMWindowsProfileLabels", testMDMWindowsProfileLabels},
|
||||
{"NewMDMWindowsConfigProfileSoftwareUpdateTracking", testNewMDMWindowsConfigProfileSoftwareUpdateTracking},
|
||||
{"TestMDMWindowsProfileLabelsCombined", testMDMWindowsProfileLabelsCombined},
|
||||
{"TestMDMWindowsSaveResponse", testSaveResponse},
|
||||
{"TestSetMDMWindowsProfilesWithVariables", testSetMDMWindowsProfilesWithVariables},
|
||||
{"TestWindowsMDMManagedSCEPCertificates", testWindowsMDMManagedSCEPCertificates},
|
||||
@@ -3550,6 +3551,114 @@ func testMDMWindowsProfileLabels(t *testing.T, ds *Datastore) {
|
||||
}, profilesToInstall)
|
||||
}
|
||||
|
||||
func testMDMWindowsProfileLabelsCombined(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Use a dedicated team to isolate from other tests in the same suite.
|
||||
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "combined-label-test-team"})
|
||||
require.NoError(t, err)
|
||||
|
||||
u := uuid.New().String()
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now().Add(-5 * time.Second),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: &u,
|
||||
UUID: u,
|
||||
Hostname: u,
|
||||
Platform: "windows",
|
||||
TeamID: &tm.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
windowsEnroll(t, ds, host)
|
||||
|
||||
incLabel, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-combined", Query: "select 1;"})
|
||||
require.NoError(t, err)
|
||||
excLabel, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "exclude-combined",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
incAllLabel1, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-combined-1", Query: "select 1;"})
|
||||
require.NoError(t, err)
|
||||
incAllLabel2, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-combined-2", Query: "select 1;"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Profile: include-any + exclude-any (scoped to the test team)
|
||||
profAnyExcl := windowsConfigProfileForTest(t, "prof-include-any-exclude", "./combined/any", incLabel, excLabel)
|
||||
profAnyExcl.TeamID = &tm.ID
|
||||
inclAnyExclProf, err := ds.NewMDMWindowsConfigProfile(ctx, *profAnyExcl, nil)
|
||||
require.NoError(t, err)
|
||||
sumAny := md5.Sum(inclAnyExclProf.SyncML) //nolint:gosec
|
||||
checksumAnyExcl := append([]byte{}, sumAny[:]...)
|
||||
|
||||
// Profile: include-all + exclude-any (scoped to the test team)
|
||||
profAllExcl := windowsConfigProfileForTest(t, "prof-include-all-exclude", "./combined/all", incAllLabel1, incAllLabel2, excLabel)
|
||||
profAllExcl.TeamID = &tm.ID
|
||||
inclAllExclProf, err := ds.NewMDMWindowsConfigProfile(ctx, *profAllExcl, nil)
|
||||
require.NoError(t, err)
|
||||
sumAll := md5.Sum(inclAllExclProf.SyncML) //nolint:gosec
|
||||
checksumAllExcl := append([]byte{}, sumAll[:]...)
|
||||
|
||||
// Host in include label, not in exclude -> both profiles should apply
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{
|
||||
{incLabel.ID, host.ID},
|
||||
{incAllLabel1.ID, host.ID},
|
||||
{incAllLabel2.ID, host.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update label_updated_at so dynamic labels are considered
|
||||
host.LabelUpdatedAt = time.Now().Add(1 * time.Second)
|
||||
err = ds.UpdateHost(ctx, host)
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err := ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
|
||||
{ProfileUUID: inclAnyExclProf.ProfileUUID, ProfileName: inclAnyExclProf.Name, HostUUID: host.UUID, Checksum: checksumAnyExcl},
|
||||
{ProfileUUID: inclAllExclProf.ProfileUUID, ProfileName: inclAllExclProf.Name, HostUUID: host.UUID, Checksum: checksumAllExcl},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Add host to exclude label -> neither profile should apply
|
||||
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{excLabel.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profilesToInstall)
|
||||
|
||||
// Remove host from exclude label -> both profiles should apply again
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{excLabel.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
|
||||
{ProfileUUID: inclAnyExclProf.ProfileUUID, ProfileName: inclAnyExclProf.Name, HostUUID: host.UUID, Checksum: checksumAnyExcl},
|
||||
{ProfileUUID: inclAllExclProf.ProfileUUID, ProfileName: inclAllExclProf.Name, HostUUID: host.UUID, Checksum: checksumAllExcl},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Remove host from one include-all label -> include-all profile should no longer apply
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{incAllLabel1.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
|
||||
{ProfileUUID: inclAnyExclProf.ProfileUUID, ProfileName: inclAnyExclProf.Name, HostUUID: host.UUID, Checksum: checksumAnyExcl},
|
||||
}, profilesToInstall)
|
||||
|
||||
// Remove host from include-any label -> include-any profile should no longer apply either
|
||||
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{incLabel.ID, host.ID}})
|
||||
require.NoError(t, err)
|
||||
|
||||
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, profilesToInstall)
|
||||
}
|
||||
|
||||
func expectWindowsProfiles(
|
||||
t *testing.T,
|
||||
ds *Datastore,
|
||||
|
||||
@@ -220,6 +220,107 @@ func TestEntityAppliesToHost_DeclarationsShareSameDispatcher(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEntityAppliesToHost_CombinedIncludeExclude covers the combined
|
||||
// include+exclude targeting cases for both include modes.
|
||||
func TestEntityAppliesToHost_CombinedIncludeExclude(t *testing.T) {
|
||||
host := &fleet.AppleHostReconcileInfo{
|
||||
HostID: 1, UUID: "h1", TeamID: nil, Platform: "darwin",
|
||||
LabelUpdatedAt: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
excLabel := fleet.AppleProfileLabelRef{
|
||||
LabelID: new(uint(9)),
|
||||
CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
t.Run("include_any + exclude_any", func(t *testing.T) {
|
||||
p := &fleet.AppleProfileForReconcile{
|
||||
TeamID: 0,
|
||||
IncludeMode: fleet.AppleProfileIncludeAny,
|
||||
IncludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(1))}, {LabelID: new(uint(2))}},
|
||||
ExcludeLabels: []fleet.AppleProfileLabelRef{excLabel},
|
||||
}
|
||||
// in include label, not in exclude -> applies
|
||||
require.True(t, EntityAppliesToHost(p, host, map[uint]struct{}{1: {}}))
|
||||
// in include label, also in exclude -> does not apply
|
||||
require.False(t, EntityAppliesToHost(p, host, map[uint]struct{}{1: {}, 9: {}}))
|
||||
// not in any include label -> does not apply
|
||||
require.False(t, EntityAppliesToHost(p, host, map[uint]struct{}{9: {}}))
|
||||
// not in any label -> does not apply
|
||||
require.False(t, EntityAppliesToHost(p, host, map[uint]struct{}{}))
|
||||
})
|
||||
|
||||
t.Run("include_all + exclude_any", func(t *testing.T) {
|
||||
p := &fleet.AppleProfileForReconcile{
|
||||
TeamID: 0,
|
||||
IncludeMode: fleet.AppleProfileIncludeAll,
|
||||
IncludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(1))}, {LabelID: new(uint(2))}},
|
||||
ExcludeLabels: []fleet.AppleProfileLabelRef{excLabel},
|
||||
}
|
||||
// in all include labels, not in exclude -> applies
|
||||
require.True(t, EntityAppliesToHost(p, host, map[uint]struct{}{1: {}, 2: {}}))
|
||||
// in all include labels, also in exclude -> does not apply
|
||||
require.False(t, EntityAppliesToHost(p, host, map[uint]struct{}{1: {}, 2: {}, 9: {}}))
|
||||
// missing one include label -> does not apply
|
||||
require.False(t, EntityAppliesToHost(p, host, map[uint]struct{}{1: {}}))
|
||||
})
|
||||
|
||||
t.Run("no include mode + exclude_any (pure exclude)", func(t *testing.T) {
|
||||
p := &fleet.AppleProfileForReconcile{
|
||||
TeamID: 0,
|
||||
IncludeMode: fleet.AppleProfileIncludeNone,
|
||||
ExcludeLabels: []fleet.AppleProfileLabelRef{excLabel},
|
||||
}
|
||||
// not in exclude label -> applies
|
||||
require.True(t, EntityAppliesToHost(p, host, map[uint]struct{}{1: {}}))
|
||||
// in exclude label -> does not apply
|
||||
require.False(t, EntityAppliesToHost(p, host, map[uint]struct{}{9: {}}))
|
||||
})
|
||||
|
||||
t.Run("dynamic exclude label created after host.LabelUpdatedAt disqualifies until host rescans", func(t *testing.T) {
|
||||
// host.LabelUpdatedAt is 2026-05-01; dynamic exclude label created after that
|
||||
dynamicExcLabel := fleet.AppleProfileLabelRef{
|
||||
LabelID: new(uint(99)),
|
||||
LabelMembershipType: int(fleet.LabelMembershipTypeDynamic),
|
||||
CreatedAt: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
p := &fleet.AppleProfileForReconcile{
|
||||
TeamID: 0,
|
||||
IncludeMode: fleet.AppleProfileIncludeAny,
|
||||
IncludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(1))}},
|
||||
ExcludeLabels: []fleet.AppleProfileLabelRef{dynamicExcLabel},
|
||||
}
|
||||
// host is in include label and NOT a member of the exclude label, but the
|
||||
// dynamic exclude label was created after label_updated_at -> disqualified
|
||||
require.False(t, EntityAppliesToHost(p, host, map[uint]struct{}{1: {}}))
|
||||
|
||||
// once host.LabelUpdatedAt advances past the label's CreatedAt, the timing
|
||||
// gate clears and the profile applies (host is not a member of the exclude label)
|
||||
advancedHost := &fleet.AppleHostReconcileInfo{
|
||||
HostID: 1, UUID: "h1", TeamID: nil, Platform: "darwin",
|
||||
LabelUpdatedAt: time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
require.True(t, EntityAppliesToHost(p, advancedHost, map[uint]struct{}{1: {}}))
|
||||
})
|
||||
}
|
||||
|
||||
// TestEntityAppliesToHost_PureExcludeNoInclude confirms that a profile with
|
||||
// only exclude labels (no include mode) applies to all hosts that are NOT
|
||||
// members of any exclude label.
|
||||
func TestEntityAppliesToHost_PureExcludeNoInclude(t *testing.T) {
|
||||
host := &fleet.AppleHostReconcileInfo{
|
||||
HostID: 1, UUID: "h1", TeamID: nil, Platform: "darwin",
|
||||
LabelUpdatedAt: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
p := &fleet.AppleProfileForReconcile{
|
||||
TeamID: 0,
|
||||
IncludeMode: fleet.AppleProfileIncludeNone,
|
||||
ExcludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(5)), CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)}},
|
||||
}
|
||||
require.True(t, EntityAppliesToHost(p, host, map[uint]struct{}{}))
|
||||
require.True(t, EntityAppliesToHost(p, host, map[uint]struct{}{1: {}}))
|
||||
require.False(t, EntityAppliesToHost(p, host, map[uint]struct{}{5: {}}))
|
||||
}
|
||||
|
||||
func TestComputeReconcileDeltas(t *testing.T) {
|
||||
hostA := &fleet.AppleHostReconcileInfo{
|
||||
HostID: 1, UUID: "uuid-A", TeamID: nil, Platform: "darwin",
|
||||
@@ -300,6 +401,89 @@ func TestComputeReconcileDeltas(t *testing.T) {
|
||||
require.Equal(t, "aDeletedProfile", toRemove[0].ProfileUUID)
|
||||
})
|
||||
|
||||
t.Run("combined include_all + exclude_any: host in include but also in exclude -> not desired", func(t *testing.T) {
|
||||
pCombined := &fleet.AppleProfileForReconcile{
|
||||
ProfileUUID: "aCombined",
|
||||
ProfileIdentifier: "com.example.combined",
|
||||
ProfileName: "Combined",
|
||||
TeamID: 0,
|
||||
Checksum: []byte("cccc"),
|
||||
IncludeMode: fleet.AppleProfileIncludeAll,
|
||||
IncludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(10))}},
|
||||
ExcludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(20)), CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)}},
|
||||
}
|
||||
profByTeam := map[uint][]*fleet.AppleProfileForReconcile{0: {pGlobal, pCombined}}
|
||||
|
||||
// Host in include label but also in exclude label -> combined profile not desired
|
||||
hostLabels := map[uint]map[uint]struct{}{
|
||||
hostA.HostID: {10: {}, 20: {}},
|
||||
}
|
||||
toInstall, toRemove := ComputeReconcileDeltas(
|
||||
[]*fleet.AppleHostReconcileInfo{hostA}, hostLabels, nil, profByTeam, profilesWithBrokenLabel,
|
||||
)
|
||||
require.Empty(t, toRemove)
|
||||
require.Len(t, toInstall, 1)
|
||||
require.Equal(t, "aProfileGlobal", toInstall[0].ProfileUUID)
|
||||
})
|
||||
|
||||
t.Run("combined include_all + exclude_any: host in include not in exclude -> desired", func(t *testing.T) {
|
||||
pCombined := &fleet.AppleProfileForReconcile{
|
||||
ProfileUUID: "aCombined",
|
||||
ProfileIdentifier: "com.example.combined",
|
||||
ProfileName: "Combined",
|
||||
TeamID: 0,
|
||||
Checksum: []byte("cccc"),
|
||||
IncludeMode: fleet.AppleProfileIncludeAll,
|
||||
IncludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(10))}},
|
||||
ExcludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(20)), CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)}},
|
||||
}
|
||||
profByTeam := map[uint][]*fleet.AppleProfileForReconcile{0: {pGlobal, pCombined}}
|
||||
|
||||
// Host in include label, not in exclude -> both profiles desired
|
||||
hostLabels := map[uint]map[uint]struct{}{
|
||||
hostA.HostID: {10: {}},
|
||||
}
|
||||
toInstall, toRemove := ComputeReconcileDeltas(
|
||||
[]*fleet.AppleHostReconcileInfo{hostA}, hostLabels, nil, profByTeam, profilesWithBrokenLabel,
|
||||
)
|
||||
require.Empty(t, toRemove)
|
||||
require.Len(t, toInstall, 2)
|
||||
})
|
||||
|
||||
t.Run("combined include_any + exclude_any: host in include but also in exclude -> not desired, removed if present", func(t *testing.T) {
|
||||
pCombined := &fleet.AppleProfileForReconcile{
|
||||
ProfileUUID: "aCombined",
|
||||
ProfileIdentifier: "com.example.combined",
|
||||
ProfileName: "Combined",
|
||||
TeamID: 0,
|
||||
Checksum: []byte("cccc"),
|
||||
IncludeMode: fleet.AppleProfileIncludeAny,
|
||||
IncludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(10))}, {LabelID: new(uint(11))}},
|
||||
ExcludeLabels: []fleet.AppleProfileLabelRef{{LabelID: new(uint(20)), CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)}},
|
||||
}
|
||||
profByTeam := map[uint][]*fleet.AppleProfileForReconcile{0: {pGlobal, pCombined}}
|
||||
|
||||
// Host in one include label AND in exclude label -> combined profile no longer desired, should be removed
|
||||
hostLabels := map[uint]map[uint]struct{}{
|
||||
hostA.HostID: {10: {}, 20: {}},
|
||||
}
|
||||
current := map[string][]*fleet.MDMAppleProfilePayload{
|
||||
"uuid-A": {{
|
||||
ProfileUUID: "aCombined",
|
||||
HostUUID: "uuid-A",
|
||||
Checksum: []byte("cccc"),
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Status: new(fleet.MDMDeliveryVerified),
|
||||
}},
|
||||
}
|
||||
toInstall, toRemove := ComputeReconcileDeltas(
|
||||
[]*fleet.AppleHostReconcileInfo{hostA}, hostLabels, current, profByTeam, profilesWithBrokenLabel,
|
||||
)
|
||||
require.Len(t, toInstall, 1) // pGlobal
|
||||
require.Len(t, toRemove, 1)
|
||||
require.Equal(t, "aCombined", toRemove[0].ProfileUUID)
|
||||
})
|
||||
|
||||
t.Run("broken label profile is not removed", func(t *testing.T) {
|
||||
brokenProf := &fleet.AppleProfileForReconcile{
|
||||
ProfileUUID: "aBrokenLabel",
|
||||
|
||||
Reference in New Issue
Block a user