fleet/server/datastore/mysql/microsoft_mdm_test.go

3580 lines
138 KiB
Go
Raw Normal View History

package mysql
import (
"context"
"crypto/md5" // nolint:gosec // used only to hash for efficient comparisons
"database/sql"
"encoding/xml"
"fmt"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMDMWindows(t *testing.T) {
ds := CreateMySQLDS(t)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"TestMDMWindowsEnrolledDevices", testMDMWindowsEnrolledDevice},
{"TestMDMWindowsInsertCommandForHosts", testMDMWindowsInsertCommandForHosts},
{"TestMDMWindowsGetPendingCommands", testMDMWindowsGetPendingCommands},
{"TestMDMWindowsCommandResults", testMDMWindowsCommandResults},
{"TestMDMWindowsCommandResultsWithPendingResult", testMDMWindowsCommandResultsWithPendingResult},
{"TestMDMWindowsProfileManagement", testMDMWindowsProfileManagement},
{"TestBulkOperationsMDMWindowsHostProfiles", testBulkOperationsMDMWindowsHostProfiles},
{"TestBulkOperationsMDMWindowsHostProfilesBatch2", testBulkOperationsMDMWindowsHostProfilesBatch2},
{"TestBulkOperationsMDMWindowsHostProfilesBatch3", testBulkOperationsMDMWindowsHostProfilesBatch3},
{"TestGetMDMWindowsProfilesContents", testGetMDMWindowsProfilesContents},
{"TestMDMWindowsConfigProfiles", testMDMWindowsConfigProfiles},
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
{"TestMDMWindowsConfigProfilesWithFleetVars", testMDMWindowsConfigProfilesWithFleetVars},
{"TestSetOrReplaceMDMWindowsConfigProfile", testSetOrReplaceMDMWindowsConfigProfile},
{"TestMDMWindowsDiskEncryption", testMDMWindowsDiskEncryption},
{"TestMDMWindowsProfilesSummary", testMDMWindowsProfilesSummary},
{"TestBatchSetMDMWindowsProfiles", testBatchSetMDMWindowsProfiles},
{"TestMDMWindowsProfileLabels", testMDMWindowsProfileLabels},
{"TestMDMWindowsSaveResponse", testSaveResponse},
{"TestSetMDMWindowsProfilesWithVariables", testSetMDMWindowsProfilesWithVariables},
{"TestWindowsMDMManagedSCEPCertificates", testWindowsMDMManagedSCEPCertificates},
{"TestGetWindowsMDMCommandsForResending", testGetWindowsMDMCommandsForResending},
{"TestResendWindowsMDMCommand", testResendWindowsMDMCommand},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) {
ctx := context.Background()
enrolledDevice := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
}
err := ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice)
require.NoError(t, err)
// inserting a device again doesn't trow an error
err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice)
require.NoError(t, err)
gotEnrolledDevice, err := ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
require.NoError(t, err)
require.NotZero(t, gotEnrolledDevice.CreatedAt)
require.Equal(t, enrolledDevice.MDMDeviceID, gotEnrolledDevice.MDMDeviceID)
require.Equal(t, enrolledDevice.MDMHardwareID, gotEnrolledDevice.MDMHardwareID)
err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID)
require.NoError(t, err)
var nfe fleet.NotFoundError
_, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
require.ErrorAs(t, err, &nfe)
err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID)
require.ErrorAs(t, err, &nfe)
// Test using device ID instead of hardware ID
err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice)
require.NoError(t, err)
// inserting a device again doesn't trow an error
err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice)
require.NoError(t, err)
gotEnrolledDevice, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
require.NoError(t, err)
require.NotZero(t, gotEnrolledDevice.CreatedAt)
require.Equal(t, enrolledDevice.MDMDeviceID, gotEnrolledDevice.MDMDeviceID)
require.Equal(t, enrolledDevice.MDMHardwareID, gotEnrolledDevice.MDMHardwareID)
require.Empty(t, gotEnrolledDevice.HostUUID)
err = ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
require.NoError(t, err)
_, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
require.ErrorAs(t, err, &nfe)
err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID)
require.ErrorAs(t, err, &nfe)
}
func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) {
ctx := context.Background()
checkBitLockerSummary := func(t *testing.T, teamID *uint, expected fleet.MDMWindowsBitLockerSummary) {
bls, err := ds.GetMDMWindowsBitLockerSummary(ctx, teamID)
require.NoError(t, err)
require.NotNil(t, bls)
require.Equal(t, expected, *bls)
}
checkMDMProfilesSummary := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) {
ps, err := ds.GetMDMWindowsProfilesSummary(ctx, teamID)
require.NoError(t, err)
require.NotNil(t, ps)
require.Equal(t, expected, *ps)
}
checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) {
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status})
require.NoError(t, err)
require.Len(t, gotHosts, len(expectedIDs))
for _, h := range gotHosts {
require.Contains(t, expectedIDs, h.ID)
}
}
checkListHostsFilterDiskEncryption := func(t *testing.T, teamID *uint, status fleet.DiskEncryptionStatus, expectedIDs []uint) {
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsDiskEncryptionFilter: status})
require.NoError(t, err)
require.Len(t, gotHosts, len(expectedIDs), "status: %s", status)
for _, h := range gotHosts {
require.Contains(t, expectedIDs, h.ID)
}
count, err := ds.CountHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsDiskEncryptionFilter: status})
require.NoError(t, err)
require.Equal(t, len(expectedIDs), count, fmt.Sprintf("status: %s", status))
}
checkHostBitLockerStatus := func(t *testing.T, expected fleet.DiskEncryptionStatus, hostIDs []uint) {
for _, id := range hostIDs {
h, err := ds.Host(ctx, id)
require.NoError(t, err)
require.NotNil(t, h)
mdmInfo, err := ds.GetHostMDM(ctx, id)
require.NoError(t, err)
require.NotNil(t, mdmInfo)
bls, err := ds.GetMDMWindowsBitLockerStatus(ctx, h)
require.NoError(t, err)
require.NotNil(t, bls)
require.NotNil(t, bls.Status)
require.Equal(t, expected, *bls.Status)
}
}
type hostIDsByDEStatus map[fleet.DiskEncryptionStatus][]uint
type hostIDsByProfileStatus map[fleet.MDMDeliveryStatus][]uint
expectedProfilesFromDE := func(expectedDE hostIDsByDEStatus) hostIDsByProfileStatus {
expectedProfiles := make(hostIDsByProfileStatus)
expectedProfiles[fleet.MDMDeliveryPending] = []uint{}
for status, hostIDs := range expectedDE {
switch status {
case fleet.DiskEncryptionVerified:
expectedProfiles[fleet.MDMDeliveryVerified] = hostIDs
case fleet.DiskEncryptionVerifying:
expectedProfiles[fleet.MDMDeliveryVerifying] = hostIDs
case fleet.DiskEncryptionFailed:
expectedProfiles[fleet.MDMDeliveryFailed] = hostIDs
case fleet.DiskEncryptionEnforcing, fleet.DiskEncryptionRemovingEnforcement, fleet.DiskEncryptionActionRequired:
expectedProfiles[fleet.MDMDeliveryPending] = append(expectedProfiles[fleet.MDMDeliveryPending], hostIDs...)
}
}
return expectedProfiles
}
checkExpected := func(t *testing.T, teamID *uint, expectedDE hostIDsByDEStatus, expectedProfiles ...hostIDsByProfileStatus) {
var ep hostIDsByProfileStatus
switch len(expectedProfiles) {
case 1:
ep = expectedProfiles[0]
case 0:
ep = expectedProfilesFromDE(expectedDE)
default:
require.FailNow(t, "expectedProfiles must have length 0 or 1")
}
for _, status := range []fleet.DiskEncryptionStatus{
fleet.DiskEncryptionVerified,
fleet.DiskEncryptionVerifying,
fleet.DiskEncryptionFailed,
fleet.DiskEncryptionEnforcing,
fleet.DiskEncryptionRemovingEnforcement,
fleet.DiskEncryptionActionRequired,
} {
hostIDs, ok := expectedDE[status]
if !ok {
hostIDs = []uint{}
}
checkListHostsFilterDiskEncryption(t, teamID, status, hostIDs)
checkHostBitLockerStatus(t, status, hostIDs)
}
checkBitLockerSummary(t, teamID, fleet.MDMWindowsBitLockerSummary{
Verified: uint(len(expectedDE[fleet.DiskEncryptionVerified])),
Verifying: uint(len(expectedDE[fleet.DiskEncryptionVerifying])),
Failed: uint(len(expectedDE[fleet.DiskEncryptionFailed])),
Enforcing: uint(len(expectedDE[fleet.DiskEncryptionEnforcing])),
RemovingEnforcement: uint(len(expectedDE[fleet.DiskEncryptionRemovingEnforcement])),
ActionRequired: uint(len(expectedDE[fleet.DiskEncryptionActionRequired])),
})
checkMDMProfilesSummary(t, teamID, fleet.MDMProfilesSummary{
Pending: uint(len(ep[fleet.MDMDeliveryPending])),
Failed: uint(len(ep[fleet.MDMDeliveryFailed])),
Verifying: uint(len(ep[fleet.MDMDeliveryVerifying])),
Verified: uint(len(ep[fleet.MDMDeliveryVerified])),
})
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, ep[fleet.MDMDeliveryVerified])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, ep[fleet.MDMDeliveryVerifying])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, ep[fleet.MDMDeliveryFailed])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, ep[fleet.MDMDeliveryPending])
}
updateHostDisks := func(t *testing.T, hostID uint, encrypted bool, updated_at time.Time) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
stmt := `UPDATE host_disks SET encrypted = ?, updated_at = ? where host_id = ?`
_, err := q.ExecContext(ctx, stmt, encrypted, updated_at, hostID)
return err
})
}
setKeyUpdatedAt := func(t *testing.T, hostID uint, keyUpdatedAt time.Time) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
stmt := `UPDATE host_disk_encryption_keys SET updated_at = ? where host_id = ?`
_, err := q.ExecContext(ctx, stmt, keyUpdatedAt, hostID)
return err
})
}
upsertHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, status fleet.MDMDeliveryStatus) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
Updated SQL modes in tests to match production. (#31445) Fixes #31444 The changes are primarily in tests. The only changes in production code are a couple validations/checks for invalid values in: - mysql/apple_mdm.go - mysql/hosts.go - mysql/queries.go # 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. ## 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** * Improved handling of timestamp and default values across various features to prevent database errors and warnings. * Enhanced validation and data consistency for Apple Business Manager tokens and MDM profiles. * Updated test data and logic to comply with stricter database constraints and realistic scenarios, including date handling and field lengths. * **Chores** * Updated test setups to reflect schema changes, improve data integrity, and avoid future compatibility issues. * Standardized SQL mode and timestamp usage in test environments. * Refined test data for VPP apps, software installers, and device enrollments for better reliability. * **Tests** * Expanded and updated tests to cover new fields, stricter validation, and more accurate simulation of real-world conditions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-03 06:18:13 +00:00
// Generate a command UUID for the profile
commandUUID := "cmd-" + profUUID
stmt := `INSERT INTO host_mdm_windows_profiles (host_uuid, profile_uuid, status, command_uuid) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`
_, err := q.ExecContext(ctx, stmt, hostUUID, profUUID, status, commandUUID, status)
return err
})
}
cleanupHostProfiles := func(t *testing.T) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_windows_profiles`)
return err
})
}
// Create some hosts
var hosts []*fleet.Host
for i := 0; i < 10; i++ {
p := "windows"
if i >= 5 {
p = "darwin"
}
u := uuid.New().String()
h, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: &u,
UUID: u,
Hostname: u,
Platform: p,
})
require.NoError(t, err)
require.NotNil(t, h)
hosts = append(hosts, h)
require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false))
if p == "darwin" {
nanoEnroll(t, ds, h, false)
} else {
windowsEnroll(t, ds, h)
}
}
t.Run("Disk encryption disabled", func(t *testing.T) {
ac, err := ds.AppConfig(ctx)
require.NoError(t, err)
ac.MDM.EnableDiskEncryption = optjson.SetBool(false)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.False(t, ac.MDM.EnableDiskEncryption.Value)
cleanupHostProfiles(t)
checkExpected(t, nil, hostIDsByDEStatus{}) // no hosts are counted because disk encryption is not enabled
})
t.Run("Disk encryption enabled", func(t *testing.T) {
ac, err := ds.AppConfig(ctx)
require.NoError(t, err)
ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.True(t, ac.MDM.EnableDiskEncryption.Value)
t.Run("Bitlocker enforcing-verifying-verified", func(t *testing.T) {
// all windows hosts are counted as enforcing because they have not reported any disk encryption status yet
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
})
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))
require.NoError(t, err)
checkExpected(t, nil, hostIDsByDEStatus{
// status is still pending because hosts_disks hasn't been updated yet
fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
})
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true))
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionEnforcing: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
})
cases := []struct {
name string
hostDisksEncrypted bool
reportedAfterKey bool
expectedWithinGracePeriod fleet.DiskEncryptionStatus
expectedOutsideGracePeriod fleet.DiskEncryptionStatus
}{
{
name: "encrypted reported after key",
hostDisksEncrypted: true,
reportedAfterKey: true,
expectedWithinGracePeriod: fleet.DiskEncryptionVerified,
expectedOutsideGracePeriod: fleet.DiskEncryptionVerified,
},
{
name: "encrypted reported before key",
hostDisksEncrypted: true,
reportedAfterKey: false,
expectedWithinGracePeriod: fleet.DiskEncryptionVerifying,
expectedOutsideGracePeriod: fleet.DiskEncryptionVerifying,
},
{
name: "not encrypted reported before key",
hostDisksEncrypted: false,
reportedAfterKey: false,
expectedWithinGracePeriod: fleet.DiskEncryptionEnforcing,
expectedOutsideGracePeriod: fleet.DiskEncryptionEnforcing,
},
{
name: "not encrypted reported after key",
hostDisksEncrypted: false,
reportedAfterKey: true,
expectedWithinGracePeriod: fleet.DiskEncryptionVerifying,
expectedOutsideGracePeriod: fleet.DiskEncryptionEnforcing,
},
}
testHostID := hosts[0].ID
otherWindowsHostIDs := []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var keyUpdatedAt, hostDisksUpdatedAt time.Time
t.Run("within grace period", func(t *testing.T) {
expected := make(hostIDsByDEStatus)
if c.expectedWithinGracePeriod == fleet.DiskEncryptionEnforcing {
expected[fleet.DiskEncryptionEnforcing] = append([]uint{testHostID}, otherWindowsHostIDs...)
} else {
expected[c.expectedWithinGracePeriod] = []uint{testHostID}
expected[fleet.DiskEncryptionEnforcing] = otherWindowsHostIDs
}
keyUpdatedAt = time.Now().Add(-10 * time.Minute)
setKeyUpdatedAt(t, testHostID, keyUpdatedAt)
if c.reportedAfterKey {
hostDisksUpdatedAt = keyUpdatedAt.Add(5 * time.Minute)
} else {
hostDisksUpdatedAt = keyUpdatedAt.Add(-5 * time.Minute)
}
updateHostDisks(t, testHostID, c.hostDisksEncrypted, hostDisksUpdatedAt)
checkExpected(t, nil, expected)
})
t.Run("outside grace period", func(t *testing.T) {
expected := make(hostIDsByDEStatus)
if c.expectedOutsideGracePeriod == fleet.DiskEncryptionEnforcing {
expected[fleet.DiskEncryptionEnforcing] = append([]uint{testHostID}, otherWindowsHostIDs...)
} else {
expected[c.expectedOutsideGracePeriod] = []uint{testHostID}
expected[fleet.DiskEncryptionEnforcing] = otherWindowsHostIDs
}
keyUpdatedAt = time.Now().Add(-2 * time.Hour)
setKeyUpdatedAt(t, testHostID, keyUpdatedAt)
if c.reportedAfterKey {
hostDisksUpdatedAt = keyUpdatedAt.Add(5 * time.Minute)
} else {
hostDisksUpdatedAt = keyUpdatedAt.Add(-5 * time.Minute)
}
updateHostDisks(t, testHostID, c.hostDisksEncrypted, hostDisksUpdatedAt)
checkExpected(t, nil, expected)
})
})
}
})
// ensure hosts[0] is set to verified for the rest of the tests
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))
require.NoError(t, err)
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true))
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionEnforcing: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
})
t.Run("BitLocker failed status", func(t *testing.T) {
// set hosts[1] to failed
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1], "", "test-error", ptr.Bool(false))
require.NoError(t, err)
expected := hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
checkExpected(t, nil, expected)
// bitlocker failed status determines MDM aggregate status (profiles status is ignored)
upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", fleet.MDMDeliveryFailed)
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", fleet.MDMDeliveryPending)
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", fleet.MDMDeliveryVerifying)
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", fleet.MDMDeliveryVerified)
checkExpected(t, nil, expected)
// profiles failed status determines MDM aggregate status (bitlocker status is ignored)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", fleet.MDMDeliveryFailed)
expectedProfiles := expectedProfilesFromDE(expected)
expectedProfiles[fleet.MDMDeliveryFailed] = append(expectedProfiles[fleet.MDMDeliveryFailed], hosts[0].ID)
expectedProfiles[fleet.MDMDeliveryVerified] = []uint{}
checkExpected(t, nil, expected, expectedProfiles)
cleanupHostProfiles(t)
})
Implement BitLocker "action required" status (#31451) for #31182 # Details This PR implements the "Action Required" state for Windows host disk encryption. This includes updates to reporting for: * disk encryption summary (`GET /fleet/disk_encryption`) * config profiles summary (`GET /configuration_profiles/summary`) * config profile status ( `GET /configuration_profiles/{profile_uuid}/status`) For disk encryption summary, the statuses are now determined according to [the rules in the Figma](https://www.figma.com/design/XbhlPuEJxQtOgTZW9EOJZp/-28133-Enforce-BitLocker-PIN?node-id=5484-928&t=JB13g8zQ2QDVEmPB-0). TL;DR if the criteria for "verified" or "verifying" are set, but a required PIN is not set, we report a host as "action required". For profiles, I followed what seems to be the existing pattern and set the profile status to "pending" if the disk encryption status is "action required". This is what we do for hosts with the "enforcing" or "removing enforcement" statuses. A lot of the changes in these files are due to the creation of the `fleet.DiskEncryptionConfig` struct to hold info about disk encryption config, and passing variables of that type to various functions instead of passing a `bool` to indicate whether encryption is enabled. Other than that, the functional changes are constrained to a few files. > Note: to get the "require bitlocker pin" UI, compile the front end with: ``` SHOW_BITLOCKER_PIN_OPTION=true NODE_ENV=development yarn run webpack --progress --watch ``` # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. Changelog will be added when feature is complete. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## 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) - [ ] QA'd all new/changed functionality manually Could use some help testing this end-to-end. I was able to test the banners showing up correctly, but testing the Disk Encryption table requires some Windows-MDM-fu (I just get all zeroes). ## Database migrations - [X] Checked table schema to confirm autoupdate - [X] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [X] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [X] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`).
2025-08-05 16:23:27 +00:00
t.Run("BitLocker profile status with PIN required", func(t *testing.T) {
// Turn on Bitlocker requirement
ac.MDM.RequireBitLockerPIN = optjson.SetBool(true)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.True(t, ac.MDM.RequireBitLockerPIN.Value)
// Expect that the host that would be "verified"
// is now in "action required" status.
// This will also verify that when filtering by profile status,
// the "verified" host is now counted as "pending".
expected := hostIDsByDEStatus{
fleet.DiskEncryptionActionRequired: []uint{hosts[0].ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
checkExpected(t, nil, expected)
// Set the "tpm_pin_set" to true for the host that would be "verified"
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE host_disks SET tpm_pin_set = true WHERE host_id = ?`, hosts[0].ID)
return err
})
expected = hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
checkExpected(t, nil, expected)
// Reset the "tpm_pin_set" to false for the host.
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE host_disks SET tpm_pin_set = false WHERE host_id = ?`, hosts[0].ID)
return err
})
// Reset "RequireBitLockerPIN" to false
ac.MDM.RequireBitLockerPIN = optjson.SetBool(false)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.False(t, ac.MDM.RequireBitLockerPIN.Value)
})
t.Run("BitLocker team filtering", func(t *testing.T) {
// Test team filtering
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team"})
require.NoError(t, err)
tm, err := ds.TeamWithExtras(ctx, team.ID)
require.NoError(t, err)
require.NotNil(t, tm)
require.False(t, tm.Config.MDM.EnableDiskEncryption) // disk encryption is not enabled for team
// Transfer hosts[2] to the team
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{hosts[2].ID})))
// Check the summary for the team
checkExpected(t, &team.ID, hostIDsByDEStatus{}) // disk encryption is not enabled for team so hosts[2] is not counted
// Check the summary for no team
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
fleet.DiskEncryptionEnforcing: []uint{hosts[3].ID, hosts[4].ID}, // hosts[2] is no longer included in the no team summary
})
// Enable disk encryption for the team
tm.Config.MDM.EnableDiskEncryption = true
tm, err = ds.SaveTeam(ctx, tm)
require.NoError(t, err)
require.NotNil(t, tm)
require.True(t, tm.Config.MDM.EnableDiskEncryption)
// Check the summary for the team
checkExpected(t, &team.ID, hostIDsByDEStatus{
fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID}, // disk encryption is enabled for team so hosts[2] is counted
})
// Check the summary for no team (should be unchanged)
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
fleet.DiskEncryptionEnforcing: []uint{hosts[3].ID, hosts[4].ID},
})
})
t.Run("BitLocker Windows server excluded", func(t *testing.T) {
require.NoError(t, ds.SetOrUpdateMDMData(ctx,
hosts[3].ID,
true, // set is_server to true for hosts[3]
true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false))
// Check Windows servers not counted
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
fleet.DiskEncryptionEnforcing: []uint{hosts[4].ID}, // hosts[3] is not counted
})
})
t.Run("OS settings filters include Windows and macOS hosts", func(t *testing.T) {
// Make macOS host fail disk encryption
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{
HostUUID: hosts[5].UUID,
ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier,
ProfileName: "Disk encryption",
ProfileUUID: "a" + uuid.NewString(),
CommandUUID: uuid.New().String(),
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryFailed,
Checksum: []byte("checksum"),
Scope: fleet.PayloadScopeSystem,
},
}))
// Check that BitLocker summary does not include macOS hosts
checkBitLockerSummary(t, nil, fleet.MDMWindowsBitLockerSummary{
Verified: 1,
Verifying: 0,
Failed: 1,
Enforcing: 1,
RemovingEnforcement: 0,
ActionRequired: 0,
})
// Check that filtered lists do include macOS hosts
checkListHostsFilterDiskEncryption(t, nil, fleet.DiskEncryptionFailed, []uint{hosts[1].ID, hosts[5].ID})
checkListHostsFilterOSSettings(t, nil, fleet.OSSettingsFailed, []uint{hosts[1].ID, hosts[5].ID})
// delete the macOS host profile
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_profiles WHERE host_uuid = ? AND profile_identifier = ?`, hosts[5].UUID, mobileconfig.FleetFileVaultPayloadIdentifier)
return err
})
})
t.Run("BitLocker host disks must update to transition from Verifying to Verified", func(t *testing.T) {
// we'll use hosts[4] as the target for this test
targetHost := hosts[4]
// confirm our initial state is as expected from previous tests
// hosts[2] is was transferred to a team and is not counted
// hosts[3] is a Windows server and is not counted
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
fleet.DiskEncryptionEnforcing: []uint{targetHost.ID}, // targetHost is initially enforcing
})
// simulate targetHost previously reported encrypted for disk encryption detail query
// results
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, targetHost.ID, true))
// manualy update host_disks for targetHost to encrypted and ensure updated_at
// timestamp is in the past
updateHostDisks(t, targetHost.ID, true, time.Now().Add(-3*time.Hour))
// simulate targetHost reporting disk encryption key
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, targetHost, "test-key", "", ptr.Bool(true))
require.NoError(t, err)
// check that targetHost is now counted as verifying (not verified because host_disks still needs to be updated)
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
fleet.DiskEncryptionVerifying: []uint{targetHost.ID},
})
// simulate targetHost reporting detail query results for disk encryption
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, targetHost.ID, true))
// status for targetHost now verified because SetOrUpdateHostDisksEncryption always sets host_disks.updated_at
// to the current timestamp even if the `encrypted` value hasn't changed
checkExpected(t, nil, hostIDsByDEStatus{
fleet.DiskEncryptionVerified: []uint{hosts[0].ID, targetHost.ID},
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
})
})
})
}
func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
ctx := context.Background()
checkMDMProfilesSummary := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) {
ps, err := ds.GetMDMWindowsProfilesSummary(ctx, teamID)
require.NoError(t, err)
require.NotNil(t, ps)
require.Equal(t, expected, *ps)
}
checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) {
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status})
require.NoError(t, err)
if len(expectedIDs) != len(gotHosts) {
gotIDs := make([]uint, len(gotHosts))
for i, h := range gotHosts {
gotIDs[i] = h.ID
}
require.Len(t, gotHosts, len(expectedIDs), fmt.Sprintf("status: %s expected: %v got: %v", status, expectedIDs, gotIDs))
}
for _, h := range gotHosts {
require.Contains(t, expectedIDs, h.ID)
}
count, err := ds.CountHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status})
require.NoError(t, err)
require.Equal(t, len(expectedIDs), count, "status: %s", status)
}
type hostIDsByProfileStatus map[fleet.MDMDeliveryStatus][]uint
checkExpected := func(t *testing.T, teamID *uint, ep hostIDsByProfileStatus) {
checkMDMProfilesSummary(t, teamID, fleet.MDMProfilesSummary{
Pending: uint(len(ep[fleet.MDMDeliveryPending])),
Failed: uint(len(ep[fleet.MDMDeliveryFailed])),
Verifying: uint(len(ep[fleet.MDMDeliveryVerifying])),
Verified: uint(len(ep[fleet.MDMDeliveryVerified])),
})
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, ep[fleet.MDMDeliveryVerified])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, ep[fleet.MDMDeliveryVerifying])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, ep[fleet.MDMDeliveryFailed])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, ep[fleet.MDMDeliveryPending])
}
upsertHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, status *fleet.MDMDeliveryStatus) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
Updated SQL modes in tests to match production. (#31445) Fixes #31444 The changes are primarily in tests. The only changes in production code are a couple validations/checks for invalid values in: - mysql/apple_mdm.go - mysql/hosts.go - mysql/queries.go # 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. ## 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** * Improved handling of timestamp and default values across various features to prevent database errors and warnings. * Enhanced validation and data consistency for Apple Business Manager tokens and MDM profiles. * Updated test data and logic to comply with stricter database constraints and realistic scenarios, including date handling and field lengths. * **Chores** * Updated test setups to reflect schema changes, improve data integrity, and avoid future compatibility issues. * Standardized SQL mode and timestamp usage in test environments. * Refined test data for VPP apps, software installers, and device enrollments for better reliability. * **Tests** * Expanded and updated tests to cover new fields, stricter validation, and more accurate simulation of real-world conditions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-03 06:18:13 +00:00
// Generate a command UUID for the profile
commandUUID := "cmd-" + profUUID
stmt := `INSERT INTO host_mdm_windows_profiles (host_uuid, profile_uuid, status, command_uuid) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`
_, err := q.ExecContext(ctx, stmt, hostUUID, profUUID, status, commandUUID, status)
if err != nil {
return err
}
stmt = `UPDATE host_mdm_windows_profiles SET operation_type = ? WHERE host_uuid = ? AND profile_uuid = ?`
_, err = q.ExecContext(ctx, stmt, fleet.MDMOperationTypeInstall, hostUUID, profUUID)
return err
})
}
cleanupTables := func(t *testing.T) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_windows_profiles`)
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_disk_encryption_keys`)
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_disks`)
return err
})
}
updateHostDisks := func(t *testing.T, hostID uint, encrypted bool, updated_at time.Time) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
stmt := `UPDATE host_disks SET encrypted = ?, updated_at = ? where host_id = ?`
_, err := q.ExecContext(ctx, stmt, encrypted, updated_at, hostID)
return err
})
}
// Create some hosts
var hosts []*fleet.Host
uuidToDeviceID := map[string]string{}
for i := 0; i < 10; i++ {
p := "windows"
if i >= 5 {
p = "darwin"
}
u := uuid.New().String()
h, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: &u,
UUID: u,
Hostname: u,
Platform: p,
})
require.NoError(t, err)
require.NotNil(t, h)
hosts = append(hosts, h)
require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false))
if p == "windows" {
uuidToDeviceID[h.UUID] = windowsEnroll(t, ds, h)
}
}
t.Run("profiles summary accounts for bitlocker status", func(t *testing.T) {
t.Run("bitlocker disabled", func(t *testing.T) {
ac, err := ds.AppConfig(ctx)
require.NoError(t, err)
ac.MDM.EnableDiskEncryption = optjson.SetBool(false)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.False(t, ac.MDM.EnableDiskEncryption.Value)
expected := hostIDsByProfileStatus{}
// no hosts are counted because no profiles and disk encryption is not enabled
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending)
expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed)
expected[fleet.MDMDeliveryFailed] = []uint{hosts[1].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[2].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying)
expected[fleet.MDMDeliveryVerifying] = []uint{hosts[2].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[3].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified)
expected[fleet.MDMDeliveryVerified] = []uint{hosts[3].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[4].UUID, "some-windows-profile", nil)
// nil status is treated as pending
expected[fleet.MDMDeliveryPending] = append(expected[fleet.MDMDeliveryPending], hosts[4].ID)
checkExpected(t, nil, expected)
cleanupTables(t)
})
t.Run("bitlocker enabled", func(t *testing.T) {
ac, err := ds.AppConfig(ctx)
require.NoError(t, err)
ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.True(t, ac.MDM.EnableDiskEncryption.Value)
t.Run("bitlocker pending", func(t *testing.T) {
expected := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
// all hosts are pending because no profiles and disk encryption is enabled
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending)
// hosts[0] status pending because both profiles status and bitlocker status are pending
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed)
// status for hosts[1] now failed because any failed status determines MDM aggregate status
expected[fleet.MDMDeliveryFailed] = []uint{hosts[1].ID}
expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[2].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying)
// status for hosts[2] still pending because bitlocker pending status takes precedence over
// profiles verifying status
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[3].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified)
// status for hosts[3] still pending because bitlocker pending status takes precedence over
// profiles verified status
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[4].UUID, "some-windows-profile", nil)
// hosts[0] status pending because bitlocker status is pending and nil profile status is
// also treated as pending
checkExpected(t, nil, expected)
cleanupTables(t)
})
t.Run("bitlocker verifying", func(t *testing.T) {
expected := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
// all hosts are pending because no profiles and disk encryption is enabled
checkExpected(t, nil, expected)
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true))
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))
require.NoError(t, err)
// simulate bitlocker verifying status by ensuring host_disks updated at timestamp is before host_disk_encryption_key
updateHostDisks(t, hosts[0].ID, true, time.Now().Add(-10*time.Minute))
// status for hosts[0] now verifying because bitlocker status is verifying and host[0] has
// no profiles
expected[fleet.MDMDeliveryVerifying] = []uint{hosts[0].ID}
expected[fleet.MDMDeliveryPending] = []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed)
// status for hosts[0] now failed because any failed status takes precedence
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryFailed: []uint{hosts[0].ID},
fleet.MDMDeliveryVerifying: []uint{},
fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending)
// status for hosts[0] now pending because profiles status pendiing takes precedence over bitlocker status verifying
expected[fleet.MDMDeliveryFailed] = []uint{}
expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying)
// status for hosts[0] now verifying because both profiles status and bitlocker status are verifying
expected[fleet.MDMDeliveryPending] = []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
expected[fleet.MDMDeliveryVerifying] = []uint{hosts[0].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified)
// status for hosts[0] still verifying because bitlocker status verifying takes
// precedence over profile status verified
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", nil)
// status for hosts[0] now pending because nil profile status is treated as pending and
// pending status takes precedence over bitlocker status verifying
expected[fleet.MDMDeliveryVerifying] = []uint{}
expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
checkExpected(t, nil, expected)
cleanupTables(t)
})
t.Run("bitlocker verified", func(t *testing.T) {
expected := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
// all hosts are pending because no profiles and disk encryption is enabled
checkExpected(t, nil, expected)
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))
require.NoError(t, err)
// status is still pending because hosts_disks hasn't been updated yet
checkExpected(t, nil, expected)
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true))
// status for hosts[0] now verified because bitlocker status is verified and host[0] has
// no profiles
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryVerified: []uint{hosts[0].ID},
fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
})
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed)
// status for hosts[0] now failed because any failed status takes precedence
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryFailed: []uint{hosts[0].ID},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending)
// status for hosts[0] now pending because profiles status pendiing takes precedence over bitlocker status verified
expected[fleet.MDMDeliveryFailed] = []uint{}
expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying)
// status for hosts[0] now verifying because profiles status verifying takes precedence over
// bitlocker status verified
expected[fleet.MDMDeliveryPending] = []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
expected[fleet.MDMDeliveryVerifying] = []uint{hosts[0].ID}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified)
// status for hosts[0] now verified because both profiles status and bitlocker status are verified
expected[fleet.MDMDeliveryPending] = []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
expected[fleet.MDMDeliveryVerified] = []uint{hosts[0].ID}
expected[fleet.MDMDeliveryVerifying] = []uint{}
checkExpected(t, nil, expected)
cleanupTables(t)
})
t.Run("BitLocker host disks must update to transition from Verifying to Verified", func(t *testing.T) {
// all hosts are pending because no profiles and disk encryption is enabled
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
})
// simulate host already has encrypted disks
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true))
// manualy update host_disks for hosts[0] to encrypted and ensure updated_at
// timestamp is in the past
updateHostDisks(t, hosts[0].ID, true, time.Now().Add(-2*time.Hour))
_, err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))
require.NoError(t, err)
// status is verifying because hosts_disks hasn't been updated again
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryVerifying: []uint{hosts[0].ID},
fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
})
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true))
// status for hosts[0] now verified because SetOrUpdateHostDisksEncryption always sets host_disks.updated_at
// to the current timestamp even if the `encrypted` value hasn't changed
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryVerified: []uint{hosts[0].ID},
fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
})
cleanupTables(t)
})
t.Run("bitlocker failed", func(t *testing.T) {
expected := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
// all hosts are pending because no profiles and disk encryption is enabled
checkExpected(t, nil, expected)
_, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "", "some-bitlocker-error", nil)
require.NoError(t, err)
// status for hosts[0] now failed because any failed status takes precedence
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryFailed: []uint{hosts[0].ID},
fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed)
// status for hosts[0] still failed because any failed status takes precedence
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending)
// status for hosts[0] still failed because bitlocker status failed takes precedence
// over profiles status pending
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying)
// status for hosts[0] still failed because bitlocker status failed takes precedence
// over profiles status verifying
checkExpected(t, nil, expected)
upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified)
// status for hosts[0] still failed because bitlocker status failed takes precedence
// over profiles status verified
checkExpected(t, nil, expected)
cleanupTables(t)
})
// turn off disk encryption so that the rest of the tests can focus on profiles status
ac.MDM.EnableDiskEncryption = optjson.SetBool(false)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
})
})
t.Run("profiles summary accounts for host profiles with mixed statuses", func(t *testing.T) {
for i := 0; i < 5; i++ {
// upsert five profiles for hosts[0] with nil statuses
upsertHostProfileStatus(t, hosts[0].UUID, fmt.Sprintf("some-windows-profile-%d", i), nil)
// upsert five profiles for hosts[1] with pending statuses
upsertHostProfileStatus(t, hosts[1].UUID, fmt.Sprintf("some-windows-profile-%d", i), &fleet.MDMDeliveryPending)
// upsert five profiles for hosts[2] with verifying statuses
upsertHostProfileStatus(t, hosts[2].UUID, fmt.Sprintf("some-windows-profile-%d", i), &fleet.MDMDeliveryVerifying)
// upsert five profiles for hosts[3] with verified statuses
upsertHostProfileStatus(t, hosts[3].UUID, fmt.Sprintf("some-windows-profile-%d", i), &fleet.MDMDeliveryVerified)
// upsert five profiles for hosts[4] with failed statuses
upsertHostProfileStatus(t, hosts[4].UUID, fmt.Sprintf("some-windows-profile-%d", i), &fleet.MDMDeliveryFailed)
}
expected := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[2].ID},
fleet.MDMDeliveryVerified: []uint{hosts[3].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// add some other windows hosts that won't be be assigned any profiles
otherHosts := make([]*fleet.Host, 0, 5)
for i := 0; i < 5; i++ {
u := uuid.New().String()
h, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: &u,
UUID: u,
Hostname: u,
Platform: "windows",
})
require.NoError(t, err)
require.NotNil(t, h)
otherHosts = append(otherHosts, h)
require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false))
windowsEnroll(t, ds, h)
}
checkExpected(t, nil, expected)
// upsert some-profile-0 to failed status for hosts[0:4]
for i := 0; i < 5; i++ {
upsertHostProfileStatus(t, hosts[i].UUID, "some-windows-profile-0", &fleet.MDMDeliveryFailed)
}
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{},
fleet.MDMDeliveryVerifying: []uint{},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
checkExpected(t, nil, expected)
// upsert some-profile-0 to pending status for hosts[0:4]
for i := 0; i < 5; i++ {
upsertHostProfileStatus(t, hosts[i].UUID, "some-windows-profile-0", &fleet.MDMDeliveryPending)
}
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID},
fleet.MDMDeliveryVerifying: []uint{},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// upsert some-profile-0 to verifying status for hosts[0:4]
for i := 0; i < 5; i++ {
upsertHostProfileStatus(t, hosts[i].UUID, "some-windows-profile-0", &fleet.MDMDeliveryVerifying)
}
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[2].ID, hosts[3].ID},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// upsert some-profile-0 to verified status for hosts[0:4]
for i := 0; i < 5; i++ {
upsertHostProfileStatus(t, hosts[i].UUID, "some-windows-profile-0", &fleet.MDMDeliveryVerified)
}
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[2].ID},
fleet.MDMDeliveryVerified: []uint{hosts[3].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// turn on disk encryption
ac, err := ds.AppConfig(ctx)
require.NoError(t, err)
ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
// hosts[0:3] are now pending because disk encryption is enabled, hosts[4] is still failed,
// and other hosts are now counted as pending
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, otherHosts[0].ID, otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID},
fleet.MDMDeliveryVerifying: []uint{},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// create a new team
t1, err := ds.NewTeam(ctx, &fleet.Team{Name: uuid.NewString()})
require.NoError(t, err)
require.NotNil(t, t1)
// transfer hosts[1:2] to the team
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&t1.ID, []uint{hosts[1].ID, hosts[2].ID})))
// hosts[1:2] now counted for the team, hosts[2] is counted as verifying again because
// disk encryption is not enabled for the team
expectedTeam1 := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[1].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[2].ID},
}
checkExpected(t, &t1.ID, expectedTeam1)
// hosts[1:2] are not counted for no team
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[3].ID, otherHosts[0].ID, otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// report otherHosts[0] as a server
require.NoError(t, ds.SetOrUpdateMDMData(ctx, otherHosts[0].ID, true, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false))
// otherHosts[0] is no longer counted
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[3].ID, otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// report hosts[0] as a server
require.NoError(t, ds.SetOrUpdateMDMData(ctx, hosts[0].ID, true, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false))
// hosts[0] is no longer counted
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[3].ID, otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// unenroll hosts[3]
require.NoError(t, ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, uuidToDeviceID[hosts[3].UUID]))
// hosts[3] is no longer counted
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// report hosts[4] as enrolled to a different MDM
require.NoError(t, ds.SetOrUpdateMDMData(ctx, hosts[4].ID, false, true, "https://some-other-mdm.example.com", false, "some-other-mdm", "", false))
require.NoError(t, ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, uuidToDeviceID[hosts[4].UUID]))
// hosts[4] is no longer counted
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID},
}
checkExpected(t, nil, expected)
cleanupTables(t)
// turn off disk encryption for future tests
ac.MDM.EnableDiskEncryption = optjson.SetBool(false)
require.NoError(t, ds.SaveAppConfig(ctx, ac))
})
}
func testMDMWindowsInsertCommandForHosts(t *testing.T, ds *Datastore) {
ctx := context.Background()
d1 := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: uuid.NewString(),
}
d2 := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: uuid.NewString(),
}
err := ds.MDMWindowsInsertEnrolledDevice(ctx, d1)
require.NoError(t, err)
err = ds.MDMWindowsInsertEnrolledDevice(ctx, d2)
require.NoError(t, err)
cmd := &fleet.MDMWindowsCommand{
CommandUUID: uuid.NewString(),
RawCommand: []byte("<Exec></Exec>"),
TargetLocURI: "./test/uri",
}
err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{}, cmd)
require.NoError(t, err)
// no commands are enqueued nor created
cmds, err := ds.MDMWindowsGetPendingCommands(ctx, d1.MDMDeviceID)
require.NoError(t, err)
require.Empty(t, cmds)
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d2.MDMDeviceID)
require.NoError(t, err)
require.Empty(t, cmds)
err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{d1.HostUUID, d2.HostUUID}, cmd)
require.NoError(t, err)
// command enqueued and created
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d1.MDMDeviceID)
require.NoError(t, err)
require.Len(t, cmds, 1)
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d2.MDMDeviceID)
require.NoError(t, err)
require.Len(t, cmds, 1)
// commands can be added by device id as well
cmd.CommandUUID = uuid.NewString()
err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{d1.MDMDeviceID, d2.MDMDeviceID}, cmd)
require.NoError(t, err)
// command enqueued and created
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d1.MDMDeviceID)
require.NoError(t, err)
require.Len(t, cmds, 2)
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d2.MDMDeviceID)
require.NoError(t, err)
require.Len(t, cmds, 2)
// create a device that enrolls with the same device id and uuid as d1
// but a different hardware id (simulates the issue in #20764).
d3 := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: d1.MDMDeviceID,
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: d1.HostUUID,
}
time.Sleep(time.Second) // ensure it gets a latest created_at
err = ds.MDMWindowsInsertEnrolledDevice(ctx, d3)
require.NoError(t, err)
// commands can still be enqueued, will be enqueued for the latest enrolled device
// when a duplicate host uuid/device id exists (i.e. for d3 even if d2 is passed -
// they have the same ids).
cmd.CommandUUID = uuid.NewString()
err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{d1.MDMDeviceID, d2.MDMDeviceID}, cmd)
require.NoError(t, err)
// command enqueued and created
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d1.MDMDeviceID)
require.NoError(t, err)
require.Len(t, cmds, 3)
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d2.MDMDeviceID)
require.NoError(t, err)
require.Len(t, cmds, 3) // d2 sees the new command as we retrieve by device_id and they share the same
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d3.MDMDeviceID)
require.NoError(t, err)
require.Len(t, cmds, 3)
}
func testMDMWindowsGetPendingCommands(t *testing.T, ds *Datastore) {
ctx := context.Background()
d := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: uuid.NewString(),
}
err := ds.MDMWindowsInsertEnrolledDevice(ctx, d)
require.NoError(t, err)
// device without commands
cmds, err := ds.MDMWindowsGetPendingCommands(ctx, d.MDMDeviceID)
require.NoError(t, err)
require.Empty(t, cmds)
// device with commands
cmd := &fleet.MDMWindowsCommand{
CommandUUID: uuid.NewString(),
RawCommand: []byte("<Exec></Exec>"),
TargetLocURI: "./test/uri",
}
err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{d.HostUUID}, cmd)
require.NoError(t, err)
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d.MDMDeviceID)
require.NoError(t, err)
require.Len(t, cmds, 1)
// non-existent device
cmds, err = ds.MDMWindowsGetPendingCommands(ctx, "fail")
require.NoError(t, err)
require.Empty(t, cmds)
}
func testMDMWindowsCommandResults(t *testing.T, ds *Datastore) {
ctx := context.Background()
insertDB := func(t *testing.T, query string, args ...interface{}) (int64, error) {
t.Helper()
res, err := ds.writer(ctx).Exec(query, args...)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
dev := createMDMWindowsEnrollment(ctx, t, ds)
var enrollmentID uint
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &enrollmentID, `SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev.MDMDeviceID))
_, err := ds.writer(ctx).ExecContext(ctx,
`UPDATE mdm_windows_enrollments SET host_uuid = ? WHERE id = ?`, dev.HostUUID, enrollmentID)
require.NoError(t, err)
rawCmd := "some-command"
cmdUUID := "some-uuid"
cmdTarget := "some-target-loc-uri"
_, err = insertDB(t, `INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri) VALUES (?, ?, ?)`, cmdUUID, rawCmd, cmdTarget)
require.NoError(t, err)
rawResponse := []byte("some-response")
responseID, err := insertDB(t, `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`, enrollmentID, rawResponse)
require.NoError(t, err)
rawResult := []byte("some-result")
statusCode := "200"
_, err = insertDB(t, `INSERT INTO windows_mdm_command_results (enrollment_id, command_uuid, raw_result, response_id, status_code) VALUES (?, ?, ?, ?, ?)`, enrollmentID, cmdUUID, rawResult, responseID, statusCode)
require.NoError(t, err)
// Create multiple command queue entries to ensure no duplicated rows.
dev2 := createEnrolledDevice(t, ds)
dev3 := createEnrolledDevice(t, ds)
var enrollmentID2 uint
var enrollmentID3 uint
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &enrollmentID2,
`SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev2.MDMDeviceID))
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &enrollmentID3,
`SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev3.MDMDeviceID))
// Insert queue entry for BOTH enrollments
_, err = ds.writer(ctx).ExecContext(ctx,
`INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid)
VALUES (?, ?), (?, ?)`,
enrollmentID2, cmdUUID,
enrollmentID3, cmdUUID,
)
require.NoError(t, err)
p, err := ds.GetMDMCommandPlatform(ctx, cmdUUID)
require.NoError(t, err)
require.Equal(t, "windows", p)
results, err := ds.GetMDMWindowsCommandResults(ctx, cmdUUID, "")
require.NoError(t, err)
require.Len(t, results, 1)
require.Equal(t, dev.HostUUID, results[0].HostUUID)
require.Equal(t, cmdUUID, results[0].CommandUUID)
require.Equal(t, rawResponse, results[0].Result)
require.Equal(t, cmdTarget, results[0].RequestType)
require.Equal(t, statusCode, results[0].Status)
require.Empty(t, results[0].Hostname) // populated only at the service layer
require.Equal(t, rawCmd, string(results[0].Payload))
p, err = ds.GetMDMCommandPlatform(ctx, "unknown-cmd-uuid")
require.True(t, fleet.IsNotFound(err))
require.Empty(t, p)
results, err = ds.GetMDMWindowsCommandResults(ctx, "unknown-cmd-uuid", "")
require.NoError(t, err) // expect no error here, just no results
require.Empty(t, results)
}
func createMDMWindowsEnrollment(ctx context.Context, t *testing.T, ds *Datastore) *fleet.MDMWindowsEnrolledDevice {
h, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-win-host-name",
OsqueryHostID: ptr.String("1337"),
NodeKey: ptr.String("1337"),
UUID: "test-win-host-uuid",
Platform: "windows",
})
require.NoError(t, err)
dev := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: "test-device-id",
MDMHardwareID: "test-hardware-id",
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "dt",
MDMDeviceName: "dn",
MDMEnrollType: "et",
MDMEnrollUserID: "euid",
MDMEnrollProtoVersion: "epv",
MDMEnrollClientVersion: "ecv",
MDMNotInOOBE: false,
HostUUID: h.UUID,
}
require.NoError(t, ds.MDMWindowsInsertEnrolledDevice(ctx, dev))
return dev
}
func testMDMWindowsCommandResultsWithPendingResult(t *testing.T, ds *Datastore) {
ctx := context.Background()
insertDB := func(t *testing.T, query string, args ...interface{}) (int64, error) {
t.Helper()
res, err := ds.writer(ctx).Exec(query, args...)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
h, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-win-host-name",
OsqueryHostID: ptr.String("1337"),
NodeKey: ptr.String("1337"),
UUID: "test-win-host-uuid",
Platform: "windows",
})
require.NoError(t, err)
dev := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: "test-device-id",
MDMHardwareID: "test-hardware-id",
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "dt",
MDMDeviceName: "dn",
MDMEnrollType: "et",
MDMEnrollUserID: "euid",
MDMEnrollProtoVersion: "epv",
MDMEnrollClientVersion: "ecv",
MDMNotInOOBE: false,
HostUUID: h.UUID,
}
require.NoError(t, ds.MDMWindowsInsertEnrolledDevice(ctx, dev))
var enrollmentID uint
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &enrollmentID, `SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev.MDMDeviceID))
_, err = ds.writer(ctx).ExecContext(ctx,
`UPDATE mdm_windows_enrollments SET host_uuid = ? WHERE id = ?`, dev.HostUUID, enrollmentID)
require.NoError(t, err)
rawCmd := "some-command"
cmdUUID := "some-uuid"
cmdTarget := "some-target-loc-uri"
_, err = insertDB(t, `INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri) VALUES (?, ?, ?)`, cmdUUID, rawCmd, cmdTarget)
require.NoError(t, err)
_, err = insertDB(t, `INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid) VALUES (?, ? )`, enrollmentID, cmdUUID)
require.NoError(t, err)
p, err := ds.GetMDMCommandPlatform(ctx, cmdUUID)
require.NoError(t, err)
require.Equal(t, "windows", p)
results, err := ds.GetMDMWindowsCommandResults(ctx, cmdUUID, "")
require.NoError(t, err)
require.Len(t, results, 1)
require.Equal(t, dev.HostUUID, results[0].HostUUID)
require.Equal(t, cmdUUID, results[0].CommandUUID)
require.Equal(t, []byte{}, results[0].Result)
require.Equal(t, cmdTarget, results[0].RequestType)
require.Equal(t, "101", results[0].Status)
require.Empty(t, results[0].Hostname) // populated only at the service layer
require.Equal(t, rawCmd, string(results[0].Payload))
p, err = ds.GetMDMCommandPlatform(ctx, "unknown-cmd-uuid")
require.True(t, fleet.IsNotFound(err))
require.Empty(t, p)
results, err = ds.GetMDMWindowsCommandResults(ctx, "unknown-cmd-uuid", "")
require.NoError(t, err) // expect no error here, just no results
require.Empty(t, results)
}
// enrolls the host in Windows MDM and returns the device's enrollment ID.
func windowsEnroll(t *testing.T, ds fleet.Datastore, h *fleet.Host) string {
ctx := context.Background()
d1 := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: h.UUID,
}
err := ds.MDMWindowsInsertEnrolledDevice(ctx, d1)
require.NoError(t, err)
return d1.MDMDeviceID
}
func testMDMWindowsProfileManagement(t *testing.T, ds *Datastore) {
ctx := context.Background()
globalProfiles := []string{
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
}
// if there are no hosts, then no profiles need to be installed
profiles, err := ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
require.Empty(t, profiles)
host1, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host1-name",
OsqueryHostID: ptr.String("1337"),
NodeKey: ptr.String("1337"),
UUID: "test-uuid-1",
TeamID: nil,
Platform: "windows",
})
require.NoError(t, err)
windowsEnroll(t, ds, host1)
// non-Windows hosts shouldn't modify any of the results below
_, err = ds.NewHost(ctx, &fleet.Host{
Hostname: "test-macos-host",
OsqueryHostID: ptr.String("4824"),
NodeKey: ptr.String("4824"),
UUID: "test-macos-host",
TeamID: nil,
Platform: "macos",
})
require.NoError(t, err)
// a windows host that's not MDM enrolled into Fleet shouldn't
// modify any of the results below
_, err = ds.NewHost(ctx, &fleet.Host{
Hostname: "test-non-mdm-host",
OsqueryHostID: ptr.String("4825"),
NodeKey: ptr.String("4825"),
UUID: "test-non-mdm-host",
TeamID: nil,
Platform: "windows",
})
require.NoError(t, err)
profilesMatch := func(t *testing.T, want []string, profs []*fleet.MDMWindowsProfilePayload) {
got := []string{}
for _, prof := range profs {
got = append(got, prof.ProfileUUID)
}
require.ElementsMatch(t, want, got)
}
// global profiles to install on the newly added host
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
profilesMatch(t, globalProfiles, profiles)
// add another host, it belongs to a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"})
require.NoError(t, err)
host2, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host2-name",
OsqueryHostID: ptr.String("1338"),
NodeKey: ptr.String("1338"),
UUID: "test-uuid-2",
TeamID: &team.ID,
Platform: "windows",
})
require.NoError(t, err)
windowsEnroll(t, ds, host2)
// still the same profiles to assign as there are no profiles for team 1
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
profilesMatch(t, globalProfiles, profiles)
// assign profiles to team 1
teamProfiles := []string{
InsertWindowsProfileForTest(t, ds, team.ID),
InsertWindowsProfileForTest(t, ds, team.ID),
}
// new profiles, this time for the new host belonging to team 1
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
profilesMatch(t, append(globalProfiles, teamProfiles...), profiles)
// add another global host
host3, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host3-name",
OsqueryHostID: ptr.String("1339"),
NodeKey: ptr.String("1339"),
UUID: "test-uuid-3",
TeamID: nil,
Platform: "windows",
})
require.NoError(t, err)
windowsEnroll(t, ds, host3)
// more profiles, this time for both global hosts and the team
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
profilesMatch(t, append(globalProfiles, append(globalProfiles, teamProfiles...)...), profiles)
profileByUUID := make(map[string]*fleet.MDMWindowsProfilePayload, len(profiles))
for _, prof := range profiles {
profileByUUID[prof.ProfileUUID] = prof
}
// cron runs and updates the status
err = ds.BulkUpsertMDMWindowsHostProfiles(
ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{
{
ProfileUUID: globalProfiles[0],
ProfileName: "foo",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: profileByUUID[globalProfiles[0]].Checksum,
},
{
ProfileUUID: globalProfiles[0],
ProfileName: "foo",
HostUUID: "test-uuid-3",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: profileByUUID[globalProfiles[0]].Checksum,
},
{
ProfileUUID: globalProfiles[1],
ProfileName: "foo",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: profileByUUID[globalProfiles[1]].Checksum,
},
{
ProfileUUID: globalProfiles[1],
ProfileName: "foo",
HostUUID: "test-uuid-3",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: profileByUUID[globalProfiles[1]].Checksum,
},
{
ProfileUUID: globalProfiles[2],
ProfileName: "foo",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: profileByUUID[globalProfiles[2]].Checksum,
},
{
ProfileUUID: globalProfiles[2],
ProfileName: "foo",
HostUUID: "test-uuid-3",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: profileByUUID[globalProfiles[2]].Checksum,
},
{
ProfileUUID: teamProfiles[0],
ProfileName: "foo",
HostUUID: "test-uuid-2",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: profileByUUID[teamProfiles[0]].Checksum,
},
{
ProfileUUID: teamProfiles[1],
ProfileName: "foo",
HostUUID: "test-uuid-2",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: profileByUUID[teamProfiles[1]].Checksum,
},
},
)
require.NoError(t, err)
// no profiles left to install
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
require.Empty(t, profiles)
// no profiles to remove yet
toRemove, err := ds.ListMDMWindowsProfilesToRemove(ctx)
require.NoError(t, err)
require.Empty(t, toRemove)
// add host1 to team
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID}))
require.NoError(t, err)
// profiles to be added for host1 are now related to the team
profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
profilesMatch(t, teamProfiles, profiles)
// profiles to be removed includes host1's old profiles
toRemove, err = ds.ListMDMWindowsProfilesToRemove(ctx)
require.NoError(t, err)
profilesMatch(t, globalProfiles, toRemove)
}
func testBulkOperationsMDMWindowsHostProfiles(t *testing.T, ds *Datastore) {
ctx := context.Background()
profiles := []string{
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
}
getAllHostProfiles := func() []*fleet.MDMWindowsProfilePayload {
var hostProfiles []*fleet.MDMWindowsProfilePayload
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
stmt := `SELECT profile_uuid, status, operation_type FROM host_mdm_windows_profiles ORDER BY profile_name ASC`
return sqlx.SelectContext(ctx, q, &hostProfiles, stmt)
})
return hostProfiles
}
// empty payloads is a noop
err := ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{})
require.NoError(t, err)
require.Empty(t, getAllHostProfiles())
// valid payload inserts new records
err = ds.BulkUpsertMDMWindowsHostProfiles(
ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{
{
ProfileUUID: profiles[0],
ProfileName: "A",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{0},
},
{
ProfileUUID: profiles[1],
ProfileName: "B",
HostUUID: "test-uuid-3",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{1},
},
{
ProfileUUID: profiles[2],
ProfileName: "C",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{2},
},
{
ProfileUUID: profiles[3],
ProfileName: "D",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{3},
},
{
ProfileUUID: profiles[4],
ProfileName: "E",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerifying,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{4},
},
},
)
require.NoError(t, err)
hostsProfs := getAllHostProfiles()
require.Len(t, hostsProfs, 5)
for i, p := range hostsProfs {
require.Equal(t, profiles[i], p.ProfileUUID)
require.Equal(t, fleet.MDMOperationTypeInstall, p.OperationType)
require.Equal(t, &fleet.MDMDeliveryVerifying, p.Status)
}
// valid payload updates existing records
err = ds.BulkUpsertMDMWindowsHostProfiles(
ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{
{
ProfileUUID: profiles[0],
ProfileName: "A",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerified,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{0},
},
{
ProfileUUID: profiles[1],
ProfileName: "B",
HostUUID: "test-uuid-3",
Status: &fleet.MDMDeliveryVerified,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{1},
},
{
ProfileUUID: profiles[2],
ProfileName: "C",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerified,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{2},
},
{
ProfileUUID: profiles[3],
ProfileName: "D",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerified,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{3},
},
{
ProfileUUID: profiles[4],
ProfileName: "E",
HostUUID: "test-uuid-1",
Status: &fleet.MDMDeliveryVerified,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte{4},
},
},
)
require.NoError(t, err)
hostsProfs = getAllHostProfiles()
require.Len(t, hostsProfs, 5)
for i, p := range hostsProfs {
require.Equal(t, profiles[i], p.ProfileUUID)
require.Equal(t, fleet.MDMOperationTypeInstall, p.OperationType)
require.Equal(t, &fleet.MDMDeliveryVerified, p.Status)
}
// empty payload
err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{})
require.NoError(t, err)
hostsProfs = getAllHostProfiles()
require.Len(t, hostsProfs, 5)
// partial deletes
err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{
{
ProfileUUID: profiles[0],
HostUUID: "test-uuid-1",
},
{
ProfileUUID: profiles[1],
HostUUID: "test-uuid-3",
},
{
ProfileUUID: profiles[2],
HostUUID: "test-uuid-1",
},
})
require.NoError(t, err)
hostsProfs = getAllHostProfiles()
require.Len(t, hostsProfs, 2)
// full deletes
err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{
{
ProfileUUID: profiles[0],
HostUUID: "test-uuid-1",
},
{
ProfileUUID: profiles[1],
HostUUID: "test-uuid-3",
},
{
ProfileUUID: profiles[2],
HostUUID: "test-uuid-1",
},
{
ProfileUUID: profiles[3],
HostUUID: "test-uuid-1",
},
{
ProfileUUID: profiles[4],
HostUUID: "test-uuid-1",
},
})
require.NoError(t, err)
hostsProfs = getAllHostProfiles()
require.Len(t, hostsProfs, 0)
}
func testBulkOperationsMDMWindowsHostProfilesBatch2(t *testing.T, ds *Datastore) {
2023-11-20 15:20:08 +00:00
ds.testUpsertMDMDesiredProfilesBatchSize = 2
ds.testDeleteMDMProfilesBatchSize = 2
t.Cleanup(func() {
2023-11-20 15:20:08 +00:00
ds.testUpsertMDMDesiredProfilesBatchSize = 0
ds.testDeleteMDMProfilesBatchSize = 0
})
testBulkOperationsMDMWindowsHostProfiles(t, ds)
}
func testBulkOperationsMDMWindowsHostProfilesBatch3(t *testing.T, ds *Datastore) {
2023-11-20 15:20:08 +00:00
ds.testUpsertMDMDesiredProfilesBatchSize = 3
ds.testDeleteMDMProfilesBatchSize = 3
t.Cleanup(func() {
2023-11-20 15:20:08 +00:00
ds.testUpsertMDMDesiredProfilesBatchSize = 0
ds.testDeleteMDMProfilesBatchSize = 0
})
testBulkOperationsMDMWindowsHostProfiles(t, ds)
}
func testGetMDMWindowsProfilesContents(t *testing.T, ds *Datastore) {
ctx := context.Background()
profileUUIDs := []string{
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
}
cases := []struct {
ids []string
want map[string]fleet.MDMWindowsProfileContents
}{
{[]string{}, nil},
{nil, nil},
{
[]string{profileUUIDs[0]},
map[string]fleet.MDMWindowsProfileContents{profileUUIDs[0]: generateDummyWindowsProfileContents(profileUUIDs[0])},
},
{
[]string{profileUUIDs[0], profileUUIDs[1], profileUUIDs[2]},
map[string]fleet.MDMWindowsProfileContents{
profileUUIDs[0]: generateDummyWindowsProfileContents(profileUUIDs[0]),
profileUUIDs[1]: generateDummyWindowsProfileContents(profileUUIDs[1]),
profileUUIDs[2]: generateDummyWindowsProfileContents(profileUUIDs[2]),
},
},
}
for _, c := range cases {
out, err := ds.GetMDMWindowsProfilesContents(ctx, c.ids)
require.NoError(t, err)
require.Equal(t, c.want, out)
}
}
func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a couple Windows profiles for no-team (nil and 0 means no team)
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
profA, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "a", TeamID: nil, SyncML: []byte("<Replace></Replace>")}, nil)
require.NoError(t, err)
require.NotEmpty(t, profA.ProfileUUID)
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
profB, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "b", TeamID: ptr.Uint(0), SyncML: []byte("<Replace></Replace>")}, nil)
require.NoError(t, err)
require.NotEmpty(t, profB.ProfileUUID)
// create an Apple profile for no-team
profC, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("c", "c", 0), nil)
require.NoError(t, err)
require.NotZero(t, profC.ProfileID)
require.NotEmpty(t, profC.ProfileUUID)
// create the same name for team 1 as Windows profile
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
profATm, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "a", TeamID: ptr.Uint(1), SyncML: []byte("<Replace></Replace>")}, nil)
require.NoError(t, err)
require.NotEmpty(t, profATm.ProfileUUID)
require.NotNil(t, profATm.TeamID)
require.Equal(t, uint(1), *profATm.TeamID)
// create the same B profile for team 1 as Apple profile
profBTm, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("b", "b", 1), nil)
require.NoError(t, err)
require.NotZero(t, profBTm.ProfileID)
require.NotEmpty(t, profBTm.ProfileUUID)
var existsErr *existsError
// create a duplicate of Windows for no-team
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
_, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "b", TeamID: nil, SyncML: []byte("<Replace></Replace>")}, nil)
require.Error(t, err)
require.ErrorAs(t, err, &existsErr)
// create a duplicate of Apple for no-team
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
_, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "c", TeamID: nil, SyncML: []byte("<Replace></Replace>")}, nil)
require.Error(t, err)
require.ErrorAs(t, err, &existsErr)
// create a duplicate of Windows for team
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
_, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "a", TeamID: ptr.Uint(1), SyncML: []byte("<Replace></Replace>")}, nil)
require.Error(t, err)
require.ErrorAs(t, err, &existsErr)
// create a duplicate of Apple for team
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
_, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "b", TeamID: ptr.Uint(1), SyncML: []byte("<Replace></Replace>")}, nil)
require.Error(t, err)
require.ErrorAs(t, err, &existsErr)
// create a duplicate name with an Apple profile for no-team
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 0), nil)
require.Error(t, err)
require.ErrorAs(t, err, &existsErr)
// create a duplicate name with an Apple profile for team
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 1), nil)
require.Error(t, err)
require.ErrorAs(t, err, &existsErr)
// create a profile with labels that don't exist
_, err = ds.NewMDMWindowsConfigProfile(
ctx,
fleet.MDMWindowsConfigProfile{
Name: "fake-labels",
TeamID: nil,
SyncML: []byte("<Replace></Replace>"),
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: "foo", LabelID: 1}},
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
}, nil)
require.NotNil(t, err)
require.True(t, fleet.IsForeignKey(err))
label := &fleet.Label{
Name: "my label",
Description: "a label",
Query: "select 1 from processes;",
}
label, err = ds.NewLabel(ctx, label)
require.NoError(t, err)
// create a profile with a label that exists
profWithLabel, err := ds.NewMDMWindowsConfigProfile(
ctx,
fleet.MDMWindowsConfigProfile{
Name: "with-labels",
TeamID: nil,
SyncML: []byte("<Replace></Replace>"),
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: label.Name, LabelID: label.ID}},
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
}, nil)
require.NoError(t, err)
require.NotEmpty(t, profWithLabel.ProfileUUID)
// get that profile with label
prof, err := ds.GetMDMWindowsConfigProfile(ctx, profWithLabel.ProfileUUID)
require.NoError(t, err)
require.Len(t, prof.LabelsIncludeAll, 1)
require.Equal(t, label.Name, prof.LabelsIncludeAll[0].LabelName)
require.Equal(t, label.ID, prof.LabelsIncludeAll[0].LabelID)
require.False(t, prof.LabelsIncludeAll[0].Broken)
// break that profile by deleting the label
require.NoError(t, ds.DeleteLabel(ctx, label.Name, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}))
prof, err = ds.GetMDMWindowsConfigProfile(ctx, profWithLabel.ProfileUUID)
require.NoError(t, err)
require.Len(t, prof.LabelsIncludeAll, 1)
require.Equal(t, label.Name, prof.LabelsIncludeAll[0].LabelName)
require.Zero(t, prof.LabelsIncludeAll[0].LabelID)
require.True(t, prof.LabelsIncludeAll[0].Broken)
_, err = ds.GetMDMWindowsConfigProfile(ctx, "not-valid")
require.Error(t, err)
require.True(t, fleet.IsNotFound(err))
prof, err = ds.GetMDMWindowsConfigProfile(ctx, profA.ProfileUUID)
require.NoError(t, err)
require.Equal(t, profA.ProfileUUID, prof.ProfileUUID)
require.NotNil(t, prof.TeamID)
require.Zero(t, *prof.TeamID)
require.Equal(t, "a", prof.Name)
require.Equal(t, "<Replace></Replace>", string(prof.SyncML))
require.NotZero(t, prof.CreatedAt)
require.NotZero(t, prof.UploadedAt)
require.Nil(t, prof.LabelsIncludeAll)
err = ds.DeleteMDMWindowsConfigProfile(ctx, "not-valid")
require.Error(t, err)
require.True(t, fleet.IsNotFound(err))
err = ds.DeleteMDMWindowsConfigProfile(ctx, profA.ProfileUUID)
require.NoError(t, err)
}
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
func testMDMWindowsConfigProfilesWithFleetVars(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Test that usesFleetVars parameter correctly persists variables in the database
// Create a profile with Fleet variables
profWithVars, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
Name: "profile_with_vars",
TeamID: nil,
SyncML: []byte("<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Test/$FLEET_VAR_HOST_UUID</LocURI></Target></Item></Replace>"),
}, []fleet.FleetVarName{fleet.FleetVarHostUUID})
require.NoError(t, err)
require.NotEmpty(t, profWithVars.ProfileUUID)
// Query the mdm_configuration_profile_variables table to verify the variables were persisted
var varNames []string
stmt := `
SELECT fv.name
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
FROM mdm_configuration_profile_variables mcpv
JOIN fleet_variables fv ON mcpv.fleet_variable_id = fv.id
WHERE mcpv.windows_profile_uuid = ?
ORDER BY fv.name
`
err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, profWithVars.ProfileUUID)
require.NoError(t, err)
// Assert that the returned variable names exactly match the provided slice
// Note: the database stores the full name with FLEET_VAR_ prefix
expectedVarNames := []string{"FLEET_VAR_" + string(fleet.FleetVarHostUUID)}
require.Equal(t, expectedVarNames, varNames, "Variable names in database should match the provided usesFleetVars slice")
// Test with empty usesFleetVars slice
profNoVars, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
Name: "profile_no_vars",
TeamID: nil,
SyncML: []byte("<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Test/NoVars</LocURI></Target></Item></Replace>"),
}, []fleet.FleetVarName{})
require.NoError(t, err)
require.NotEmpty(t, profNoVars.ProfileUUID)
err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, profNoVars.ProfileUUID)
require.NoError(t, err)
require.Empty(t, varNames, "No variables should be persisted when usesFleetVars is empty")
// Test with nil usesFleetVars
profNilVars, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
Name: "profile_nil_vars",
TeamID: nil,
SyncML: []byte("<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Test/NilVars</LocURI></Target></Item></Replace>"),
}, nil)
require.NoError(t, err)
require.NotEmpty(t, profNilVars.ProfileUUID)
err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, profNilVars.ProfileUUID)
require.NoError(t, err)
require.Empty(t, varNames, "No variables should be persisted when usesFleetVars is nil")
// Test that BatchSetMDMProfiles properly clears stale variable associations
// Create a team profile with variables
teamProf1, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
Name: "team_profile_1",
TeamID: ptr.Uint(1),
SyncML: []byte("<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Test/$FLEET_VAR_HOST_UUID</LocURI></Target></Item></Replace>"),
}, []fleet.FleetVarName{fleet.FleetVarHostUUID})
require.NoError(t, err)
// Verify the variable was persisted
err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, teamProf1.ProfileUUID)
require.NoError(t, err)
require.Equal(t, expectedVarNames, varNames, "Team profile should have HOST_UUID variable")
// Now update the profile via BatchSetMDMProfiles to remove the variable
teamProf1Updated := &fleet.MDMWindowsConfigProfile{
Name: "team_profile_1",
TeamID: ptr.Uint(1),
SyncML: []byte("<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Test/NoVarsAnymore</LocURI></Target></Item></Replace>"),
}
// BatchSetMDMProfiles should process this profile and clear its variable associations
// since the content no longer contains variables
_, err = ds.BatchSetMDMProfiles(ctx, ptr.Uint(1), nil, []*fleet.MDMWindowsConfigProfile{teamProf1Updated}, nil, nil, nil)
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
require.NoError(t, err)
// Verify the variable associations were cleared
err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, teamProf1.ProfileUUID)
require.NoError(t, err)
require.Empty(t, varNames, "Variables should be cleared when profile is updated without variables")
// Create another team profile to test multiple profiles with mixed variables
teamProf2, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
Name: "team_profile_2",
TeamID: ptr.Uint(1),
SyncML: []byte("<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Test/Profile2</LocURI></Target></Item></Replace>"),
}, nil)
require.NoError(t, err)
// Update both profiles - one adds variables, one keeps no variables
teamProf1WithVarsAgain := &fleet.MDMWindowsConfigProfile{
Name: "team_profile_1",
TeamID: ptr.Uint(1),
SyncML: []byte("<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Test/WithVarsAgain/$FLEET_VAR_HOST_UUID</LocURI></Target></Item></Replace>"),
}
teamProf2NoChange := &fleet.MDMWindowsConfigProfile{
Name: "team_profile_2",
TeamID: ptr.Uint(1),
SyncML: []byte("<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Test/Profile2Updated</LocURI></Target></Item></Replace>"),
}
// Mock the profilesVariablesByIdentifier that would be passed from service layer
profilesVars := []fleet.MDMProfileIdentifierFleetVariables{
{Identifier: "team_profile_1", FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostUUID}},
}
_, err = ds.BatchSetMDMProfiles(ctx, ptr.Uint(1), nil, []*fleet.MDMWindowsConfigProfile{teamProf1WithVarsAgain, teamProf2NoChange}, nil, nil, profilesVars)
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
require.NoError(t, err)
// Verify profile 1 has variables again
err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, teamProf1.ProfileUUID)
require.NoError(t, err)
require.Equal(t, expectedVarNames, varNames, "Profile 1 should have variables again")
// Verify profile 2 still has no variables
err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, teamProf2.ProfileUUID)
require.NoError(t, err)
require.Empty(t, varNames, "Profile 2 should still have no variables")
}
func testSetOrReplaceMDMWindowsConfigProfile(t *testing.T, ds *Datastore) {
ctx := context.Background()
getProfileByTeamAndName := func(tmID *uint, name string) *fleet.MDMWindowsConfigProfile {
var prof fleet.MDMWindowsConfigProfile
var teamID uint
if tmID != nil {
teamID = *tmID
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &prof,
`SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`,
teamID, name)
})
return &prof
}
// nothing for no-team, nothing for team 1
expectWindowsProfiles(t, ds, nil, nil)
expectWindowsProfiles(t, ds, ptr.Uint(1), nil)
// create a profile for no-team
cp1 := *windowsConfigProfileForTest(t, "N1", "N1")
err := ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp1)
require.NoError(t, err)
profNoTmN1 := getProfileByTeamAndName(nil, "N1")
// creating the same profile for Apple / no-team fails
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("N1", "I1", 0), nil)
require.Error(t, err)
cp1.UploadedAt = profNoTmN1.UploadedAt
profs1 := expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp1})
// wait a second to ensure timestamps in the DB change
time.Sleep(time.Second)
// update the profile content for no-team
cp2 := *windowsConfigProfileForTest(t, "N1", "N1.modified")
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp2)
require.NoError(t, err)
profNoTmN1b := getProfileByTeamAndName(nil, "N1")
profs2 := expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2})
// profile UUIDs are the same
require.Equal(t, profs1["N1"], profs2["N1"])
// uploaded_at is not the same
require.False(t, profNoTmN1.UploadedAt.Equal(profNoTmN1b.UploadedAt))
// wait a second to ensure timestamps in the DB change
time.Sleep(time.Second)
// update the profile for no-team without change
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp2)
require.NoError(t, err)
cp2.UploadedAt = profNoTmN1b.UploadedAt
expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2})
// create a profile for Apple and team 1 with that name works
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("N1", "I1", 1), nil)
require.NoError(t, err)
// try to create that profile for Windows and team 1 fails
cp3 := *windowsConfigProfileForTest(t, "N1", "N1")
cp3.TeamID = ptr.Uint(1)
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp3)
require.Error(t, err)
expectWindowsProfiles(t, ds, ptr.Uint(1), nil)
// create a profile with the same name for team 2 works
cp4 := *windowsConfigProfileForTest(t, "N1", "N1")
cp4.TeamID = ptr.Uint(2)
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp4)
require.NoError(t, err)
profs3 := expectWindowsProfiles(t, ds, ptr.Uint(2), []*fleet.MDMWindowsConfigProfile{&cp4})
// profile UUIDs are not the same as for no-team
require.NotEqual(t, profs3["N1"], profs2["N1"])
// create a different profile for no-team
cp5 := *windowsConfigProfileForTest(t, "N2", "N2")
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp5)
require.NoError(t, err)
expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2, &cp5})
// update that profile for no-team
cp6 := *windowsConfigProfileForTest(t, "N2", "N2.modified")
err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp6)
require.NoError(t, err)
expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2, &cp6})
}
func testMDMWindowsProfileLabels(t *testing.T, ds *Datastore) {
ctx := context.Background()
// Create a windows host
u := uuid.New().String()
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
// Set this slightly in the past to test dynamic label exclusion
LabelUpdatedAt: time.Now().Add(-5 * time.Second),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: &u,
UUID: u,
Hostname: u,
Platform: "windows",
})
require.NoError(t, err)
windowsEnroll(t, ds, host)
// "include-any" labels
l1, err := ds.NewLabel(ctx, &fleet.Label{
Name: "include-any-label1",
Description: "desc",
Query: "select 1;",
})
require.NoError(t, err)
l2, err := ds.NewLabel(ctx, &fleet.Label{
Name: "include-any-label2",
Description: "desc",
Query: "select 1;",
})
require.NoError(t, err)
l3, err := ds.NewLabel(ctx, &fleet.Label{
Name: "include-any-label3",
Description: "desc",
Query: "select 1;",
})
require.NoError(t, err)
// include-all labels
l4, err := ds.NewLabel(ctx, &fleet.Label{
Name: "include-all-label4",
Description: "desc",
Query: "select 1;",
})
require.NoError(t, err)
l5, err := ds.NewLabel(ctx, &fleet.Label{
Name: "include-all-label5",
Description: "desc",
Query: "select 1;",
})
require.NoError(t, err)
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
// exclude-any labels
l6, err := ds.NewLabel(ctx, &fleet.Label{
Name: "exclude-any-label6",
Description: "desc",
Query: "select 1;",
})
require.NoError(t, err)
l7, err := ds.NewLabel(ctx, &fleet.Label{
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
Name: "exclude-any-label7",
Description: "desc",
LabelMembershipType: fleet.LabelMembershipTypeManual,
})
require.NoError(t, err)
// Create a profile with "include-any" with l1
includeAnyProf, err := ds.NewMDMWindowsConfigProfile(
ctx,
*windowsConfigProfileForTest(t, "prof-include-any", "./Foo/Bar", l1, l2, l3),
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
nil,
)
require.NoError(t, err)
require.NotEmpty(t, includeAnyProf.ProfileUUID)
profileChecksums := make(map[string][]byte)
checksum := md5.Sum(includeAnyProf.SyncML) // nolint:gosec // used only to hash for efficient comparisons
profileChecksums[includeAnyProf.ProfileUUID] = checksum[:]
// Create a profile with "include-all" with l4 and l5
includeAllProf, err := ds.NewMDMWindowsConfigProfile(
ctx,
*windowsConfigProfileForTest(t, "prof-include-all", "./Foo/Bar", l4, l5),
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
nil,
)
require.NoError(t, err)
require.NotEmpty(t, includeAllProf.ProfileUUID)
checksum = md5.Sum(includeAllProf.SyncML) // nolint:gosec // used only to hash for efficient comparisons
profileChecksums[includeAllProf.ProfileUUID] = checksum[:]
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
// Create a profile with "exclude-any" with l6 and l7
excludeAnyProf, err := ds.NewMDMWindowsConfigProfile(
ctx,
*windowsConfigProfileForTest(t, "prof-exclude-any", "./Foo/Bar", l6, l7),
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
nil,
)
require.NoError(t, err)
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
checksum = md5.Sum(excludeAnyProf.SyncML) // nolint:gosec // used only to hash for efficient comparisons
profileChecksums[excludeAnyProf.ProfileUUID] = checksum[:]
// Create a profile with "exclude-any" with l7 only since it is a manual label
excludeAnyManualProf, err := ds.NewMDMWindowsConfigProfile(
ctx,
*windowsConfigProfileForTest(t, "prof-exclude-any-manual", "./Foo/Bar", l7),
nil,
)
require.NoError(t, err)
checksum = md5.Sum(excludeAnyManualProf.SyncML) // nolint:gosec // used only to hash for efficient comparisons
profileChecksums[excludeAnyManualProf.ProfileUUID] = checksum[:]
// Connect the host and l1, l4, l5
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l1.ID, host.ID}, {l4.ID, host.ID}, {l5.ID, host.ID}})
require.NoError(t, err)
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
// We should see 3 profiles in the "to install" list
profilesToInstall, err := ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
{
ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[includeAllProf.ProfileUUID],
},
{
ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[includeAnyProf.ProfileUUID],
},
{
ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID],
},
}, profilesToInstall)
host.LabelUpdatedAt = time.Now().Add(1 * time.Second)
2024-11-06 15:13:47 +00:00
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
// We should see all 4 profiles in the "to install" list
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
{
ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[includeAllProf.ProfileUUID],
},
{
ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[includeAnyProf.ProfileUUID],
},
{
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
ProfileUUID: excludeAnyProf.ProfileUUID, ProfileName: excludeAnyProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyProf.ProfileUUID],
},
{
ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID],
},
}, profilesToInstall)
// Remove the l1<->host relationship, but add l2<->labelHost. The profile should still show
// up since it's "include any"
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l1.ID, host.ID}})
require.NoError(t, err)
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l2.ID, host.ID}})
require.NoError(t, err)
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
{
ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[includeAllProf.ProfileUUID],
},
{
ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[includeAnyProf.ProfileUUID],
},
{
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
ProfileUUID: excludeAnyProf.ProfileUUID, ProfileName: excludeAnyProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyProf.ProfileUUID],
},
{
ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID],
},
}, profilesToInstall)
// Remove the l2<->host relationship. Since the profile is "include-any", it should no longer
// show up
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l2.ID, host.ID}})
require.NoError(t, err)
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
{
ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[includeAllProf.ProfileUUID],
},
{
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
ProfileUUID: excludeAnyProf.ProfileUUID, ProfileName: excludeAnyProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyProf.ProfileUUID],
},
{
ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID],
},
}, profilesToInstall)
// Remove the l4<->host relationship. Since the profile is "include-all", it should no longer show
// up even though the l5<->host connection is still there.
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l4.ID, host.ID}})
require.NoError(t, err)
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
{
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
ProfileUUID: excludeAnyProf.ProfileUUID, ProfileName: excludeAnyProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyProf.ProfileUUID],
},
{
ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID],
},
}, profilesToInstall)
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
// Add a l6<->host relationship. The exclude-any profile with l6 and l7 should be gone now with only
// the exclude-any-manual profile remaining.
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l6.ID, host.ID}})
require.NoError(t, err)
profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx)
require.NoError(t, err)
Fix exclude-any label scoping on non-osquery platforms (#35353) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33132 Changes label scoping logic to handle manual labels without needing the host's label_updated_at at all and to update Android/iOS hosts' label_updated_at on checkins so they update at a similar cadence to platforms where they're actually supported and should we ever support queries on those hosts should "just work" # 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) - [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] 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [x] Alerted the release DRI if additional load testing is needed --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-11-12 15:16:01 +00:00
require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{
{
ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID,
Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID],
},
}, profilesToInstall)
}
func expectWindowsProfiles(
t *testing.T,
ds *Datastore,
tmID *uint,
want []*fleet.MDMWindowsConfigProfile,
) map[string]string {
if tmID == nil {
tmID = ptr.Uint(0)
}
var got []*fleet.MDMWindowsConfigProfile
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
ctx := context.Background()
return sqlx.SelectContext(ctx, q, &got,
`SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ?`,
tmID)
})
// create map of expected profiles keyed by name
wantMap := make(map[string]*fleet.MDMWindowsConfigProfile, len(want))
for _, cp := range want {
wantMap[cp.Name] = cp
}
// compare only the fields we care about, and build the resulting map of
// profile name as key to profile UUID as value
m := make(map[string]string)
for _, gotp := range got {
m[gotp.Name] = gotp.ProfileUUID
if gotp.TeamID != nil && *gotp.TeamID == 0 {
gotp.TeamID = nil
}
// ProfileUUID is non-empty and starts with "w", but otherwise we don't
// care about it for test assertions.
require.NotEmpty(t, gotp.ProfileUUID)
require.True(t, strings.HasPrefix(gotp.ProfileUUID, "w"))
gotp.ProfileUUID = ""
gotp.CreatedAt = time.Time{}
// if an expected uploaded_at timestamp is provided for this profile, keep
// its value, otherwise clear it as we don't care about asserting its
// value.
if wantp := wantMap[gotp.Name]; wantp == nil || wantp.UploadedAt.IsZero() {
gotp.UploadedAt = time.Time{}
}
}
// order is not guaranteed
require.ElementsMatch(t, want, got)
return m
}
func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
ctx := context.Background()
applyAndExpect := func(newSet []*fleet.MDMWindowsConfigProfile, tmID *uint, want []*fleet.MDMWindowsConfigProfile,
wantUpdated bool,
) map[string]string {
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
updatedDB, err := ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet, nil)
require.NoError(t, err)
assert.Equal(t, wantUpdated, updatedDB)
return err
})
require.NoError(t, err)
return expectWindowsProfiles(t, ds, tmID, want)
}
getProfileByTeamAndName := func(tmID *uint, name string) *fleet.MDMWindowsConfigProfile {
var prof fleet.MDMWindowsConfigProfile
var teamID uint
if tmID != nil {
teamID = *tmID
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &prof,
`SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`,
teamID, name)
})
return &prof
}
withTeamID := func(p *fleet.MDMWindowsConfigProfile, tmID uint) *fleet.MDMWindowsConfigProfile {
p.TeamID = &tmID
return p
}
withUploadedAt := func(p *fleet.MDMWindowsConfigProfile, ua time.Time) *fleet.MDMWindowsConfigProfile {
p.UploadedAt = ua
return p
}
// apply empty set for no-team
applyAndExpect(nil, nil, nil, false)
// apply single profile set for tm1
mTm1 := applyAndExpect([]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N1", "l1"),
}, ptr.Uint(1), []*fleet.MDMWindowsConfigProfile{
withTeamID(windowsConfigProfileForTest(t, "N1", "l1"), 1),
}, true)
profTm1N1 := getProfileByTeamAndName(ptr.Uint(1), "N1")
// apply single profile set for no-team
applyAndExpect([]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N1", "l1"),
}, nil, []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N1", "l1"),
}, true)
// wait a second to ensure timestamps in the DB change
time.Sleep(time.Second)
// apply new profile set for tm1
mTm1b := applyAndExpect([]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N1", "l1"), // unchanged
windowsConfigProfileForTest(t, "N2", "l2"),
}, ptr.Uint(1), []*fleet.MDMWindowsConfigProfile{
withUploadedAt(withTeamID(windowsConfigProfileForTest(t, "N1", "l1"), 1), profTm1N1.UploadedAt),
withTeamID(windowsConfigProfileForTest(t, "N2", "l2"), 1),
}, true)
// uuid for N1-I1 is unchanged
require.Equal(t, mTm1["I1"], mTm1b["I1"])
profTm1N2 := getProfileByTeamAndName(ptr.Uint(1), "N2")
// wait a second to ensure timestamps in the DB change
time.Sleep(time.Second)
// apply edited profile (by content only), unchanged profile and new profile
// for tm1
mTm1c := applyAndExpect([]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N1", "l1b"), // content updated
windowsConfigProfileForTest(t, "N2", "l2"), // unchanged
windowsConfigProfileForTest(t, "N3", "l3"), // new
}, ptr.Uint(1), []*fleet.MDMWindowsConfigProfile{
withTeamID(windowsConfigProfileForTest(t, "N1", "l1b"), 1),
withUploadedAt(withTeamID(windowsConfigProfileForTest(t, "N2", "l2"), 1), profTm1N2.UploadedAt),
withTeamID(windowsConfigProfileForTest(t, "N3", "l3"), 1),
}, true)
// uuid for N1-I1 is unchanged
require.Equal(t, mTm1b["I1"], mTm1c["I1"])
// uuid for N2-I2 is unchanged
require.Equal(t, mTm1b["I2"], mTm1c["I2"])
profTm1N1c := getProfileByTeamAndName(ptr.Uint(1), "N1")
// uploaded-at was modified because the content changed
require.False(t, profTm1N1.UploadedAt.Equal(profTm1N1c.UploadedAt))
// apply only new profiles to no-team
applyAndExpect([]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N4", "l4"),
windowsConfigProfileForTest(t, "N5", "l5"),
}, nil, []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N4", "l4"),
windowsConfigProfileForTest(t, "N5", "l5"),
}, true)
// apply the same thing again -- nothing updated
applyAndExpect([]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N4", "l4"),
windowsConfigProfileForTest(t, "N5", "l5"),
}, nil, []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N4", "l4"),
windowsConfigProfileForTest(t, "N5", "l5"),
}, false)
// Change the content of one profile -- update expected
applyAndExpect([]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N4", "l4b"),
windowsConfigProfileForTest(t, "N5", "l5"),
}, nil, []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "N4", "l4b"),
windowsConfigProfileForTest(t, "N5", "l5"),
}, true)
// clear profiles for tm1
applyAndExpect(nil, ptr.Uint(1), nil, true)
}
// if the label name starts with "exclude-", the label is considered an "exclude-any", otherwise
// it is an "include-all".
func windowsConfigProfileForTest(t *testing.T, name, locURI string, labels ...*fleet.Label) *fleet.MDMWindowsConfigProfile {
prof := &fleet.MDMWindowsConfigProfile{
Name: name,
SyncML: []byte(fmt.Sprintf(`
<Replace>
2023-11-29 14:32:42 +00:00
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
</Item>
</Replace>
`, locURI)),
}
for _, lbl := range labels {
switch {
case strings.HasPrefix(lbl.Name, "exclude-"):
prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
case strings.HasPrefix(lbl.Name, "include-any-"):
prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
default:
prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
}
}
return prof
}
func testSaveResponse(t *testing.T, ds *Datastore) {
// Set up: 3 devices, 1 command, 1 response for 1 device
enrolledDevice1 := createEnrolledDevice(t, ds)
enrolledDevice2 := createEnrolledDevice(t, ds)
enrolledDevice3 := createEnrolledDevice(t, ds)
atomicCommandUUID := uuid.NewString()
replaceCommandUUID := uuid.NewString()
cmd := &fleet.MDMWindowsCommand{
CommandUUID: atomicCommandUUID,
RawCommand: []byte(fmt.Sprintf(`
<Atomic>
<!-- CmdID generated by Fleet -->
<CmdID>%s</CmdID>
<Replace>
<!-- CmdID generated by Fleet -->
<CmdID>%s</CmdID>
<Item>
<Target>
<LocURI>./Device/Vendor/MSFT/Policy/Config/System/DisableOneDriveFileSync</LocURI>
</Target>
<Meta>
<Format
xmlns="syncml:metinf">int
</Format>
</Meta>
<Data>1</Data>
</Item>
</Replace>
</Atomic>
`, atomicCommandUUID, replaceCommandUUID)),
TargetLocURI: "",
}
err := ds.mdmWindowsInsertCommandForHostsDB(context.Background(), ds.primary,
[]string{enrolledDevice1.MDMDeviceID, enrolledDevice2.MDMDeviceID, enrolledDevice3.MDMDeviceID}, cmd)
require.NoError(t, err)
// We only found a batch update method, so we are using a single statement here to insert host profile, for simplicity.
ExecAdhocSQL(t, ds, func(t sqlx.ExtContext) error {
_, err := t.ExecContext(context.Background(), `
INSERT INTO host_mdm_windows_profiles (host_uuid, status, operation_type, command_uuid, profile_name, profile_uuid)
VALUES (?, 'pending', 'install', ?, 'disable-onedrive', ?)`, enrolledDevice1.HostUUID, atomicCommandUUID, uuid.NewString())
return err
})
enrichedSyncML := createResponseAsEnrichedSyncML(t, enrolledDevice1, atomicCommandUUID, replaceCommandUUID)
// Do test
err = ds.MDMWindowsSaveResponse(context.Background(), enrolledDevice1.MDMDeviceID, enrichedSyncML, []string{})
require.NoError(t, err)
// Verify results
results, err := ds.GetMDMWindowsCommandResults(context.Background(), cmd.CommandUUID, "")
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, enrolledDevice1.HostUUID, results[0].HostUUID)
assert.Equal(t, cmd.CommandUUID, results[0].CommandUUID)
assert.Equal(t, "200", results[0].Status)
var count int
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ?",
atomicCommandUUID)
})
assert.Equal(t, 2, count, "Only one device has responded, so the command should still be in the queue")
// Finish setting up the second device for testing
ExecAdhocSQL(t, ds, func(t sqlx.ExtContext) error {
_, err := t.ExecContext(context.Background(), `
INSERT INTO host_mdm_windows_profiles (host_uuid, status, operation_type, command_uuid, profile_name, profile_uuid)
VALUES (?, 'pending', 'install', ?, 'disable-onedrive', ?)`, enrolledDevice2.HostUUID, atomicCommandUUID, uuid.NewString())
return err
})
enrichedSyncML2 := createResponseAsEnrichedSyncML(t, enrolledDevice2, atomicCommandUUID, replaceCommandUUID)
// Do test on the second device
err = ds.MDMWindowsSaveResponse(context.Background(), enrolledDevice2.MDMDeviceID, enrichedSyncML2, []string{})
require.NoError(t, err)
// Verify results for the second device
results, err = ds.GetMDMWindowsCommandResults(context.Background(), cmd.CommandUUID, "")
require.NoError(t, err)
require.Len(t, results, 2)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ?",
atomicCommandUUID)
})
assert.Equal(t, count, 1, "Two out of three have responded, so the command should still be in the queue for the last host")
// Third device, which in our test case failed and will have it's command resent
ExecAdhocSQL(t, ds, func(t sqlx.ExtContext) error {
_, err := t.ExecContext(context.Background(), `
INSERT INTO host_mdm_windows_profiles (host_uuid, status, operation_type, command_uuid, profile_name, profile_uuid)
VALUES (?, 'pending', 'install', ?, 'disable-onedrive', ?)`, enrolledDevice3.HostUUID, atomicCommandUUID, uuid.NewString())
return err
})
enrichedSyncML3 := createResponseAsEnrichedSyncML(t, enrolledDevice3, atomicCommandUUID, replaceCommandUUID)
// Do test on the third device
err = ds.MDMWindowsSaveResponse(context.Background(), enrolledDevice3.MDMDeviceID, enrichedSyncML3, []string{atomicCommandUUID})
require.NoError(t, err)
// Verify results does not exist for the third device
results, err = ds.GetMDMWindowsCommandResults(context.Background(), cmd.CommandUUID, "")
require.NoError(t, err)
require.Len(t, results, 2) // still two
for _, res := range results {
assert.NotEqual(t, enrolledDevice3.HostUUID, res.HostUUID, "Host 3 should not have a result recorded")
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ?",
atomicCommandUUID)
})
// We still expect one here, as the clearing of the command from the queue will happen in the resend flow.
assert.Equal(t, count, 1, "All devices have responded, so the command should be completely removed from the queue")
}
func createResponseAsEnrichedSyncML(t *testing.T, enrolledDevice *fleet.MDMWindowsEnrolledDevice, atomicCommandUUID string,
replaceCommandUUID string,
) fleet.EnrichedSyncML {
rawResponse := fmt.Sprintf(`
<SyncML
xmlns="SYNCML:SYNCML1.2">
<SyncHdr>
<VerDTD>1.2</VerDTD>
<VerProto>DM/1.2</VerProto>
<SessionID>81</SessionID>
<MsgID>2</MsgID>
<Target>
<LocURI>https://example.com/api/mdm/microsoft/management</LocURI>
</Target>
<Source>
<LocURI>%s</LocURI>
</Source>
</SyncHdr>
<SyncBody>
<Status>
<CmdID>1</CmdID>
<MsgRef>1</MsgRef>
<CmdRef>0</CmdRef>
<Cmd>SyncHdr</Cmd>
<Data>200</Data>
</Status>
<Status>
<CmdID>2</CmdID>
<MsgRef>1</MsgRef>
<CmdRef>%s</CmdRef>
<Cmd>Atomic</Cmd>
<Data>200</Data>
</Status>
<Status>
<CmdID>3</CmdID>
<MsgRef>1</MsgRef>
<CmdRef>%s</CmdRef>
<Cmd>Replace</Cmd>
<Data>200</Data>
</Status>
<Final/>
</SyncBody>
</SyncML>
`, enrolledDevice.MDMDeviceID, atomicCommandUUID, replaceCommandUUID)
syncML := &fleet.SyncML{}
err := xml.Unmarshal([]byte(rawResponse), syncML)
require.NoError(t, err)
syncML.Raw = []byte(rawResponse)
enrichedSyncML := fleet.NewEnrichedSyncML(syncML)
return enrichedSyncML
}
func createEnrolledDevice(t *testing.T, ds *Datastore) *fleet.MDMWindowsEnrolledDevice {
enrolledDevice := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: uuid.NewString(),
}
err := ds.MDMWindowsInsertEnrolledDevice(context.Background(), enrolledDevice)
require.NoError(t, err)
return enrolledDevice
}
func testSetMDMWindowsProfilesWithVariables(t *testing.T, ds *Datastore) {
// NOTE: as of this code being written, Fleet variables are not yet supported
// in Windows profiles, but the profile-variable batch-association function
// is already implemented as platform-independent (as it was not
// harder/longer to do this way). This just sanity-checks that the function
// works as expected for Windows.
ctx := context.Background()
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
checkProfileVariables := func(profUUID string, teamID uint, wantVars []fleet.FleetVarName) {
var gotVars []string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &gotVars, `
SELECT
fv.name
FROM
mdm_windows_configuration_profiles mwcp
INNER JOIN mdm_configuration_profile_variables mcpv ON mwcp.profile_uuid = mcpv.windows_profile_uuid
INNER JOIN fleet_variables fv ON mcpv.fleet_variable_id = fv.id
WHERE
mwcp.name = ? AND
mwcp.team_id = ?`, "name-"+profUUID, teamID) // test profiles are created with a name = "name-" + uuid
})
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
wantVarStrings := make([]string, len(wantVars))
for i := range wantVars {
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
wantVarStrings[i] = "FLEET_VAR_" + string(wantVars[i])
}
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
require.ElementsMatch(t, wantVarStrings, gotVars)
}
globalProfiles := []string{
InsertWindowsProfileForTest(t, ds, 0),
InsertWindowsProfileForTest(t, ds, 0),
}
// both profiles have no variable
_, err := batchSetProfileVariableAssociationsDB(ctx, ds.writer(ctx), []fleet.MDMProfileUUIDFleetVariables{
{ProfileUUID: globalProfiles[0], FleetVariables: nil},
{ProfileUUID: globalProfiles[1], FleetVariables: nil},
}, "windows")
require.NoError(t, err)
checkProfileVariables(globalProfiles[0], 0, nil)
checkProfileVariables(globalProfiles[1], 0, nil)
// add some variables
_, err = batchSetProfileVariableAssociationsDB(ctx, ds.writer(ctx), []fleet.MDMProfileUUIDFleetVariables{
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
{ProfileUUID: globalProfiles[0], FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}},
{ProfileUUID: globalProfiles[1], FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}},
}, "windows")
require.NoError(t, err)
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695) Fixes #30879 Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc I added a `FleetVarName` type, which should improve safety/maintainability, but that resulted in a lot of files touched. I also added the following. However, these are not strictly needed for this feature (only useful for debug right now). But we are following the pattern created by MDM team. 1. Add the migration to insert HOST_UUID into fleet_variables 2. Update the Windows profile save logic to populate mdm_configuration_profile_variables # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation] - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM configuration profiles, enabling per-host customization during profile deployment. * Enhanced profile delivery by substituting Fleet variables with actual host data in Windows profiles. * Introduced a database migration to register the new Fleet variable for host UUID. * **Bug Fixes** * Improved validation and error handling to reject unsupported Fleet variables in Windows MDM profiles with detailed messages. * Ensured robust handling of errors during profile command insertion without aborting the entire reconciliation process. * **Tests** * Added extensive tests covering validation, substitution, error handling, and reconciliation workflows for Windows MDM profiles using Fleet variables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 10:24:38 +00:00
checkProfileVariables(globalProfiles[0], 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarDigiCertDataPrefix})
checkProfileVariables(globalProfiles[1], 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups})
}
func testWindowsMDMManagedSCEPCertificates(t *testing.T, ds *Datastore) {
ctx := context.Background()
testCases := []struct {
name string
caName string
caType fleet.CAConfigAssetType
challengeRetrievedAt *time.Time
}{
/* {
name: "NDES",
caName: "ndes",
caType: fleet.CAConfigNDES,
challengeRetrievedAt: ptr.Time(time.Now().Add(-time.Hour).UTC().Round(time.Microsecond)),
}, */
{
name: "Custom SCEP",
caName: "test-ca",
caType: fleet.CAConfigCustomSCEPProxy,
challengeRetrievedAt: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
caName := tc.caName
caType := tc.caType
challengeRetrievedAt := tc.challengeRetrievedAt
profileUUID := uuid.NewString()
dummySyncML := generateDummyWindowsProfile(profileUUID)
dummyCP := fleet.MDMWindowsConfigProfile{
Name: tc.caName,
SyncML: dummySyncML,
}
initialCP, err := ds.NewMDMWindowsConfigProfile(ctx, dummyCP, nil)
require.NoError(t, err)
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("host0-osquery-id" + tc.caName),
NodeKey: ptr.String("host0-node-key" + tc.caName),
UUID: "host0-test-mdm-profiles" + tc.caName,
Hostname: "hostname0",
})
require.NoError(t, err)
// Host and profile are not linked
profile, err := ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
assert.Nil(t, profile)
err = ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{
{
ProfileUUID: initialCP.ProfileUUID,
ProfileName: initialCP.Name,
HostUUID: host.UUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "command-uuid",
Checksum: []byte("checksum"),
},
},
)
require.NoError(t, err)
// Host and profile do not have certificate metadata
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
assert.Nil(t, profile)
// Initial certificate state where a host has been requested to install but we have no metadata
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
{
HostUUID: host.UUID,
ProfileUUID: initialCP.ProfileUUID,
ChallengeRetrievedAt: challengeRetrievedAt,
Type: caType,
CAName: caName,
},
})
require.NoError(t, err)
// Check that the managed certificate was inserted correctly
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile)
assert.Equal(t, host.UUID, profile.HostUUID)
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt)
assert.Equal(t, caType, profile.Type)
assert.Nil(t, profile.Serial)
assert.Nil(t, profile.NotValidBefore)
assert.Nil(t, profile.NotValidAfter)
assert.Equal(t, caName, profile.CAName)
// Renew should not do anything yet
err = ds.RenewMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile.Status)
assert.Equal(t, fleet.MDMDeliveryPending, *profile.Status)
// Cleanup should do nothing
err = ds.CleanUpMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile)
serial := "8ABADCAFEF684D6348F5EC95AEFF468F237A9D75"
t.Run("Non renewal scenario 1 - validity window > 30 days but not yet time to renew", func(t *testing.T) {
// Set not_valid_before to 1 day in the past and not_valid_after to 31 days in the future so
// the validity window is 32 days of which there are 31 left which should not trigger renewal
notValidAfter := time.Now().Add(31 * 24 * time.Hour).UTC().Round(time.Microsecond)
notValidBefore := time.Now().Add(-1 * 24 * time.Hour).UTC().Round(time.Microsecond)
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
{
HostUUID: host.UUID,
ProfileUUID: initialCP.ProfileUUID,
ChallengeRetrievedAt: challengeRetrievedAt,
NotValidBefore: &notValidBefore,
NotValidAfter: &notValidAfter,
Type: caType,
CAName: caName,
Serial: &serial,
},
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
UPDATE host_mdm_windows_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ?
`, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID)
if err != nil {
return err
}
return nil
})
// Verify the policy is not currently marked for resend and that the upsert executed correctly
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile.Status)
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
assert.Equal(t, host.UUID, profile.HostUUID)
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt)
assert.Equal(t, &notValidBefore, profile.NotValidBefore)
assert.Equal(t, &notValidAfter, profile.NotValidAfter)
assert.Equal(t, caType, profile.Type)
require.NotNil(t, profile.Serial)
assert.Equal(t, serial, *profile.Serial)
assert.Equal(t, caName, profile.CAName)
// Renew should not change the MDM delivery status
err = ds.RenewMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile.Status)
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
// Cleanup should do nothing
err = ds.CleanUpMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile)
})
t.Run("Non renewal scenario 2 - validity window < 30 days but not yet time to renew", func(t *testing.T) {
// Set not_valid_before to 13 days in the past and not_valid_after to 15 days in the future so
// the validity window is 28 days of which there are 15 left which should not trigger renewal
notValidAfter := time.Now().Add(15 * 24 * time.Hour).UTC().Round(time.Microsecond)
notValidBefore := time.Now().Add(-13 * 24 * time.Hour).UTC().Round(time.Microsecond)
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
{
HostUUID: host.UUID,
ProfileUUID: initialCP.ProfileUUID,
ChallengeRetrievedAt: challengeRetrievedAt,
NotValidBefore: &notValidBefore,
NotValidAfter: &notValidAfter,
Type: caType,
CAName: caName,
Serial: &serial,
},
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
UPDATE host_mdm_windows_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ?
`, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID)
if err != nil {
return err
}
return nil
})
// Verify the policy is not currently marked for resend and that the upsert executed correctly
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile.Status)
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
assert.Equal(t, host.UUID, profile.HostUUID)
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt)
assert.Equal(t, &notValidBefore, profile.NotValidBefore)
assert.Equal(t, &notValidAfter, profile.NotValidAfter)
assert.Equal(t, caType, profile.Type)
require.NotNil(t, profile.Serial)
assert.Equal(t, serial, *profile.Serial)
assert.Equal(t, caName, profile.CAName)
// Renew should not change the MDM delivery status
err = ds.RenewMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile.Status)
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
// Cleanup should do nothing
err = ds.CleanUpMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile)
})
t.Run("Renew scenario 1 - validity window > 30 days", func(t *testing.T) {
// Set not_valid_before to 31 days in the past the validity window becomes 60 days, of which there are
// 29 left which should trigger the first renewal scenario(window > 30 days, renew when < 30
// days left)
notValidAfter := time.Now().Add(29 * 24 * time.Hour).UTC().Round(time.Microsecond)
notValidBefore := time.Now().Add(-31 * 24 * time.Hour).UTC().Round(time.Microsecond)
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
{
HostUUID: host.UUID,
ProfileUUID: initialCP.ProfileUUID,
ChallengeRetrievedAt: challengeRetrievedAt,
NotValidBefore: &notValidBefore,
NotValidAfter: &notValidAfter,
Type: caType,
CAName: caName,
Serial: &serial,
},
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
UPDATE host_mdm_windows_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ?
`, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID)
if err != nil {
return err
}
return nil
})
// Verify the policy is not currently marked for resend and that the upsert executed correctly
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile.Status)
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
assert.Equal(t, host.UUID, profile.HostUUID)
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt)
assert.Equal(t, &notValidBefore, profile.NotValidBefore)
assert.Equal(t, &notValidAfter, profile.NotValidAfter)
assert.Equal(t, caType, profile.Type)
require.NotNil(t, profile.Serial)
assert.Equal(t, serial, *profile.Serial)
assert.Equal(t, caName, profile.CAName)
// Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed
err = ds.RenewMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.Nil(t, profile.Status)
// Cleanup should do nothing
err = ds.CleanUpMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile)
})
t.Run("Renew scenario 2 - validity window < 30 days", func(t *testing.T) {
// Set not_valid_before to 15 days in the past and not_valid_after to 14 days in the future so the
// validity window becomes 29 days, of which there are 14 left which should trigger the second
// renewal scenario(window < 30 days, renew when there is half that time left)
notValidBefore := time.Now().Add(-15 * 24 * time.Hour).UTC().Round(time.Microsecond)
notValidAfter := time.Now().Add(14 * 24 * time.Hour).UTC().Round(time.Microsecond)
err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
{
HostUUID: host.UUID,
ProfileUUID: initialCP.ProfileUUID,
ChallengeRetrievedAt: challengeRetrievedAt,
NotValidBefore: &notValidBefore,
NotValidAfter: &notValidAfter,
Type: caType,
CAName: caName,
Serial: &serial,
},
})
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
UPDATE host_mdm_windows_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ?
`, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID)
if err != nil {
return err
}
return nil
})
require.NoError(t, err)
// Verify the policy is not currently marked for resend and that the upsert executed correctly
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile.Status)
assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status)
assert.Equal(t, host.UUID, profile.HostUUID)
assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID)
assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt)
assert.Equal(t, &notValidBefore, profile.NotValidBefore)
assert.Equal(t, &notValidAfter, profile.NotValidAfter)
assert.Equal(t, caType, profile.Type)
require.NotNil(t, profile.Serial)
assert.Equal(t, serial, *profile.Serial)
assert.Equal(t, caName, profile.CAName)
// Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed
err = ds.RenewMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.Nil(t, profile.Status)
// Cleanup should do nothing
err = ds.CleanUpMDMManagedCertificates(ctx)
require.NoError(t, err)
profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName)
require.NoError(t, err)
require.NotNil(t, profile)
})
})
}
}
func testGetWindowsMDMCommandsForResending(t *testing.T, ds *Datastore) {
ctx := context.Background()
topLevelCmdUUID := uuid.NewString()
cmdUUID := uuid.NewString()
// Create entry in mdm_windows_enrollments table for command queue
dev := createMDMWindowsEnrollment(ctx, t, ds)
// No commands in windows_mdm_commands so doesn't matter what we put in
commands, err := ds.GetWindowsMDMCommandsForResending(ctx, []string{cmdUUID})
require.NoError(t, err)
require.Empty(t, commands)
// Insert a command
rawCommand := []byte(cmdUUID)
err = ds.mdmWindowsInsertCommandForHostsDB(ctx, ds.writer(ctx), []string{dev.HostUUID}, &fleet.MDMWindowsCommand{
CommandUUID: topLevelCmdUUID,
RawCommand: rawCommand,
})
require.NoError(t, err)
// Fetch command for resending
commands, err = ds.GetWindowsMDMCommandsForResending(ctx, []string{cmdUUID})
require.NoError(t, err)
require.Len(t, commands, 1)
assert.Equal(t, topLevelCmdUUID, commands[0].CommandUUID)
assert.Equal(t, rawCommand, commands[0].RawCommand)
// Check that we search raw body and not match on command_uuid
commands, err = ds.GetWindowsMDMCommandsForResending(ctx, []string{topLevelCmdUUID})
require.NoError(t, err)
require.Empty(t, commands)
}
func testResendWindowsMDMCommand(t *testing.T, ds *Datastore) {
ctx := context.Background()
dev := createMDMWindowsEnrollment(ctx, t, ds)
cmdUUID := uuid.NewString()
// Query enrollment id from mdm_windows_enrollments
var enrollmentID int64
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &enrollmentID, "SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?", dev.MDMDeviceID)
})
require.Greater(t, enrollmentID, int64(0), "Enrollment ID should be greater than 0")
// Insert host profile entry
err := ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{
{
HostUUID: dev.HostUUID,
ProfileUUID: uuid.NewString(),
ProfileName: "test-profile",
Status: &fleet.MDMDeliveryFailed,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: cmdUUID,
Checksum: []byte("checksum"),
Detail: "fake detail we expect to be cleared on resend",
},
})
require.NoError(t, err)
// Insert a command for the original profile
cmdBody := []byte(`<Add></Add>`)
cmd := &fleet.MDMWindowsCommand{
CommandUUID: cmdUUID,
RawCommand: cmdBody,
}
err = ds.mdmWindowsInsertCommandForHostsDB(ctx, ds.writer(ctx), []string{dev.HostUUID}, cmd)
require.NoError(t, err)
// Verify we have a windows_mdm_command_queue entry
var count int
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ? AND enrollment_id = ?",
cmd.CommandUUID, enrollmentID)
})
assert.Equal(t, 1, count, "Command queue entry should exist before resend")
// Resend command
// We manually do replacement here
newCmdUUID := uuid.NewString()
newBody := []byte(`<Replace></Replace>`)
newCmd := &fleet.MDMWindowsCommand{
CommandUUID: newCmdUUID,
RawCommand: newBody,
}
err = ds.ResendWindowsMDMCommand(ctx, dev.MDMDeviceID, newCmd, cmd)
require.NoError(t, err)
// Verify we have a windows_mdm_command_queue entry for the new command
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ? AND enrollment_id = ?",
newCmd.CommandUUID, enrollmentID)
})
assert.Equal(t, 1, count, "New command queue entry should exist after resend")
// verify we don't have a windows_mdm_command_queue entry for the old command
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ? AND enrollment_id = ?",
cmd.CommandUUID, enrollmentID)
})
assert.Equal(t, 0, count, "Old command queue entry should not exist after resend")
// Verify host profile status is reset and detail cleared
var status string
var detail sql.NullString
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &status, "SELECT status FROM host_mdm_windows_profiles WHERE command_uuid = ? AND host_uuid = ?",
newCmd.CommandUUID, dev.HostUUID)
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &detail, "SELECT detail FROM host_mdm_windows_profiles WHERE command_uuid = ? AND host_uuid = ?",
newCmd.CommandUUID, dev.HostUUID)
})
assert.Equal(t, string(fleet.MDMDeliveryPending), status, "Host profile status should be reset to pending on resend")
require.True(t, detail.Valid, "Host profile detail should be cleared on resend")
assert.Empty(t, detail.String, "Host profile detail should be cleared on resend")
}