fleet/server/datastore/mysql/microsoft_mdm.go
Konstantin Sykulev 779cdd663b
Periodic background job to cleanup Windows MDM command queue (#44458)
**Related issue:** Resolves #44190

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [x] Added/updated automated tests
- [ ] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a periodic cleanup job that removes aged, acknowledged Windows
MDM command-queue entries to reduce write pressure during ACK
processing.

* **Bug Fixes**
* Pending-command detection now excludes already-ACKed commands from
dispatch; queue rows are retained after ACK and cleaned later.

* **Tests**
* Added and updated tests to validate cleanup behavior and revised
ACK/queue semantics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-04 11:32:45 -05:00

3474 lines
127 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mysql
import (
"bytes"
"context"
"database/sql"
"encoding/xml"
"errors"
"fmt"
"maps"
"math"
"slices"
"strings"
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
// windowsMDMProfileDeleteBatchSize is the number of rows to process per batch
// when enqueuing <Delete> commands, resolving enrollment IDs, and updating
// host profile rows during profile deletion.
//
// 10,000 stays under MySQL's 65,535 placeholder limit on every caller. The
// densest caller is the batched UPDATE with tuple IN + CASE per profile in
// cancelWindowsHostInstallsForDeletedMDMProfiles, whose placeholder count is
//
// 2 (constants) + 2*distinctProfilesInBatch (CASE arms) + 2*rowsInBatch (IN)
//
// At batchSize=10,000 the realistic case (tens of profiles × many hosts each)
// is ~20,000 placeholders, and the worst case (up to 10,000 distinct profiles
// sharing a 10,000-row batch) is 40,002, still well under 65,535. The value
// also matches the batch sizes used elsewhere in Fleet for host-table bulk ops.
const windowsMDMProfileDeleteBatchSize = 10000
func isWindowsHostConnectedToFleetMDM(ctx context.Context, q sqlx.QueryerContext, h *fleet.Host) (bool, error) {
var unused string
// safe to use with interpolation rather than prepared statements because we're using a numeric ID here
err := sqlx.GetContext(ctx, q, &unused, fmt.Sprintf(`
SELECT mwe.host_uuid
FROM mdm_windows_enrollments mwe
JOIN hosts h ON h.uuid = mwe.host_uuid
JOIN host_mdm hm ON hm.host_id = h.id
WHERE h.id = %d
AND mwe.device_state = '`+microsoft_mdm.MDMDeviceStateEnrolled+`'
AND hm.enrolled = 1 LIMIT 1
`, h.ID))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, err
}
return true, nil
}
// MDMWindowsGetEnrolledDeviceWithDeviceID receives a Windows MDM device id and
// returns the device information.
func (ds *Datastore) MDMWindowsGetEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
// Only fetch the most recently enrolled entry which matches the one we enqueue commands for
stmt := `SELECT
id,
mdm_device_id,
mdm_hardware_id,
device_state,
device_type,
device_name,
enroll_type,
enroll_user_id,
enroll_proto_version,
enroll_client_version,
not_in_oobe,
awaiting_configuration,
awaiting_configuration_at,
credentials_hash,
credentials_acknowledged,
created_at,
updated_at,
host_uuid
FROM mdm_windows_enrollments WHERE mdm_device_id = ? ORDER BY created_at DESC, id DESC LIMIT 1`
var winMDMDevice fleet.MDMWindowsEnrolledDevice
if err := sqlx.GetContext(ctx, ds.reader(ctx), &winMDMDevice, stmt, mdmDeviceID); err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice").WithMessage(mdmDeviceID))
}
return nil, ctxerr.Wrap(ctx, err, "get MDMWindowsGetEnrolledDeviceWithDeviceID")
}
return &winMDMDevice, nil
}
// MDMWindowsGetEnrolledDeviceWithDeviceID receives a Windows MDM device id and
// returns the device information.
func (ds *Datastore) MDMWindowsGetEnrolledDeviceWithHostUUID(ctx context.Context, hostUUID string) (*fleet.MDMWindowsEnrolledDevice, error) {
// Only fetch the most recently enrolled entry which matches the one we enqueue commands for
stmt := `SELECT
id,
mdm_device_id,
mdm_hardware_id,
device_state,
device_type,
device_name,
enroll_type,
enroll_user_id,
enroll_proto_version,
enroll_client_version,
not_in_oobe,
awaiting_configuration,
awaiting_configuration_at,
credentials_hash,
credentials_acknowledged,
created_at,
updated_at,
host_uuid
FROM mdm_windows_enrollments WHERE host_uuid = ? ORDER BY created_at DESC, id DESC LIMIT 1`
var winMDMDevice fleet.MDMWindowsEnrolledDevice
// use the writer because this is sometimes fetched soon after updating the host UUID
if err := sqlx.GetContext(ctx, ds.writer(ctx), &winMDMDevice, stmt, hostUUID); err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice").WithMessage(hostUUID))
}
return nil, ctxerr.Wrap(ctx, err, "get MDMWindowsGetEnrolledDeviceWithHostUUID")
}
return &winMDMDevice, nil
}
// MDMWindowsInsertEnrolledDevice inserts a new MDMWindowsEnrolledDevice in the
// database.
func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device *fleet.MDMWindowsEnrolledDevice) error {
stmt := `
INSERT INTO mdm_windows_enrollments (
mdm_device_id,
mdm_hardware_id,
device_state,
device_type,
device_name,
enroll_type,
enroll_user_id,
enroll_proto_version,
enroll_client_version,
not_in_oobe,
awaiting_configuration,
awaiting_configuration_at,
host_uuid,
credentials_hash,
credentials_acknowledged)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
mdm_device_id = VALUES(mdm_device_id),
device_state = VALUES(device_state),
device_type = VALUES(device_type),
device_name = VALUES(device_name),
enroll_type = VALUES(enroll_type),
enroll_user_id = VALUES(enroll_user_id),
enroll_proto_version = VALUES(enroll_proto_version),
enroll_client_version = VALUES(enroll_client_version),
not_in_oobe = VALUES(not_in_oobe),
awaiting_configuration = VALUES(awaiting_configuration),
awaiting_configuration_at = VALUES(awaiting_configuration_at),
host_uuid = VALUES(host_uuid),
credentials_hash = VALUES(credentials_hash),
credentials_acknowledged = VALUES(credentials_acknowledged)
`
_, err := ds.writer(ctx).ExecContext(
ctx,
stmt,
device.MDMDeviceID,
device.MDMHardwareID,
device.MDMDeviceState,
device.MDMDeviceType,
device.MDMDeviceName,
device.MDMEnrollType,
device.MDMEnrollUserID,
device.MDMEnrollProtoVersion,
device.MDMEnrollClientVersion,
device.MDMNotInOOBE,
device.AwaitingConfiguration,
device.AwaitingConfigurationAt,
device.HostUUID,
device.CredentialsHash,
device.CredentialsAcknowledged)
if err != nil {
if IsDuplicate(err) {
return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsEnrolledDevice", device.MDMHardwareID))
}
return ctxerr.Wrap(ctx, err, "inserting MDMWindowsEnrolledDevice")
}
return nil
}
// MDMWindowsDeleteEnrolledDeviceOnReenrollment deletes a Windows device
// enrollment entry from the database using the device's hardware ID as it is
// re-enrolling. It also cleans up host_mdm_windows_profiles so profile
// delivery statuses are reset for the new enrollment.
func (ds *Datastore) MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx context.Context, mdmDeviceHWID string) error {
const (
delStmt = "DELETE FROM mdm_windows_enrollments WHERE mdm_hardware_id = ?"
loadStmt = "SELECT host_uuid FROM mdm_windows_enrollments WHERE mdm_hardware_id = ? LIMIT 1"
delActionsStmt = "DELETE FROM host_mdm_actions WHERE host_id = (SELECT id FROM hosts WHERE uuid = ? LIMIT 1)"
delProfilesStmt = "DELETE FROM host_mdm_windows_profiles WHERE host_uuid = ?"
)
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
var hostUUID sql.NullString
switch err := sqlx.GetContext(ctx, tx, &hostUUID, loadStmt, mdmDeviceHWID); err {
case nil:
if hostUUID.Valid {
// Clear lock/wipe status
if _, err := tx.ExecContext(ctx, delActionsStmt, hostUUID.String); err != nil {
return ctxerr.Wrap(ctx, err, "delete host_mdm_actions for host")
}
// Clear profile delivery statuses so they get re-delivered
// on the new enrollment.
if _, err := tx.ExecContext(ctx, delProfilesStmt, hostUUID.String); err != nil {
return ctxerr.Wrap(ctx, err, "delete host_mdm_windows_profiles for host")
}
}
case sql.ErrNoRows:
// nothing to delete, return early
return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice"))
default:
return ctxerr.Wrap(ctx, err, "load host_uuid for MDMWindowsEnrolledDevice")
}
res, err := tx.ExecContext(ctx, delStmt, mdmDeviceHWID)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete MDMWindowsEnrolledDevice")
}
deleted, _ := res.RowsAffected()
if deleted == 1 {
return nil
}
return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice"))
})
}
// MDMWindowsDeleteEnrolledDeviceWithDeviceID deletes a given
// MDMWindowsEnrolledDevice entry from the database using the device id.
// It also cleans up all host_mdm_windows_profiles rows for the host since
// the device can no longer receive MDM commands after unenrollment.
func (ds *Datastore) MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// Look up host_uuid before deleting the enrollment so we can clean up profile rows.
// Use sql.NullString since host_uuid may be NULL if the enrollment hasn't been linked to a host yet.
var hostUUID sql.NullString
err := sqlx.GetContext(ctx, tx,
&hostUUID, `SELECT host_uuid FROM mdm_windows_enrollments WHERE mdm_device_id = ? ORDER BY created_at DESC LIMIT 1`, mdmDeviceID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice"))
}
return ctxerr.Wrap(ctx, err, "looking up host_uuid for enrolled device")
}
res, err := tx.ExecContext(ctx, `DELETE FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, mdmDeviceID)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting Windows enrolled device")
}
deleted, _ := res.RowsAffected()
if deleted != 1 {
return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice"))
}
// Clean up all host_mdm_windows_profiles rows for this host since it can no longer receive MDM commands.
if hostUUID.Valid && hostUUID.String != "" {
if _, err := tx.ExecContext(ctx,
`DELETE FROM host_mdm_windows_profiles WHERE host_uuid = ?`, hostUUID.String); err != nil {
return ctxerr.Wrap(ctx, err, "cleaning up Windows host MDM profiles after unenrollment")
}
}
return nil
})
}
// this function inserts both the host_mdm_windows_profile entries and the actual mdm_windows_command_queue entries for a given command and list of hosts.
// We do the host-targeting pieces in a transaction to ensure that we don't end up with queued commands that don't have corresponding host profile entries,
// which would previously cause issues when processing responses from the device if there was a long delay between enqueing the command and the host profile
// entry insertion. It is done in batches for performance reasons and the command itself is inserted before the batches begin. It need not be one big tranasaction
// as long as a given host's command queue entry and host profile entry are inserted in the same transaction. Note that unlike the insert command function below
// this does not work with device IDs, only host UUIDs
func (ds *Datastore) MDMWindowsInsertCommandAndUpsertHostProfilesForHosts(ctx context.Context, hostUUIDs []string, cmd *fleet.MDMWindowsCommand, payload []*fleet.MDMWindowsBulkUpsertHostProfilePayload) error {
if len(hostUUIDs) == 0 {
return nil
}
const defaultBatchSize = 1000
batchSize := defaultBatchSize
if ds.testUpsertMDMDesiredProfilesBatchSize > 0 {
batchSize = ds.testUpsertMDMDesiredProfilesBatchSize
}
// Insert the command once, outside of the batched transactions.
cmdStmt := `
INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri)
VALUES (?, ?, ?)
`
if _, err := ds.writer(ctx).ExecContext(ctx, cmdStmt, cmd.CommandUUID, cmd.RawCommand, cmd.TargetLocURI); err != nil {
if IsDuplicate(err) {
return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsCommand", cmd.CommandUUID))
}
return ctxerr.Wrap(ctx, err, "inserting MDMWindowsCommand")
}
// Build a map from host UUID to its corresponding profile payload for quick lookup.
payloadByHostUUID := make(map[string]*fleet.MDMWindowsBulkUpsertHostProfilePayload, len(payload))
for _, p := range payload {
payloadByHostUUID[p.HostUUID] = p
}
// Insert command queue entries and host profile entries in batches.
// The queue INSERT ... SELECT silently skips hosts without an
// enrollment; profile rows are upserted for all hosts in the batch.
var (
queueHostUUIDs []string
profileArgs []any
profileSB strings.Builder
batchCount int
)
executeBatch := func() error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// Insert command queue entries via INSERT ... SELECT so that
// hosts whose enrollment was deleted produce 0 rows instead
// of a NULL enrollment_id / FK error.
if len(queueHostUUIDs) > 0 {
queueQuery, queueArgs, err := sqlx.In(`
INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid)
SELECT MAX(mwe.id), ?
FROM mdm_windows_enrollments mwe
WHERE mwe.host_uuid IN (?)
GROUP BY mwe.host_uuid`,
cmd.CommandUUID, queueHostUUIDs,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "building IN for MDMWindowsCommandQueue insert")
}
if _, err := tx.ExecContext(ctx, queueQuery, queueArgs...); err != nil {
if IsDuplicate(err) {
return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsCommandQueue", cmd.CommandUUID))
}
return ctxerr.Wrap(ctx, err, "batch inserting MDMWindowsCommandQueue")
}
}
// Should never happen
if len(profileArgs) == 0 {
return nil
}
// Upsert host profile entries.
profileStmt := fmt.Sprintf(`
INSERT INTO host_mdm_windows_profiles (
profile_uuid, host_uuid, status, operation_type,
detail, command_uuid, profile_name, checksum
)
VALUES %s
ON DUPLICATE KEY UPDATE
status = VALUES(status),
operation_type = VALUES(operation_type),
detail = VALUES(detail),
profile_name = VALUES(profile_name),
checksum = VALUES(checksum),
command_uuid = VALUES(command_uuid)`,
strings.TrimSuffix(profileSB.String(), ","),
)
if _, err := tx.ExecContext(ctx, profileStmt, profileArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "batch upserting host_mdm_windows_profiles")
}
return nil
})
}
resetBatch := func() {
batchCount = 0
queueHostUUIDs = queueHostUUIDs[:0]
profileArgs = profileArgs[:0]
profileSB.Reset()
}
for _, hostUUID := range hostUUIDs {
// This may seem odd running the batch up front but it helps ensure we don't run oversized batches if for instance a caller
// makes an error leading to the warning below about mismatch host profile/command entries
if batchCount >= batchSize {
if err := executeBatch(); err != nil {
return err
}
resetBatch()
}
// Host profile entry.
p := payloadByHostUUID[hostUUID]
if p == nil {
ds.logger.WarnContext(ctx, "skipping host with no corresponding profile payload", "host_uuid", hostUUID, "command_uuid", cmd.CommandUUID)
continue
}
batchCount++
queueHostUUIDs = append(queueHostUUIDs, hostUUID)
profileSB.WriteString("(?, ?, ?, ?, ?, ?, ?, ?),")
profileArgs = append(profileArgs, p.ProfileUUID, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID, p.ProfileName, p.Checksum)
}
if batchCount > 0 {
if err := executeBatch(); err != nil {
return err
}
}
return nil
}
func (ds *Datastore) MDMWindowsInsertCommandForHosts(ctx context.Context, hostUUIDsOrDeviceIDs []string, cmd *fleet.MDMWindowsCommand) error {
if len(hostUUIDsOrDeviceIDs) == 0 {
return nil
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
return ds.mdmWindowsInsertCommandForHostsDB(ctx, tx, hostUUIDsOrDeviceIDs, cmd)
})
}
func (ds *Datastore) mdmWindowsInsertCommandForHostsDB(ctx context.Context, tx sqlx.ExtContext, hostUUIDsOrDeviceIDs []string, cmd *fleet.MDMWindowsCommand) error {
// Resolve host UUIDs / device IDs to enrollment IDs using the general-purpose
// lookup (supports both host_uuid and mdm_device_id via subquery).
enrollmentIDs, err := ds.getEnrollmentIDsByHostUUIDOrDeviceIDDB(ctx, tx, hostUUIDsOrDeviceIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "fetching enrollment IDs for command queue")
}
return ds.mdmWindowsInsertCommandForEnrollmentIDsDB(ctx, tx, enrollmentIDs, cmd)
}
// mdmWindowsInsertCommandForHostUUIDsDB is the fast path for bulk operations
// that always have host UUIDs (not device IDs). Uses an indexed batch SELECT
// instead of per-row subqueries.
func (ds *Datastore) mdmWindowsInsertCommandForHostUUIDsDB(ctx context.Context, tx sqlx.ExtContext, hostUUIDs []string, cmd *fleet.MDMWindowsCommand) error {
enrollmentIDs, err := ds.getEnrollmentIDsByHostUUIDDB(ctx, tx, hostUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "fetching enrollment IDs by host UUID")
}
return ds.mdmWindowsInsertCommandForEnrollmentIDsDB(ctx, tx, enrollmentIDs, cmd)
}
// mdmWindowsInsertCommandForEnrollmentIDsDB inserts the command and queues it
// for the given enrollment IDs.
func (ds *Datastore) mdmWindowsInsertCommandForEnrollmentIDsDB(ctx context.Context, tx sqlx.ExtContext, enrollmentIDs []uint, cmd *fleet.MDMWindowsCommand) error {
// Create the command entry.
stmt := `INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri) VALUES (?, ?, ?)`
if _, err := tx.ExecContext(ctx, stmt, cmd.CommandUUID, cmd.RawCommand, cmd.TargetLocURI); err != nil {
if IsDuplicate(err) {
return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsCommand", cmd.CommandUUID))
}
return ctxerr.Wrap(ctx, err, "inserting MDMWindowsCommand")
}
if len(enrollmentIDs) == 0 {
return nil
}
// Batch insert into command queue.
return common_mysql.BatchProcessSimple(enrollmentIDs, windowsMDMProfileDeleteBatchSize, func(batch []uint) error {
valuesPart := strings.Repeat("(?, ?),", len(batch))
valuesPart = strings.TrimSuffix(valuesPart, ",")
args := make([]any, 0, len(batch)*2)
for _, eid := range batch {
args = append(args, eid, cmd.CommandUUID)
}
batchStmt := `INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid) VALUES ` + valuesPart
if _, err := tx.ExecContext(ctx, batchStmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "batch inserting MDMWindowsCommandQueue")
}
return nil
})
}
// getEnrollmentIDsByHostUUIDDB fetches enrollment IDs for a list of host UUIDs
// using an indexed batch query. Returns the most recent enrollment per host.
func (ds *Datastore) getEnrollmentIDsByHostUUIDDB(ctx context.Context, tx sqlx.ExtContext, hostUUIDs []string) ([]uint, error) {
var allIDs []uint
err := common_mysql.BatchProcessSimple(hostUUIDs, windowsMDMProfileDeleteBatchSize, func(batch []string) error {
stmt, args, err := sqlx.In(
`SELECT MAX(id) FROM mdm_windows_enrollments WHERE host_uuid IN (?) GROUP BY host_uuid`,
batch)
if err != nil {
return err
}
var ids []uint
if err := sqlx.SelectContext(ctx, tx, &ids, stmt, args...); err != nil {
return err
}
allIDs = append(allIDs, ids...)
return nil
})
return allIDs, err
}
// getEnrollmentIDsByHostUUIDOrDeviceIDDB fetches enrollment IDs using a
// per-row SELECT that supports both host_uuid and mdm_device_id lookups.
// Used by the general-purpose command insertion path (typically 1-2 IDs).
func (ds *Datastore) getEnrollmentIDsByHostUUIDOrDeviceIDDB(ctx context.Context, tx sqlx.ExtContext, hostUUIDsOrDeviceIDs []string) ([]uint, error) {
var allIDs []uint
for _, id := range hostUUIDsOrDeviceIDs {
var eid uint
err := sqlx.GetContext(ctx, tx, &eid,
`SELECT id FROM mdm_windows_enrollments WHERE host_uuid = ? OR mdm_device_id = ? ORDER BY created_at DESC, id DESC LIMIT 1`,
id, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
continue // host not enrolled, skip
}
return nil, ctxerr.Wrap(ctx, err, "looking up enrollment ID")
}
allIDs = append(allIDs, eid)
}
return allIDs, nil
}
// MDMWindowsGetPendingCommands retrieves all commands awaiting execution for the given enrollment.
func (ds *Datastore) MDMWindowsGetPendingCommands(ctx context.Context, enrollmentID uint) ([]*fleet.MDMWindowsCommand, error) {
// Fast path: probe the queue. An MDM management session runs this query on every
// check-in, and the overwhelming majority of devices have nothing queued, so short-circuit
// before paying for the full scan + anti-join. SELECT EXISTS always returns a row, so the
// idle path does not go through a sql.ErrNoRows branch.
// Queue rows now persist after ACK (cleaned by periodic GC), so the probe
// must also exclude rows that already have a result in
// windows_mdm_command_results.
const probe = `SELECT EXISTS(
SELECT 1 FROM windows_mdm_command_queue wmcq
WHERE wmcq.enrollment_id = ?
AND NOT EXISTS (
SELECT 1 FROM windows_mdm_command_results wmcr
WHERE wmcr.enrollment_id = wmcq.enrollment_id
AND wmcr.command_uuid = wmcq.command_uuid
)
)`
var hasPending bool
if err := sqlx.GetContext(ctx, ds.reader(ctx), &hasPending, probe, enrollmentID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "probe pending Windows MDM commands")
}
if !hasPending {
return nil, nil
}
const query = `
SELECT
wmc.command_uuid,
wmc.raw_command,
wmc.target_loc_uri,
wmc.created_at,
wmc.updated_at
FROM
windows_mdm_command_queue wmcq
INNER JOIN
windows_mdm_commands wmc
ON
wmc.command_uuid = wmcq.command_uuid
WHERE
wmcq.enrollment_id = ? AND
NOT EXISTS (
SELECT 1
FROM
windows_mdm_command_results wmcr
WHERE
wmcr.enrollment_id = wmcq.enrollment_id AND
wmcr.command_uuid = wmcq.command_uuid
)
ORDER BY
wmc.created_at ASC
`
var commands []*fleet.MDMWindowsCommand
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &commands, query, enrollmentID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get pending Windows MDM commands by enrollment id")
}
return commands, nil
}
func (ds *Datastore) MDMWindowsSaveResponse(ctx context.Context, enrolledDevice *fleet.MDMWindowsEnrolledDevice, enrichedSyncML fleet.EnrichedSyncML, commandIDsBeingResent []string) (*fleet.MDMWindowsSaveResponseResult, error) {
if len(enrichedSyncML.Raw) == 0 {
return nil, ctxerr.New(ctx, "empty raw response")
}
if enrolledDevice == nil {
return nil, ctxerr.New(ctx, "enrolled device is nil")
}
var result *fleet.MDMWindowsSaveResponseResult
if err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
result = nil
// store the full response
const saveFullRespStmt = `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`
sqlResult, err := tx.ExecContext(ctx, saveFullRespStmt, enrolledDevice.ID, enrichedSyncML.Raw)
if err != nil {
return ctxerr.Wrap(ctx, err, "saving full response")
}
responseID, _ := sqlResult.LastInsertId()
// find commands we sent that match the UUID responses we've got
findCommandsStmt := `SELECT command_uuid, raw_command, target_loc_uri FROM windows_mdm_commands WHERE command_uuid IN (?)`
stmt, params, err := sqlx.In(findCommandsStmt, enrichedSyncML.CmdRefUUIDs)
if len(commandIDsBeingResent) > 0 {
// If we're resending any commands, avoid selecting them here
placeholders := make([]string, len(commandIDsBeingResent))
for i, id := range commandIDsBeingResent {
placeholders[i] = "?"
params = append(params, id)
}
stmt += fmt.Sprintf(" AND command_uuid NOT IN (%s)", strings.Join(placeholders, ","))
}
if err != nil {
return ctxerr.Wrap(ctx, err, "building IN to search matching commands")
}
var matchingCmds []fleet.MDMWindowsCommand
err = sqlx.SelectContext(ctx, tx, &matchingCmds, stmt, params...)
if err != nil {
return ctxerr.Wrap(ctx, err, "selecting matching commands")
}
if len(matchingCmds) == 0 {
if len(commandIDsBeingResent) == 0 {
// Only log if not resending commands as we then can expect no matching commands
ds.logger.WarnContext(ctx, "unmatched Windows MDM commands", "uuids", strings.Join(enrichedSyncML.CmdRefUUIDs, ","), "mdm_device_id",
enrolledDevice.MDMDeviceID)
}
return nil
}
// for all the matching UUIDs, try to find any <Status> or
// <Result> entries to track them as responses.
var (
args []any
sb strings.Builder
potentialProfilePayloads []*fleet.MDMWindowsProfilePayload
wipeCmdUUID string
wipeCmdStatus string
)
// Look up operation types for matching commands so we can pass isRemoveOperation to BuildMDMWindowsProfilePayloadFromMDMResponse.
cmdOperationTypes := make(map[string]fleet.MDMOperationType)
matchingCmdUUIDs := make([]string, 0, len(matchingCmds))
for _, cmd := range matchingCmds {
matchingCmdUUIDs = append(matchingCmdUUIDs, cmd.CommandUUID)
}
const getOpTypesStmt = `SELECT command_uuid, operation_type FROM host_mdm_windows_profiles WHERE host_uuid = ? AND command_uuid IN (?)`
opStmt, opArgs, opErr := sqlx.In(getOpTypesStmt, enrolledDevice.HostUUID, matchingCmdUUIDs)
if opErr != nil {
return ctxerr.Wrap(ctx, opErr, "building IN for operation types")
}
var opResults []struct {
CommandUUID string `db:"command_uuid"`
OperationType fleet.MDMOperationType `db:"operation_type"`
}
if err := sqlx.SelectContext(ctx, tx, &opResults, opStmt, opArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "selecting operation types for matching commands")
}
for _, r := range opResults {
cmdOperationTypes[r.CommandUUID] = r.OperationType
}
for _, cmd := range matchingCmds {
statusCode := ""
if status, ok := enrichedSyncML.CmdRefUUIDToStatus[cmd.CommandUUID]; ok && status.Data != nil {
statusCode = *status.Data
if status.Cmd != nil {
// The raw MDM command may contain a $FLEET_SECRET_XXX, which should never be exposed or stored unencrypted.
// Note: As of 2024/12/17, on <Add>, <Replace>, and <Exec> commands are exposed to Windows MDM users, so we should not see any secrets in <Atomic> commands. This code is here for future-proofing.
rawCommandStr := string(cmd.RawCommand)
rawCommandWithSecret, err := ds.ExpandEmbeddedSecrets(ctx, rawCommandStr)
if err != nil {
// This error should never happen since we validate the presence of needed secrets on profile upload.
return ctxerr.Wrap(ctx, err, "expanding embedded secrets")
}
// Secret may be found in the command, so we make a new struct with the expanded secret.
cmdWithSecret := cmd
cmdWithSecret.RawCommand = []byte(rawCommandWithSecret)
pp, err := fleet.BuildMDMWindowsProfilePayloadFromMDMResponse(cmdWithSecret, enrichedSyncML.CmdRefUUIDToStatus,
enrolledDevice.HostUUID, cmdOperationTypes[cmd.CommandUUID] == fleet.MDMOperationTypeRemove)
if err != nil {
return ctxerr.Wrap(ctx, err, "building profile payload from MDM response")
}
potentialProfilePayloads = append(potentialProfilePayloads, pp)
}
}
rawResult := []byte{}
if result, ok := enrichedSyncML.CmdRefUUIDToResults[cmd.CommandUUID]; ok && result.Data != nil {
var err error
rawResult, err = xml.Marshal(result)
if err != nil {
ds.logger.ErrorContext(ctx, "marshaling command result", "err", err, "cmd_uuid", cmd.CommandUUID)
}
}
args = append(args, enrolledDevice.ID, cmd.CommandUUID, rawResult, responseID, statusCode)
sb.WriteString("(?, ?, ?, ?, ?),")
// if the command is a Wipe, keep track of it so we can update
// host_mdm_actions accordingly.
if strings.Contains(cmd.TargetLocURI, "/Device/Vendor/MSFT/RemoteWipe/") {
wipeCmdUUID = cmd.CommandUUID
wipeCmdStatus = statusCode
}
}
if err := updateMDMWindowsHostProfileStatusFromResponseDB(ctx, tx, potentialProfilePayloads); err != nil {
return ctxerr.Wrap(ctx, err, "updating host profile status")
}
// store the command results
const insertResultsStmt = `
INSERT INTO windows_mdm_command_results
(enrollment_id, command_uuid, raw_result, response_id, status_code)
VALUES %s
ON DUPLICATE KEY UPDATE
raw_result = COALESCE(VALUES(raw_result), raw_result),
status_code = COALESCE(VALUES(status_code), status_code)
`
stmt = fmt.Sprintf(insertResultsStmt, strings.TrimSuffix(sb.String(), ","))
if _, err = tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "inserting command results")
}
// if we received a Wipe command result, update the host's status
if wipeCmdUUID != "" {
wipeSucceeded := strings.HasPrefix(wipeCmdStatus, "2")
rowsAffected, err := updateHostLockWipeStatusFromResultAndHostUUID(ctx, tx, enrolledDevice.HostUUID,
"wipe_ref", wipeCmdUUID, wipeSucceeded, false,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "updating wipe command result in host_mdm_actions")
}
if wipeCmdStatus != "" && rowsAffected > 0 {
if wipeSucceeded {
result = &fleet.MDMWindowsSaveResponseResult{
WipeSucceeded: &fleet.MDMWindowsWipeResult{
HostUUID: enrolledDevice.HostUUID,
},
}
} else {
result = &fleet.MDMWindowsSaveResponseResult{
WipeFailed: &fleet.MDMWindowsWipeResult{
HostUUID: enrolledDevice.HostUUID,
},
}
}
}
}
return nil
}); err != nil {
return nil, err
}
return result, nil
}
// updateMDMWindowsHostProfileStatusFromResponseDB takes a slice of potential
// profile payloads and updates the corresponding `status` and `detail` columns
// in `host_mdm_windows_profiles`
// TODO(roberto): much of this logic should be living in the service layer,
// would be nice to get the time to properly plan and implement.
func updateMDMWindowsHostProfileStatusFromResponseDB(
ctx context.Context,
tx sqlx.ExtContext,
payloads []*fleet.MDMWindowsProfilePayload,
) error {
if len(payloads) == 0 {
return nil
}
// this statement will act as a batch-update, no new host profiles
// should be inserted from a device MDM response, so we first check for
// matching entries and then perform the INSERT ... ON DUPLICATE KEY to
// update their detail and status.
const updateHostProfilesStmt = `
INSERT INTO host_mdm_windows_profiles
(host_uuid, profile_uuid, detail, status, retries, command_uuid, checksum)
VALUES %s
ON DUPLICATE KEY UPDATE
checksum = VALUES(checksum),
detail = VALUES(detail),
status = VALUES(status),
retries = VALUES(retries)`
// MySQL will use the `host_uuid` part of the primary key as a first
// pass, and then filter that subset by `command_uuid`.
const getMatchingHostProfilesStmt = `
SELECT host_uuid, profile_uuid, command_uuid, retries, checksum, operation_type
FROM host_mdm_windows_profiles
WHERE host_uuid = ? AND command_uuid IN (?)`
// grab command UUIDs to find matching entries using `getMatchingHostProfilesStmt`
commandUUIDs := make([]string, 0, len(payloads))
// also grab the payloads keyed by the command uuid, so we can easily
// grab the corresponding `Detail` and `Status` from the matching
// command later on.
uuidsToPayloads := make(map[string]*fleet.MDMWindowsProfilePayload, len(payloads))
hostUUID := payloads[0].HostUUID
for _, payload := range payloads {
if payload.HostUUID != hostUUID {
return errors.New("all payloads must be for the same host uuid")
}
commandUUIDs = append(commandUUIDs, payload.CommandUUID)
uuidsToPayloads[payload.CommandUUID] = payload
}
// find the matching entries for the given host_uuid, command_uuid combinations.
stmt, args, err := sqlx.In(getMatchingHostProfilesStmt, hostUUID, commandUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sqlx.In query")
}
var matchingHostProfiles []fleet.MDMWindowsProfilePayload
if err := sqlx.SelectContext(ctx, tx, &matchingHostProfiles, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "running query to get matching profiles")
}
// Partition matching entries into upsert and delete buckets.
var sb strings.Builder
args = args[:0]
var deleteCommandUUIDs []string
for _, hp := range matchingHostProfiles {
payload := uuidsToPayloads[hp.CommandUUID]
if payload.Status != nil && *payload.Status == fleet.MDMDeliveryFailed {
// Don't retry remove operations; removal is best-effort. Only retry install operations up to the max retry count.
if hp.OperationType != fleet.MDMOperationTypeRemove && hp.Retries < mdm.MaxWindowsProfileRetries {
// if we haven't hit the max retries, we set
// the host profile status to nil (which causes
// an install profile command to be enqueued
// the next time the profile manager cron runs)
// and increment the retry count
payload.Status = nil
hp.Retries++
}
}
// Delete bucket: remove operations that resolved to a terminal state.
// Removes are best-effort; both verified and failed are terminal since
// failed removes are non-retryable and should not surface as host-level
// failures in profile summaries.
if hp.OperationType == fleet.MDMOperationTypeRemove && payload.Status != nil &&
(*payload.Status == fleet.MDMDeliveryVerified || *payload.Status == fleet.MDMDeliveryFailed) {
deleteCommandUUIDs = append(deleteCommandUUIDs, hp.CommandUUID)
continue
}
args = append(args, hp.HostUUID, hp.ProfileUUID, payload.Detail, payload.Status, hp.Retries, hp.Checksum)
sb.WriteString("(?, ?, ?, ?, ?, command_uuid, ?),")
}
// Execute batched UPSERT for the upsert bucket.
values := strings.TrimSuffix(sb.String(), ",")
if len(values) > 0 {
stmt = fmt.Sprintf(updateHostProfilesStmt, values)
if _, err = tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "updating host profiles")
}
}
// Execute batched DELETE for terminal remove operations.
if len(deleteCommandUUIDs) > 0 {
deleteStmt, deleteArgs, err := sqlx.In(`
DELETE FROM host_mdm_windows_profiles
WHERE host_uuid = ? AND command_uuid IN (?)`,
hostUUID, deleteCommandUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "building IN for remove cleanup")
}
if _, err = tx.ExecContext(ctx, deleteStmt, deleteArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "cleaning up completed remove profiles")
}
}
return nil
}
func (ds *Datastore) GetMDMWindowsCommandResults(ctx context.Context, commandUUID string, hostUUID string) ([]*fleet.MDMCommandResult, error) {
query := `SELECT
mwe.host_uuid,
wmc.command_uuid,
COALESCE(wmcr.status_code, '101') AS status,
COALESCE(
wmcr.updated_at,
wmc.updated_at
) as updated_at,
wmc.target_loc_uri AS request_type,
COALESCE(wmr.raw_response, '') AS result,
wmc.raw_command AS payload
FROM
windows_mdm_commands wmc
LEFT JOIN windows_mdm_command_results wmcr ON wmcr.command_uuid = wmc.command_uuid
LEFT JOIN windows_mdm_responses wmr ON wmr.id = wmcr.response_id
LEFT JOIN windows_mdm_command_queue wmcq ON wmcq.command_uuid = wmc.command_uuid AND wmcr.command_uuid IS NULL
LEFT JOIN mdm_windows_enrollments mwe ON mwe.id = COALESCE(
wmcr.enrollment_id,
wmcq.enrollment_id
)
WHERE
wmc.command_uuid = ?`
args := []any{commandUUID}
if hostUUID != "" {
query += " AND mwe.host_uuid = ?"
args = append(args, hostUUID)
}
var results []*fleet.MDMCommandResult
err := sqlx.SelectContext(
ctx,
ds.reader(ctx),
&results,
query,
args...,
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get command results")
}
return results, nil
}
func (ds *Datastore) UpdateMDMWindowsEnrollmentsHostUUID(ctx context.Context, hostUUID string, mdmDeviceID string) (bool, error) {
// The final clause ensures we only update if the host UUID changes so we can tell the caller as this basically
// signals a new MDM enrollment in certain cases, as it is the first time we associate a host with an enrollment
stmt := `UPDATE mdm_windows_enrollments SET host_uuid = ? WHERE mdm_device_id = ? AND host_uuid <> ?`
res, err := ds.writer(ctx).Exec(stmt, hostUUID, mdmDeviceID, hostUUID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "setting host_uuid for windows enrollment")
}
aff, err := res.RowsAffected()
if err != nil {
return false, ctxerr.Wrap(ctx, err, "checking rows affected when setting host_uuid for windows enrollment")
}
return aff > 0, nil
}
func (ds *Datastore) SetMDMWindowsAwaitingConfiguration(ctx context.Context, mdmDeviceID string, expectFrom, to fleet.WindowsMDMAwaitingConfiguration) (bool, error) {
stmt := `UPDATE mdm_windows_enrollments SET awaiting_configuration = ? WHERE mdm_device_id = ? AND awaiting_configuration = ?`
res, err := ds.writer(ctx).ExecContext(ctx, stmt, to, mdmDeviceID, expectFrom)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "set windows awaiting configuration")
}
aff, err := res.RowsAffected()
if err != nil {
return false, ctxerr.Wrap(ctx, err, "rows affected for set windows awaiting configuration")
}
return aff > 0, nil
}
// whereBitLockerStatus returns a string suitable for inclusion within a SQL WHERE clause to filter by
// the given status. The caller is responsible for ensuring the status is valid. In the case of an invalid
// status, the function will return the string "FALSE". The caller should also ensure that the query in
// which this is used joins the following tables with the specified aliases:
// - host_disk_encryption_keys: hdek
// - host_mdm: hmdm
// - host_disks: hd
func (ds *Datastore) whereBitLockerStatus(ctx context.Context, status fleet.DiskEncryptionStatus, bitLockerPINRequired bool) string {
const (
whereNotServer = `(hmdm.is_server IS NOT NULL AND hmdm.is_server = 0)`
whereKeyAvailable = `(hdek.base64_encrypted IS NOT NULL AND hdek.base64_encrypted != '' AND hdek.decryptable IS NOT NULL AND hdek.decryptable = 1)`
whereEncrypted = `(hd.encrypted IS NOT NULL AND hd.encrypted = 1)`
whereHostDisksUpdated = `(hd.updated_at IS NOT NULL AND hdek.updated_at IS NOT NULL AND hd.updated_at >= hdek.updated_at)`
whereClientError = `(hdek.client_error IS NOT NULL AND hdek.client_error != '')`
withinGracePeriod = `(hdek.updated_at IS NOT NULL AND hdek.updated_at >= DATE_SUB(NOW(6), INTERVAL 1 HOUR))`
whereProtectionOn = `(hd.bitlocker_protection_status IS NULL OR hd.bitlocker_protection_status != 0)`
whereProtectionOff = `(hd.bitlocker_protection_status = 0)`
)
whereBitLockerPINSet := `TRUE`
if bitLockerPINRequired {
whereBitLockerPINSet = `(hd.tpm_pin_set = true)`
}
// TODO: what if windows sends us a key for an already encrypted volumne? could it get stuck
// in pending or verifying? should we modify SetOrUpdateHostDiskEncryption to ensure that we
// increment the updated_at timestamp on the host_disks table for all encrypted volumes
// host_disks if the hdek timestamp is newer? What about SetOrUpdateHostDiskEncryptionKey?
switch status {
case fleet.DiskEncryptionVerified:
// Verified requires protection to be on (or unknown/NULL for backward compatibility).
return whereNotServer + `
AND NOT ` + whereClientError + `
AND ` + whereKeyAvailable + `
AND ` + whereEncrypted + `
AND ` + whereHostDisksUpdated + `
AND ` + whereProtectionOn + `
AND ` + whereBitLockerPINSet
case fleet.DiskEncryptionVerifying:
// Possible verifying scenarios:
// - we have the key and host_disks already encrypted before the key but hasn't been updated yet
// - we have the key and host_disks reported unencrypted during the 1-hour grace period after key was updated
// Protection must be on for encrypted disks. For the grace period path (encryption
// still in progress), protection is expected to be off so we don't check it.
return whereNotServer + `
AND NOT ` + whereClientError + `
AND ` + whereKeyAvailable + `
AND (
(` + whereEncrypted + ` AND NOT ` + whereHostDisksUpdated + ` AND ` + whereProtectionOn + `)
OR (NOT ` + whereEncrypted + ` AND ` + whereHostDisksUpdated + ` AND ` + withinGracePeriod + `)
)
AND ` + whereBitLockerPINSet
case fleet.DiskEncryptionActionRequired:
// Action required when:
// 1. We _would_ be in verified/verifying but PIN is required and not set, OR
// 2. Disk is encrypted and key is escrowed but BitLocker protection is off
// (e.g., suspended for a BIOS update, or a TPM configuration issue)
return whereNotServer + `
AND NOT ` + whereClientError + `
AND ` + whereKeyAvailable + `
AND (` + whereEncrypted + ` OR (NOT ` + whereEncrypted + ` AND ` + whereHostDisksUpdated + ` AND ` + withinGracePeriod + `))
AND (NOT ` + whereBitLockerPINSet + ` OR (` + whereEncrypted + ` AND ` + whereProtectionOff + `))`
case fleet.DiskEncryptionEnforcing:
// Possible enforcing scenarios:
// - we don't have the key
// - we have the key and host_disks reported unencrypted before the key was updated or outside the 1-hour grace period after key was updated
return whereNotServer + `
AND NOT ` + whereClientError + `
AND (
NOT ` + whereKeyAvailable + `
OR (` + whereKeyAvailable + `
AND (NOT ` + whereEncrypted + `
AND (NOT ` + whereHostDisksUpdated + ` OR NOT ` + withinGracePeriod + `)
)
)
)`
case fleet.DiskEncryptionFailed:
return whereNotServer + ` AND ` + whereClientError
default:
ds.logger.DebugContext(ctx, "unknown bitlocker status", "status", status)
return "FALSE"
}
}
func (ds *Datastore) GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) {
diskEncryptionConfig, err := ds.GetConfigEnableDiskEncryption(ctx, teamID)
if err != nil {
return nil, err
}
if !diskEncryptionConfig.Enabled {
return &fleet.MDMWindowsBitLockerSummary{}, nil
}
// Note removing_enforcement is not applicable to Windows hosts
sqlFmt := `
SELECT
COUNT(if((%s), 1, NULL)) AS verified,
COUNT(if((%s), 1, NULL)) AS verifying,
COUNT(if((%s), 1, NULL)) AS action_required,
COUNT(if((%s), 1, NULL)) AS enforcing,
COUNT(if((%s), 1, NULL)) AS failed,
0 AS removing_enforcement
FROM
hosts h
JOIN host_mdm hmdm ON h.id = hmdm.host_id
JOIN mdm_windows_enrollments mwe ON h.uuid = mwe.host_uuid
LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id
LEFT JOIN host_disks hd ON h.id = hd.host_id
WHERE
mwe.device_state = '%s' AND
h.platform = 'windows' AND
hmdm.is_server = 0 AND
hmdm.enrolled = 1 AND
%s`
var args []interface{}
teamFilter := "h.team_id IS NULL"
if teamID != nil && *teamID > 0 {
teamFilter = "h.team_id = ?"
args = append(args, *teamID)
}
var res fleet.MDMWindowsBitLockerSummary
stmt := fmt.Sprintf(
sqlFmt,
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionVerified, diskEncryptionConfig.BitLockerPINRequired),
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionVerifying, diskEncryptionConfig.BitLockerPINRequired),
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionActionRequired, diskEncryptionConfig.BitLockerPINRequired),
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionEnforcing, diskEncryptionConfig.BitLockerPINRequired),
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionFailed, diskEncryptionConfig.BitLockerPINRequired),
microsoft_mdm.MDMDeviceStateEnrolled,
teamFilter,
)
if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, args...); err != nil {
return nil, err
}
return &res, nil
}
func (ds *Datastore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {
if host == nil {
return nil, ctxerr.New(ctx, "cannot get bitlocker status for nil host")
}
if host.Platform != "windows" {
// the caller should have already checked this
return nil, ctxerr.Errorf(ctx, "cannot get bitlocker status for non-windows host %d", host.ID)
}
mdmInfo, err := ds.GetHostMDM(ctx, host.ID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, ctxerr.Wrap(ctx, err, "cannot get bitlocker status because mdm info lookup failed")
}
if mdmInfo.IsServer {
// It is currently expected that server hosts do not have a bitlocker status so we can skip
// the query and return nil. We log for potential debugging in case this changes in the future.
ds.logger.DebugContext(ctx, "no bitlocker status for server host", "host_id", host.ID)
return nil, nil
}
diskEncryptionConfig, err := ds.GetConfigEnableDiskEncryption(ctx, host.TeamID)
if err != nil {
return nil, err
}
if !diskEncryptionConfig.Enabled {
return nil, nil
}
stmt := fmt.Sprintf(`
SELECT
CASE
WHEN (%s) THEN '%s'
WHEN (%s) THEN '%s'
WHEN (%s) THEN '%s'
WHEN (%s) THEN '%s'
WHEN (%s) THEN '%s'
ELSE ''
END AS status,
COALESCE(client_error, '') as detail,
hd.bitlocker_protection_status,
COALESCE(hd.tpm_pin_set, false) as tpm_pin_set
FROM
host_mdm hmdm
LEFT JOIN host_disk_encryption_keys hdek ON hmdm.host_id = hdek.host_id
LEFT JOIN host_disks hd ON hmdm.host_id = hd.host_id
WHERE
hmdm.host_id = ?`,
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionActionRequired, diskEncryptionConfig.BitLockerPINRequired),
fleet.DiskEncryptionActionRequired,
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionVerified, diskEncryptionConfig.BitLockerPINRequired),
fleet.DiskEncryptionVerified,
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionVerifying, diskEncryptionConfig.BitLockerPINRequired),
fleet.DiskEncryptionVerifying,
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionEnforcing, diskEncryptionConfig.BitLockerPINRequired),
fleet.DiskEncryptionEnforcing,
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionFailed, diskEncryptionConfig.BitLockerPINRequired),
fleet.DiskEncryptionFailed,
)
var dest struct {
Status fleet.DiskEncryptionStatus `db:"status"`
Detail string `db:"detail"`
ProtectionStatus *int `db:"bitlocker_protection_status"`
TpmPinSet bool `db:"tpm_pin_set"`
}
if err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, host.ID); err != nil {
if err != sql.ErrNoRows {
return &fleet.HostMDMDiskEncryption{}, err
}
// At this point we know disk encryption is enabled so if there are no rows for the
// host then we treat it as enforcing and log for potential debugging
ds.logger.DebugContext(ctx, "no bitlocker status found for host", "host_id", host.ID)
dest.Status = fleet.DiskEncryptionEnforcing
}
if dest.Status == "" {
// This is unexpected. We know that disk encryption is enabled so we treat it failed to draw
// attention to the issue and log potential debugging
ds.logger.DebugContext(ctx, "no bitlocker status found for host", "host_id", host.ID)
dest.Status = fleet.DiskEncryptionFailed
}
// Build a meaningful detail message for action_required when there's no client error.
if dest.Status == fleet.DiskEncryptionActionRequired && dest.Detail == "" {
protectionOff := dest.ProtectionStatus != nil && *dest.ProtectionStatus == fleet.BitLockerProtectionStatusOff
pinMissing := diskEncryptionConfig.BitLockerPINRequired && !dest.TpmPinSet
switch {
case protectionOff && pinMissing:
dest.Detail = "BitLocker protection is off and a required startup PIN is not set. The disk is encrypted but the TPM protector is not active, and a BitLocker PIN must be configured."
case protectionOff:
dest.Detail = "BitLocker protection is off. The disk is encrypted but the TPM protector is not active. This may be due to a suspended BitLocker state or a TPM configuration issue."
case pinMissing:
dest.Detail = "A required BitLocker startup PIN is not set. The disk is encrypted but a PIN must be configured for compliance."
}
}
return &fleet.HostMDMDiskEncryption{
Status: &dest.Status,
Detail: dest.Detail,
}, nil
}
func (ds *Datastore) GetMDMWindowsConfigProfile(ctx context.Context, profileUUID string) (*fleet.MDMWindowsConfigProfile, error) {
stmt := `
SELECT
profile_uuid,
team_id,
name,
syncml,
created_at,
uploaded_at
FROM
mdm_windows_configuration_profiles
WHERE
profile_uuid=?`
var res fleet.MDMWindowsConfigProfile
err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, profileUUID)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMWindowsProfile").WithName(profileUUID))
}
return nil, ctxerr.Wrap(ctx, err, "get mdm windows config profile")
}
labels, err := ds.listProfileLabelsForProfiles(ctx, []string{res.ProfileUUID}, nil, nil, nil)
if err != nil {
return nil, err
}
for _, lbl := range labels {
switch {
case lbl.Exclude && lbl.RequireAll:
// this should never happen so log it for debugging
ds.logger.DebugContext(ctx, "unsupported profile label: cannot be both exclude and require all",
"profile_uuid", lbl.ProfileUUID,
"label_name", lbl.LabelName,
)
case lbl.Exclude && !lbl.RequireAll:
res.LabelsExcludeAny = append(res.LabelsExcludeAny, lbl)
case !lbl.Exclude && !lbl.RequireAll:
res.LabelsIncludeAny = append(res.LabelsIncludeAny, lbl)
default:
// default include all
res.LabelsIncludeAll = append(res.LabelsIncludeAll, lbl)
}
}
return &res, nil
}
func (ds *Datastore) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUUID string) error {
// SyncML bytes and team ID are needed to generate <Delete> commands and
// scope the LocURI protection set to the profile's team.
var profile struct {
TeamID uint `db:"team_id"`
SyncML []byte `db:"syncml"`
}
if err := sqlx.GetContext(ctx, ds.reader(ctx), &profile,
`SELECT team_id, syncml FROM mdm_windows_configuration_profiles WHERE profile_uuid = ?`, profileUUID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return ctxerr.Wrap(ctx, notFound("MDMWindowsProfile").WithName(profileUUID))
}
return ctxerr.Wrap(ctx, err, "reading profile syncml before deletion")
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if err := deleteMDMWindowsConfigProfile(ctx, tx, profileUUID); err != nil {
return err
}
profileContents := map[string][]byte{profileUUID: profile.SyncML}
if err := ds.cancelWindowsHostInstallsForDeletedMDMProfiles(ctx, tx, profile.TeamID, []string{profileUUID}, profileContents); err != nil {
return err
}
return nil
})
}
func deleteMDMWindowsConfigProfile(ctx context.Context, tx sqlx.ExtContext, profileUUID string) error {
res, err := tx.ExecContext(ctx, `DELETE FROM mdm_windows_configuration_profiles WHERE profile_uuid=?`, profileUUID)
if err != nil {
return ctxerr.Wrap(ctx, err)
}
deleted, _ := res.RowsAffected() // cannot fail for mysql
if deleted != 1 {
return ctxerr.Wrap(ctx, notFound("MDMWindowsProfile").WithName(profileUUID))
}
return nil
}
// cancelWindowsHostInstallsForDeletedMDMProfiles handles host-profile cleanup
// when config profiles are deleted. It uses a two-phase approach:
// - Phase 1: Delete rows that were never sent to the device (NULL status + install)
// - Phase 2: For rows that were sent (non-NULL status + install), generate SyncML
// <Delete> commands and enqueue them, then mark the rows for removal.
//
// profTeamID is the team the deleted profiles belong to (0 for "No team"/Unassigned).
// It is passed by the caller rather than derived from the hosts table because
// host_mdm_windows_profiles rows can reference profile UUIDs from a host's
// previous team after the host has been moved (the rows stay marked for removal
// until the reconciler dispatches <Delete> commands). Deriving the team from
// those rows would return the host's current team, not the profile's team.
// The team is used to scope the LocURI protection set built from OTHER active
// profiles; without a fixed team scope, a profile in one team could spuriously
// suppress deletes for hosts whose rows happen to join to a different team.
func (ds *Datastore) cancelWindowsHostInstallsForDeletedMDMProfiles(
ctx context.Context, tx sqlx.ExtContext,
profTeamID uint, profileUUIDs []string, profileContents map[string][]byte,
) error {
if len(profileUUIDs) == 0 {
return nil
}
// Phase 0: Clean up remove+failed rows from previous failed removal attempts.
// These are terminal: the device already processed them, nothing more to do.
terminalStatuses := []fleet.MDMDeliveryStatus{fleet.MDMDeliveryFailed, fleet.MDMDeliveryVerified, fleet.MDMDeliveryVerifying}
delRemStmt, delRemArgs, err := sqlx.In(`
DELETE FROM host_mdm_windows_profiles
WHERE profile_uuid IN (?) AND operation_type = ? AND status IN (?)`,
profileUUIDs, fleet.MDMOperationTypeRemove, terminalStatuses)
if err != nil {
return ctxerr.Wrap(ctx, err, "building IN for phase 0 remove cleanup")
}
if _, err := tx.ExecContext(ctx, delRemStmt, delRemArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "cleaning up terminal remove rows")
}
// Phase 1: Delete host-profile rows that were never sent to the device.
const delNeverSentStmt = `
DELETE FROM host_mdm_windows_profiles
WHERE profile_uuid IN (?) AND status IS NULL AND operation_type = ?`
delStmt, delArgs, err := sqlx.In(delNeverSentStmt, profileUUIDs, fleet.MDMOperationTypeInstall)
if err != nil {
return ctxerr.Wrap(ctx, err, "building IN for phase 1 delete")
}
if _, err := tx.ExecContext(ctx, delStmt, delArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "deleting never-sent host profiles")
}
// Phase 2: Find rows that need <Delete> commands. This includes:
// - install rows with non-NULL status (profile was sent to device)
// - rows already marked for removal but whose <Delete> command hasn't
// been sent yet (e.g. the host moved teams and the profile was flagged
// for removal, but the command wasn't generated before the team was deleted)
const selectSentStmt = `
SELECT host_uuid, profile_uuid
FROM host_mdm_windows_profiles
WHERE profile_uuid IN (?)
AND ((status IS NOT NULL AND operation_type = ?) OR (operation_type = ? AND status IS NULL))`
selStmt, selArgs, err := sqlx.In(selectSentStmt, profileUUIDs, fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove)
if err != nil {
return ctxerr.Wrap(ctx, err, "building IN for phase 2 select")
}
var rowsToRemove []struct {
HostUUID string `db:"host_uuid"`
ProfileUUID string `db:"profile_uuid"`
}
if err := sqlx.SelectContext(ctx, tx, &rowsToRemove, selStmt, selArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "selecting sent host profiles for removal")
}
if len(rowsToRemove) == 0 {
return nil
}
// Group hosts by profile UUID for efficient command generation.
type removeTarget struct {
cmdUUID string
hostUUIDs []string
}
targets := make(map[string]*removeTarget)
for _, row := range rowsToRemove {
t := targets[row.ProfileUUID]
if t == nil {
t = &removeTarget{cmdUUID: uuid.NewString()}
targets[row.ProfileUUID] = t
}
t.hostUUIDs = append(t.hostUUIDs, row.HostUUID)
}
// Generate and enqueue <Delete> commands for each profile.
// Track which profiles were successfully enqueued so we only
// update rows that have a corresponding queued command.
// Collect LocURIs from OTHER active profiles so we
// don't send <Delete> for settings still enforced by a remaining profile.
// This prevents deleting one profile from undoing settings in another.
//
// This is a two-pass approach for performance:
// Pass 1 (team-wide): Build a global protection set from ALL other profiles
// in the team. This is fast and handles the common case.
// Pass 2 (per-host, only if needed): For any LocURIs that were protected in
// pass 1, check if the protecting profile actually applies to each host
// (considering label scope). If it doesn't, send the <Delete> anyway.
activeLocURIs := make(map[string]struct{})
// Map each protected LocURI to the profile UUIDs that protect it,
// so pass 2 can check per-host applicability.
locURIToProtectingProfiles := make(map[string][]string)
if len(profileUUIDs) > 0 {
// Query profile UUIDs and SyncML from profiles in the same team that
// are NOT being deleted. Failures here must not be swallowed: an empty
// activeLocURIs would make every LocURI look safe to delete and could
// undo settings still enforced by remaining profiles.
const activeProfilesStmt = `
SELECT profile_uuid, syncml FROM mdm_windows_configuration_profiles
WHERE team_id = ? AND profile_uuid NOT IN (?)`
apStmt, apArgs, err := sqlx.In(activeProfilesStmt, profTeamID, profileUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "building IN for active profiles LocURI protection")
}
var activeProfiles []struct {
ProfileUUID string `db:"profile_uuid"`
SyncML []byte `db:"syncml"`
}
if err := sqlx.SelectContext(ctx, tx, &activeProfiles, apStmt, apArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "selecting active profiles for LocURI protection")
}
for _, ap := range activeProfiles {
// Substitute SCEP variable so LocURIs are compared on
// resolved paths, consistent with the deleted profile side.
resolved := fleet.FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll(ap.SyncML, []byte(ap.ProfileUUID))
for _, uri := range fleet.ExtractLocURIsFromProfileBytes(resolved) {
activeLocURIs[uri] = struct{}{}
locURIToProtectingProfiles[uri] = append(locURIToProtectingProfiles[uri], ap.ProfileUUID)
}
}
}
enqueuedTargets := make(map[string]*removeTarget)
var pass2Params []locURIProtectionParams
for profUUID, target := range targets {
syncML, ok := profileContents[profUUID]
if !ok || len(syncML) == 0 {
ds.logger.WarnContext(ctx, "skipping delete command generation: no SyncML content", "profile.uuid", profUUID)
continue
}
// Extract all LocURIs from this profile (done once, reused for pass 2).
allURIs := fleet.ExtractLocURIsFromProfileBytes(
fleet.FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll(syncML, []byte(profUUID)),
)
// Partition into safe (not protected) and protected (in activeLocURIs).
var safeURIs, protectedURIs []string
for _, uri := range allURIs {
if _, isProtected := activeLocURIs[uri]; isProtected {
protectedURIs = append(protectedURIs, uri)
} else {
safeURIs = append(safeURIs, uri)
}
}
// Generate <Delete> commands for the safe (unprotected) LocURIs.
deleteCmd, err := fleet.BuildDeleteCommandFromLocURIs(safeURIs, target.cmdUUID)
if err != nil {
ds.logger.ErrorContext(ctx, "skipping delete command generation: build error",
"profile.uuid", profUUID, "err", err)
ctxerr.Handle(ctx, err)
continue
}
if deleteCmd != nil {
// Enqueue the primary delete command for unprotected LocURIs.
if err := ds.mdmWindowsInsertCommandForHostUUIDsDB(ctx, tx, target.hostUUIDs, deleteCmd); err != nil {
return ctxerr.Wrap(ctx, err, "inserting delete commands for hosts")
}
enqueuedTargets[profUUID] = target
} else {
// No primary delete command (all LocURIs protected or profile only
// has Exec commands). Delete the host-profile rows since the config
// profile is being removed and there's no command to track.
delSkipStmt, delSkipArgs, delSkipErr := sqlx.In(
`DELETE FROM host_mdm_windows_profiles WHERE profile_uuid = ? AND host_uuid IN (?)`,
profUUID, target.hostUUIDs)
if delSkipErr != nil {
return ctxerr.Wrap(ctx, delSkipErr, "building IN for protected profile cleanup")
}
if _, err := tx.ExecContext(ctx, delSkipStmt, delSkipArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "cleaning up protected profile rows")
}
}
// Collect protected URIs for pass 2 (label-scoped check).
if len(protectedURIs) > 0 {
pass2Params = append(pass2Params, locURIProtectionParams{
protectedURIs: protectedURIs,
hostUUIDs: target.hostUUIDs,
})
}
}
// Pass 2: For LocURIs that were protected in pass 1, check if the protecting
// profile is label-scoped and doesn't actually apply to some hosts. If so,
// send supplemental <Delete> commands for those specific hosts.
// This only runs when there are protected LocURIs, which is rare.
if len(pass2Params) > 0 {
if err := ds.checkAndEnqueueLabelScopedDeletes(ctx, tx, pass2Params, locURIToProtectingProfiles); err != nil {
return ctxerr.Wrap(ctx, err, "label-scoped LocURI protection check")
}
}
// Update host-profile rows only for profiles that had delete commands enqueued.
// This covers both install rows (being flipped to remove) and remove+NULL rows
// (being given a command_uuid and set to pending).
//
// Flatten (host_uuid, profile_uuid, cmd_uuid) triples across all profiles and
// batch them into a single UPDATE per batch. Each batch can span multiple
// profiles, with a CASE mapping each row's profile_uuid to its command_uuid.
// The WHERE clause uses a tuple IN on (host_uuid, profile_uuid), which matches
// the PK and lets the optimizer perform direct PK point lookups. This avoids
// the previous per-profile loop, which under-utilized batches when profiles
// affected fewer than batchSize hosts.
//
// Profile UUIDs are iterated in sorted order so concurrent callers
// acquire InnoDB row locks on host_mdm_windows_profiles in the same
// order, reducing the deadlock surface on this path. The SQL text
// itself is placeholder-only and already deterministic for a given
// batch size, so iteration order does not affect plan-cache / query
// digest stability.
type pendingRemoveRow struct {
hostUUID string
profileUUID string
cmdUUID string
}
sortedProfUUIDs := slices.Sorted(maps.Keys(enqueuedTargets))
totalRows := 0
for _, profUUID := range sortedProfUUIDs {
totalRows += len(enqueuedTargets[profUUID].hostUUIDs)
}
rows := make([]pendingRemoveRow, 0, totalRows)
for _, profUUID := range sortedProfUUIDs {
target := enqueuedTargets[profUUID]
for _, hostUUID := range target.hostUUIDs {
rows = append(rows, pendingRemoveRow{
hostUUID: hostUUID,
profileUUID: profUUID,
cmdUUID: target.cmdUUID,
})
}
}
if err := common_mysql.BatchProcessSimple(rows, windowsMDMProfileDeleteBatchSize, func(batch []pendingRemoveRow) error {
// Collect the profile_uuid -> cmd_uuid mapping needed by this batch. Most
// batches span 1 to N profiles; we only need one CASE arm per distinct
// profile in the batch.
profileCmds := make(map[string]string)
for _, r := range batch {
profileCmds[r.profileUUID] = r.cmdUUID
}
sortedBatchProfUUIDs := slices.Sorted(maps.Keys(profileCmds))
var sb strings.Builder
sb.WriteString(`UPDATE host_mdm_windows_profiles
SET operation_type = ?,
status = ?,
detail = '',
command_uuid = CASE profile_uuid`)
args := make([]any, 0, 2+2*len(profileCmds)+2*len(batch))
args = append(args, fleet.MDMOperationTypeRemove, fleet.MDMDeliveryPending)
for _, profUUID := range sortedBatchProfUUIDs {
sb.WriteString(" WHEN ? THEN ?")
args = append(args, profUUID, profileCmds[profUUID])
}
// ELSE command_uuid is defensive: WHERE restricts the update to rows
// whose profile_uuid is present in profileCmds, so in practice every
// updated row matches a WHEN arm.
sb.WriteString(` ELSE command_uuid END
WHERE (host_uuid, profile_uuid) IN (`)
for i, r := range batch {
if i > 0 {
sb.WriteByte(',')
}
sb.WriteString("(?,?)")
args = append(args, r.hostUUID, r.profileUUID)
}
sb.WriteByte(')')
if _, err := tx.ExecContext(ctx, sb.String(), args...); err != nil {
return ctxerr.Wrap(ctx, err, "updating host profiles to remove")
}
return nil
}); err != nil {
return err
}
return nil
}
// checkAndEnqueueLabelScopedDeletes identifies which protecting profiles are
// label-scoped and, if any, sends supplemental <Delete> commands for hosts
// where the protector doesn't apply.
func (ds *Datastore) checkAndEnqueueLabelScopedDeletes(
ctx context.Context,
tx sqlx.ExtContext,
toCheck []locURIProtectionParams,
locURIToProtectingProfiles map[string][]string,
) error {
// Collect all protecting profile UUIDs.
allProtectingUUIDs := make(map[string]struct{})
for _, uuids := range locURIToProtectingProfiles {
for _, u := range uuids {
allProtectingUUIDs[u] = struct{}{}
}
}
if len(allProtectingUUIDs) == 0 {
return nil
}
// Check which are label-scoped.
lsStmt, lsArgs, lsErr := sqlx.In(
`SELECT DISTINCT windows_profile_uuid FROM mdm_configuration_profile_labels
WHERE windows_profile_uuid IN (?)`, slices.Collect(maps.Keys(allProtectingUUIDs)))
if lsErr != nil {
return ctxerr.Wrap(ctx, lsErr, "building IN for label-scoped profile check")
}
var labelScoped []string
if err := sqlx.SelectContext(ctx, tx, &labelScoped, lsStmt, lsArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "querying label-scoped profiles")
}
labelScopedProfiles := make(map[string]struct{})
for _, u := range labelScoped {
labelScopedProfiles[u] = struct{}{}
}
if len(labelScopedProfiles) == 0 {
return nil
}
return ds.enqueueSupplementalDeletesForLabelScopedProtection(
ctx, tx, toCheck, locURIToProtectingProfiles, labelScopedProfiles)
}
// locURIProtectionParams holds the data needed by enqueueSupplementalDeletesForLabelScopedProtection.
type locURIProtectionParams struct {
// protectedURIs are the LocURIs from this profile that were filtered
// out by the team-wide protection in pass 1 (i.e., another profile in
// the team also targets them). Pass 2 checks per-host if the protector
// actually applies.
protectedURIs []string
hostUUIDs []string
}
// enqueueSupplementalDeletesForLabelScopedProtection handles pass 2 of
// LocURI protection. For each profile being deleted, it checks if any
// protected LocURIs are only protected by label-scoped profiles. If a
// label-scoped protector doesn't actually apply to a host, a supplemental
// <Delete> is enqueued for that host.
//
// Label type handling (include-any, include-all, exclude-any): this function
// does NOT re-implement label matching logic. Instead, it checks
// host_mdm_windows_profiles for an existing install assignment. The reconciler
// already evaluated all label types when it created those rows, so a row with
// operation_type='install' means the profile applies to that host regardless
// of how the label matching was computed.
//
// This is only called when there are protected LocURIs AND at least one
// protecting profile is label-scoped, which is rare.
func (ds *Datastore) enqueueSupplementalDeletesForLabelScopedProtection(
ctx context.Context,
tx sqlx.ExtContext,
profilesToCheck []locURIProtectionParams,
locURIToProtectingProfiles map[string][]string,
labelScopedProfiles map[string]struct{},
) error {
for _, p := range profilesToCheck {
if len(p.protectedURIs) == 0 || len(p.hostUUIDs) == 0 {
continue
}
// Filter to LocURIs where at least one protector is label-scoped.
var labelProtectedURIs []string
for _, uri := range p.protectedURIs {
for _, protector := range locURIToProtectingProfiles[uri] {
if _, isScoped := labelScopedProfiles[protector]; isScoped {
labelProtectedURIs = append(labelProtectedURIs, uri)
break
}
}
}
if len(labelProtectedURIs) == 0 {
continue
}
// Batch: get which label-scoped protecting profiles are installed on which hosts.
type hostProfile struct {
HostUUID string `db:"host_uuid"`
ProfileUUID string `db:"profile_uuid"`
}
var hostProfs []hostProfile
hpStmt, hpArgs, hpErr := sqlx.In(
`SELECT host_uuid, profile_uuid FROM host_mdm_windows_profiles
WHERE host_uuid IN (?) AND profile_uuid IN (?) AND operation_type = 'install'`,
p.hostUUIDs, slices.Collect(maps.Keys(labelScopedProfiles)))
if hpErr != nil {
return ctxerr.Wrap(ctx, hpErr, "building IN for host-profile label check")
}
if err := sqlx.SelectContext(ctx, tx, &hostProfs, hpStmt, hpArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "querying host-profile assignments for label check")
}
hostHasProfile := make(map[string]map[string]struct{})
for _, hp := range hostProfs {
if hostHasProfile[hp.HostUUID] == nil {
hostHasProfile[hp.HostUUID] = make(map[string]struct{})
}
hostHasProfile[hp.HostUUID][hp.ProfileUUID] = struct{}{}
}
// For each host, determine which protected LocURIs are safe to delete.
// Group hosts by their safe-URI set so we can batch the command insertion.
// Key: sorted comma-joined URIs; Value: list of host UUIDs.
hostsByURISet := make(map[string][]string)
for _, hostUUID := range p.hostUUIDs {
var hostSafeURIs []string
for _, uri := range labelProtectedURIs {
protectorApplies := false
for _, protectorUUID := range locURIToProtectingProfiles[uri] {
if _, isScoped := labelScopedProfiles[protectorUUID]; !isScoped {
protectorApplies = true // non-label profile, always applies
break
}
if _, ok := hostHasProfile[hostUUID][protectorUUID]; ok {
protectorApplies = true
break
}
}
if !protectorApplies {
hostSafeURIs = append(hostSafeURIs, uri)
}
}
if len(hostSafeURIs) > 0 {
slices.Sort(hostSafeURIs)
key := strings.Join(hostSafeURIs, ",")
hostsByURISet[key] = append(hostsByURISet[key], hostUUID)
}
}
// One command per unique URI set, shared across all hosts in the group.
for uriKey, hostUUIDs := range hostsByURISet {
uris := strings.Split(uriKey, ",")
cmdUUID := uuid.NewString()
deleteCmd, err := fleet.BuildDeleteCommandFromLocURIs(uris, cmdUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "building supplemental delete command")
}
if deleteCmd == nil {
continue
}
if err := ds.mdmWindowsInsertCommandForHostUUIDsDB(ctx, tx, hostUUIDs, deleteCmd); err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing supplemental delete for label-scoped LocURI")
}
}
}
return nil
}
func (ds *Datastore) DeleteMDMWindowsConfigProfileByTeamAndName(ctx context.Context, teamID *uint, profileName string) error {
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = *teamID
}
// Read the profile UUID and SyncML before the transaction to keep it short.
var profile struct {
ProfileUUID string `db:"profile_uuid"`
SyncML []byte `db:"syncml"`
}
if err := sqlx.GetContext(ctx, ds.reader(ctx), &profile,
`SELECT profile_uuid, syncml FROM mdm_windows_configuration_profiles WHERE team_id=? AND name=?`,
globalOrTeamID, profileName); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil // nothing to delete
}
return ctxerr.Wrap(ctx, err, "reading profile before deletion")
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if _, err := tx.ExecContext(ctx, `DELETE FROM mdm_windows_configuration_profiles WHERE profile_uuid=?`, profile.ProfileUUID); err != nil {
return ctxerr.Wrap(ctx, err)
}
profileContents := map[string][]byte{profile.ProfileUUID: profile.SyncML}
return ds.cancelWindowsHostInstallsForDeletedMDMProfiles(ctx, tx, globalOrTeamID, []string{profile.ProfileUUID}, profileContents)
})
}
// windowsHostProfileStatusSubquery returns a correlated SQL scalar subquery
// that resolves to one of `<statusPrefix>failed`, `<statusPrefix>pending`,
// `<statusPrefix>verifying`, `<statusPrefix>verified`, or '<empty>' for the host
// identified by h.uuid in the outer query.
//
// The subquery does a single aggregation pass over host_mdm_windows_profiles
// via the PK(host_uuid, profile_uuid) prefix.
//
// The returned SQL does NOT include outer parentheses; callers wrap in
// `(...)` as needed for the context (scalar subquery or CASE switch).
//
// Priority logic:
// - failed: any non-reserved profile has status='failed'.
// - pending: any non-reserved profile has status NULL or 'pending'.
// - verifying: at least one non-reserved install-type profile has
// status='verifying'.
// At this CASE branch we already know failed=0 and pending=0, so no
// profile has status NULL/pending/failed; since profile status is always
// one of {NULL,pending,failed,verifying,verified}, that leaves only
// verifying and verified for install-type rows.
// - verified: at least one non-reserved install-type profile has
// status='verified' and no install verifying exists (enforced by the
// earlier verifying branch).
func windowsHostProfileStatusSubquery(statusPrefix string) (string, []any, error) {
reserved := mdm.ListFleetReservedWindowsProfileNames()
stmt := fmt.Sprintf(`
SELECT CASE
WHEN SUM(CASE WHEN hmwp.status = ? AND hmwp.profile_name NOT IN (?) THEN 1 ELSE 0 END) > 0
THEN '%sfailed'
WHEN SUM(CASE WHEN (hmwp.status IS NULL OR hmwp.status = ?) AND hmwp.profile_name NOT IN (?) THEN 1 ELSE 0 END) > 0
THEN '%spending'
WHEN SUM(CASE WHEN hmwp.operation_type = ? AND hmwp.status = ? AND hmwp.profile_name NOT IN (?) THEN 1 ELSE 0 END) > 0
THEN '%sverifying'
WHEN SUM(CASE WHEN hmwp.operation_type = ? AND hmwp.status = ? AND hmwp.profile_name NOT IN (?) THEN 1 ELSE 0 END) > 0
THEN '%sverified'
ELSE ''
END
FROM host_mdm_windows_profiles hmwp
WHERE hmwp.host_uuid = h.uuid`,
statusPrefix, statusPrefix, statusPrefix, statusPrefix,
)
args := []any{
fleet.MDMDeliveryFailed, reserved,
fleet.MDMDeliveryPending, reserved,
fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying, reserved,
fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified, reserved,
}
return sqlx.In(stmt, args...)
}
func (ds *Datastore) GetMDMWindowsProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) {
diskEncryptionConfig, err := ds.GetConfigEnableDiskEncryption(ctx, teamID)
if err != nil {
return nil, err
}
var counts []statusCounts
if !diskEncryptionConfig.Enabled {
counts, err = getMDMWindowsStatusCountsProfilesOnlyDB(ctx, ds, teamID)
} else {
counts, err = getMDMWindowsStatusCountsProfilesAndBitLockerDB(ctx, ds, teamID, diskEncryptionConfig.BitLockerPINRequired)
}
if err != nil {
return nil, err
}
var res fleet.MDMProfilesSummary
// Note that hosts with "BitLocker action required" are counted as pending.
for _, c := range counts {
switch c.Status {
case "failed":
res.Failed = c.Count
case "pending":
res.Pending += c.Count
case "verifying":
res.Verifying = c.Count
case "verified":
res.Verified = c.Count
case "action_required":
res.Pending += c.Count
case "":
ds.logger.DebugContext(ctx, fmt.Sprintf("counted %d windows hosts on team %v with mdm turned on but no profiles or bitlocker status", c.Count, teamID))
default:
return nil, ctxerr.New(ctx, fmt.Sprintf("unexpected mdm windows status count: status=%s, count=%d", c.Status, c.Count))
}
}
return &res, nil
}
type statusCounts struct {
Status string `db:"final_status"`
Count uint `db:"count"`
}
func getMDMWindowsStatusCountsProfilesOnlyDB(ctx context.Context, ds *Datastore, teamID *uint) ([]statusCounts, error) {
profilesStatus, profilesStatusArgs, err := windowsHostProfileStatusSubquery("")
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "windows host profile status subquery")
}
args := make([]any, 0, len(profilesStatusArgs)+1)
args = append(args, profilesStatusArgs...)
teamFilter := "h.team_id IS NULL"
if teamID != nil && *teamID > 0 {
teamFilter = "h.team_id = ?"
args = append(args, *teamID)
}
// profilesStatus is a correlated scalar subquery that does one aggregation
// pass over host_mdm_windows_profiles per host (via the PK(host_uuid,
// profile_uuid) prefix) and resolves directly to one of
// 'failed'|'pending'|'verifying'|'verified'|''. It replaces the previous
// four correlated EXISTS (three with a nested NOT EXISTS) with a single
// PK range scan per outer row. The outer SELECT/FROM/WHERE/GROUP BY shape
// is preserved verbatim so row-level counts (including duplicate enrolled
// rows in mdm_windows_enrollments, if any) match the prior implementation.
stmt := fmt.Sprintf(`
SELECT
(%s) AS final_status,
SUM(1) AS count
FROM
hosts h
JOIN host_mdm hmdm ON h.id = hmdm.host_id
JOIN mdm_windows_enrollments mwe ON h.uuid = mwe.host_uuid
WHERE
mwe.device_state = '%s' AND
h.platform = 'windows' AND
hmdm.is_server = 0 AND
hmdm.enrolled = 1 AND
%s
GROUP BY
final_status`,
profilesStatus,
microsoft_mdm.MDMDeviceStateEnrolled,
teamFilter,
)
var counts []statusCounts
err = sqlx.SelectContext(ctx, ds.reader(ctx), &counts, stmt, args...)
if err != nil {
return nil, err
}
return counts, nil
}
func getMDMWindowsStatusCountsProfilesAndBitLockerDB(ctx context.Context, ds *Datastore, teamID *uint, bitLockerPINRequired bool) ([]statusCounts, error) {
profilesStatus, profilesStatusArgs, err := windowsHostProfileStatusSubquery("profiles_")
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "windows host profile status subquery")
}
args := make([]any, 0, len(profilesStatusArgs)+1)
args = append(args, profilesStatusArgs...)
teamFilter := "h.team_id IS NULL"
if teamID != nil && *teamID > 0 {
teamFilter = "h.team_id = ?"
args = append(args, *teamID)
}
bitlockerStatus := fmt.Sprintf(`
CASE WHEN (%s) THEN
'bitlocker_verified'
WHEN (%s) THEN
'bitlocker_verifying'
WHEN (%s) THEN
'bitlocker_action_required'
WHEN (%s) THEN
'bitlocker_pending'
WHEN (%s) THEN
'bitlocker_failed'
ELSE
''
END`,
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionVerified, bitLockerPINRequired),
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionVerifying, bitLockerPINRequired),
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionActionRequired, bitLockerPINRequired),
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionEnforcing, bitLockerPINRequired),
ds.whereBitLockerStatus(ctx, fleet.DiskEncryptionFailed, bitLockerPINRequired),
)
// profilesStatus is a scalar subquery that does one aggregation pass over
// host_mdm_windows_profiles per host (correlated on h.uuid).
stmt := fmt.Sprintf(`
SELECT
CASE (%s)
WHEN 'profiles_failed' THEN
'failed'
WHEN 'profiles_pending' THEN (
CASE (%s)
WHEN 'bitlocker_failed' THEN
'failed'
ELSE
'pending'
END)
WHEN 'profiles_verifying' THEN (
CASE (%s)
WHEN 'bitlocker_failed' THEN
'failed'
WHEN 'bitlocker_pending' THEN
'pending'
WHEN 'bitlocker_action_required' THEN
'pending'
ELSE
'verifying'
END)
WHEN 'profiles_verified' THEN (
CASE (%s)
WHEN 'bitlocker_failed' THEN
'failed'
WHEN 'bitlocker_pending' THEN
'pending'
WHEN 'bitlocker_action_required' THEN
'pending'
WHEN 'bitlocker_verifying' THEN
'verifying'
ELSE
'verified'
END)
ELSE
REPLACE((%s), 'bitlocker_', '')
END as final_status,
SUM(1) as count
FROM
hosts h
JOIN host_mdm hmdm ON h.id = hmdm.host_id
JOIN mdm_windows_enrollments mwe ON h.uuid = mwe.host_uuid
LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
LEFT JOIN host_disks hd ON hd.host_id = h.id
WHERE
mwe.device_state = '%s' AND
h.platform = 'windows' AND
hmdm.is_server = 0 AND
hmdm.enrolled = 1 AND
%s
GROUP BY
final_status`,
profilesStatus,
bitlockerStatus,
bitlockerStatus,
bitlockerStatus,
bitlockerStatus,
microsoft_mdm.MDMDeviceStateEnrolled,
teamFilter,
)
var counts []statusCounts
err = sqlx.SelectContext(ctx, ds.reader(ctx), &counts, stmt, args...)
if err != nil {
return nil, err
}
return counts, nil
}
const windowsMDMProfilesDesiredStateQuery = `
-- non label-based profiles
SELECT
mwcp.profile_uuid,
mwcp.name,
mwcp.checksum,
mwcp.secrets_updated_at,
h.uuid as host_uuid,
0 as count_profile_labels,
0 as count_non_broken_labels,
0 as count_host_labels,
0 as count_host_updated_after_labels
FROM
mdm_windows_configuration_profiles mwcp
JOIN hosts h
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
JOIN mdm_windows_enrollments mwe
ON mwe.host_uuid = h.uuid
WHERE
h.platform = 'windows' AND
NOT EXISTS (
SELECT 1
FROM mdm_configuration_profile_labels mcpl
WHERE mcpl.windows_profile_uuid = mwcp.profile_uuid
) AND
( %s )
UNION
-- label-based profiles where the host is a member of all the labels (include-all).
-- by design, "include" labels cannot match if they are broken (the host cannot be
-- a member of a deleted label).
SELECT
mwcp.profile_uuid,
mwcp.name,
mwcp.checksum,
mwcp.secrets_updated_at,
h.uuid as host_uuid,
COUNT(*) as count_profile_labels,
COUNT(mcpl.label_id) as count_non_broken_labels,
COUNT(lm.label_id) as count_host_labels,
0 as count_host_updated_after_labels
FROM
mdm_windows_configuration_profiles mwcp
JOIN hosts h
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
JOIN mdm_windows_enrollments mwe
ON mwe.host_uuid = h.uuid
JOIN mdm_configuration_profile_labels mcpl
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0 AND mcpl.require_all = 1
LEFT OUTER JOIN label_membership lm
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
WHERE
h.platform = 'windows' AND
( %s )
GROUP BY
mwcp.profile_uuid, mwcp.name, h.uuid
HAVING
count_profile_labels > 0 AND count_host_labels = count_profile_labels
UNION
-- label-based entities where the host is NOT a member of any of the labels (exclude-any).
-- explicitly ignore profiles with broken excluded labels so that they are never applied,
-- and ignore profiles that depend on labels created _after_ the label_updated_at timestamp
-- of the host (because we don't have results for that label yet, the host may or may not be
-- a member).
SELECT
mwcp.profile_uuid,
mwcp.name,
mwcp.checksum,
mwcp.secrets_updated_at,
h.uuid as host_uuid,
COUNT(*) as count_profile_labels,
COUNT(mcpl.label_id) as count_non_broken_labels,
COUNT(lm.label_id) as count_host_labels,
-- this helps avoid the case where the host is not a member of a label
-- just because it hasn't reported results for that label yet. But we
-- only need consider this for dynamic labels - manual(type=1) can be
-- considered at any time
SUM(
CASE WHEN lbl.label_membership_type <> 1 AND lbl.created_at IS NOT NULL AND h.label_updated_at >= lbl.created_at THEN 1
WHEN lbl.label_membership_type = 1 AND lbl.created_at IS NOT NULL THEN 1
ELSE 0 END) as count_host_updated_after_labels
FROM
mdm_windows_configuration_profiles mwcp
JOIN hosts h
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
JOIN mdm_windows_enrollments mwe
ON mwe.host_uuid = h.uuid
JOIN mdm_configuration_profile_labels mcpl
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 1 AND mcpl.require_all = 0
LEFT OUTER JOIN labels lbl
ON lbl.id = mcpl.label_id
LEFT OUTER JOIN label_membership lm
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
WHERE
h.platform = 'windows' AND
( %s )
GROUP BY
mwcp.profile_uuid, mwcp.name, h.uuid
HAVING
-- considers only the profiles with labels, without any broken label, with results reported after all labels were created and with the host not in any label
count_profile_labels > 0 AND count_profile_labels = count_non_broken_labels AND count_profile_labels = count_host_updated_after_labels AND count_host_labels = 0
UNION
-- label-based profiles where the host is a member of any of the labels (include-any).
-- by design, "include" labels cannot match if they are broken (the host cannot be
-- a member of a deleted label).
SELECT
mwcp.profile_uuid,
mwcp.name,
mwcp.checksum,
mwcp.secrets_updated_at,
h.uuid as host_uuid,
COUNT(*) as count_profile_labels,
COUNT(mcpl.label_id) as count_non_broken_labels,
COUNT(lm.label_id) as count_host_labels,
0 as count_host_updated_after_labels
FROM
mdm_windows_configuration_profiles mwcp
JOIN hosts h
ON h.team_id = mwcp.team_id OR (h.team_id IS NULL AND mwcp.team_id = 0)
JOIN mdm_windows_enrollments mwe
ON mwe.host_uuid = h.uuid
JOIN mdm_configuration_profile_labels mcpl
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 0 AND mcpl.require_all = 0
LEFT OUTER JOIN label_membership lm
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
WHERE
h.platform = 'windows' AND
( %s )
GROUP BY
mwcp.profile_uuid, mwcp.name, h.uuid
HAVING
count_profile_labels > 0 AND count_host_labels >= 1
`
func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
var result []*fleet.MDMWindowsProfilePayload
// TODO(mna): why is this in a transaction/reading from the primary, but not
// Apple's implementation? I see that the called private method is sometimes
// called inside a transaction, but when called from here it could (should?)
// be without and use the reader replica?
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
var err error
result, err = ds.listAllMDMWindowsProfilesToInstallDB(ctx, tx)
return err
})
return result, err
}
// The query below is a set difference between:
//
// - Set A (ds), the "desired state", can be obtained from a JOIN between
// mdm_windows_configuration_profiles and hosts.
//
// - Set B, the "current state" given by host_mdm_windows_profiles.
//
// A - B gives us the profiles that need to be installed:
//
// - profiles that are in A but not in B
//
// - profiles that are in A and in B, with an operation type of "install"
// and a NULL status. Other statuses mean that the operation is already in
// flight (pending), the operation has been completed but is still subject
// to independent verification by Fleet (verifying), or has reached a terminal
// state (failed or verified). If the profile's content is edited, all relevant hosts will
// be marked as status NULL so that it gets re-installed.
//
// Note that for label-based profiles, only fully-satisfied profiles are
// considered for installation. This means that a broken label-based profile,
// where one of the labels does not exist anymore, will not be considered for
// installation.
const windowsProfilesToInstallQuery = `
SELECT
ds.profile_uuid,
ds.host_uuid,
ds.name as profile_name,
ds.checksum,
ds.secrets_updated_at
FROM ( ` + windowsMDMProfilesDesiredStateQuery + ` ) as ds
LEFT JOIN host_mdm_windows_profiles hmwp
ON hmwp.profile_uuid = ds.profile_uuid AND hmwp.host_uuid = ds.host_uuid
WHERE
-- profile or secret variables have been updated
( hmwp.checksum != ds.checksum ) OR IFNULL(hmwp.secrets_updated_at < ds.secrets_updated_at, FALSE) OR
-- profiles in A but not in B
( hmwp.profile_uuid IS NULL AND hmwp.host_uuid IS NULL ) OR
-- profiles in A and B with operation type "install" and NULL status
( hmwp.host_uuid IS NOT NULL AND hmwp.operation_type = ? AND hmwp.status IS NULL ) OR
-- profiles in desired state that are currently marked for removal need
-- to be re-installed, excluding in-flight or completed removals
( hmwp.host_uuid IS NOT NULL AND hmwp.operation_type = ? AND COALESCE(hmwp.status, '') NOT IN ('verifying', 'verified') )
`
func (ds *Datastore) listAllMDMWindowsProfilesToInstallDB(ctx context.Context, tx sqlx.ExtContext) ([]*fleet.MDMWindowsProfilePayload, error) {
var profiles []*fleet.MDMWindowsProfilePayload
err := sqlx.SelectContext(ctx, tx, &profiles, fmt.Sprintf(windowsProfilesToInstallQuery, "TRUE", "TRUE", "TRUE", "TRUE"), fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove)
if err != nil {
return nil, ctxerr.Wrapf(ctx, err, "selecting windows MDM profiles to install")
}
return profiles, nil
}
func (ds *Datastore) listMDMWindowsProfilesToInstallDB(
ctx context.Context,
tx sqlx.QueryerContext,
hostUUIDs []string,
onlyProfileUUIDs []string,
) (profiles []*fleet.MDMWindowsProfilePayload, err error) {
if len(hostUUIDs) == 0 {
return profiles, nil
}
hostFilter := "h.uuid IN (?)"
if len(onlyProfileUUIDs) > 0 {
hostFilter = "mwcp.profile_uuid IN (?) AND h.uuid IN (?)"
}
toInstallQuery := fmt.Sprintf(windowsProfilesToInstallQuery, hostFilter, hostFilter, hostFilter, hostFilter)
// use a 10k host batch size to match what we do on the macOS side.
selectProfilesBatchSize := 10_000
if ds.testSelectMDMProfilesBatchSize > 0 {
selectProfilesBatchSize = ds.testSelectMDMProfilesBatchSize
}
selectProfilesTotalBatches := int(math.Ceil(float64(len(hostUUIDs)) / float64(selectProfilesBatchSize)))
for i := range selectProfilesTotalBatches {
start := i * selectProfilesBatchSize
end := min(start+selectProfilesBatchSize, len(hostUUIDs))
batchUUIDs := hostUUIDs[start:end]
var args []any
var stmt string
if len(onlyProfileUUIDs) > 0 {
stmt, args, err = sqlx.In(
toInstallQuery,
onlyProfileUUIDs, batchUUIDs,
onlyProfileUUIDs, batchUUIDs,
onlyProfileUUIDs, batchUUIDs,
onlyProfileUUIDs, batchUUIDs,
fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove,
)
} else {
stmt, args, err = sqlx.In(toInstallQuery, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove)
}
if err != nil {
return nil, ctxerr.Wrapf(ctx, err, "building sqlx.In for list MDM windows profiles to install, batch %d of %d", i, selectProfilesTotalBatches)
}
var partialResult []*fleet.MDMWindowsProfilePayload
err = sqlx.SelectContext(ctx, tx, &partialResult, stmt, args...)
if err != nil {
return nil, ctxerr.Wrapf(ctx, err, "selecting windows MDM profiles to install, batch %d of %d", i, selectProfilesTotalBatches)
}
profiles = append(profiles, partialResult...)
}
return profiles, nil
}
func (ds *Datastore) ListMDMWindowsProfilesToInstallForHost(ctx context.Context, hostUUID string) ([]*fleet.MDMWindowsProfilePayload, error) {
return ds.listMDMWindowsProfilesToInstallDB(ctx, ds.reader(ctx), []string{hostUUID}, nil)
}
func (ds *Datastore) ListMDMWindowsProfilesToRemove(ctx context.Context) ([]*fleet.MDMWindowsProfilePayload, error) {
var result []*fleet.MDMWindowsProfilePayload
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
var err error
result, err = ds.listAllMDMWindowsProfilesToRemoveDB(ctx, tx)
return err
})
return result, err
}
// ListMDMWindowsProfilesToInstallForHosts is the scoped variant of
// ListMDMWindowsProfilesToInstall: it returns only rows for the given host
// UUIDs. Used by the cron's batched reconciliation path to bound per-tick
// work; see ReconcileWindowsProfiles.
func (ds *Datastore) ListMDMWindowsProfilesToInstallForHosts(ctx context.Context, hostUUIDs []string) ([]*fleet.MDMWindowsProfilePayload, error) {
if len(hostUUIDs) == 0 {
return nil, nil
}
return ds.listMDMWindowsProfilesToInstallDB(ctx, ds.reader(ctx), hostUUIDs, nil)
}
// ListMDMWindowsProfilesToRemoveForHosts is the scoped variant of
// ListMDMWindowsProfilesToRemove: it returns only rows for the given host
// UUIDs. Used by the cron's batched reconciliation path to bound per-tick
// work; see ReconcileWindowsProfiles.
func (ds *Datastore) ListMDMWindowsProfilesToRemoveForHosts(ctx context.Context, hostUUIDs []string) ([]*fleet.MDMWindowsProfilePayload, error) {
if len(hostUUIDs) == 0 {
return nil, nil
}
return ds.listMDMWindowsProfilesToRemoveDB(ctx, ds.reader(ctx), hostUUIDs, nil)
}
// ListNextPendingMDMWindowsHostUUIDs returns up to batchSize host UUIDs
// (sorted ascending, lexicographic) where host_uuid > afterHostUUID and
// the host has any pending Windows MDM profile reconciliation work
// (install or remove). If afterHostUUID is empty, scanning starts from
// the beginning. The cron uses this to slice its per-tick work into a
// bounded host window; see ReconcileWindowsProfiles.
func (ds *Datastore) ListNextPendingMDMWindowsHostUUIDs(ctx context.Context, afterHostUUID string, batchSize int) ([]string, error) {
// Push the cursor predicate (host_uuid > ?) into each branch of the
// UNION so the optimizer applies it before deduplication. The install
// query has 4 host-filter slots, one per UNION branch in the
// desired-state subquery; each gets h.uuid > ?. The remove query
// inverts desired-state membership (it keeps rows where ds.host_uuid
// IS NULL), so its 4 desired-state slots stay TRUE; the cursor goes
// in the 5th slot, which filters hmwp.host_uuid after the RIGHT JOIN
// to host_mdm_windows_profiles. hmwp.host_uuid is the leading column
// of that table's PK, so this is a clean PK range scan.
toInstall := fmt.Sprintf(windowsProfilesToInstallQuery, "h.uuid > ?", "h.uuid > ?", "h.uuid > ?", "h.uuid > ?")
toRemove := fmt.Sprintf(windowsProfilesToRemoveQuery, "TRUE", "TRUE", "TRUE", "TRUE", "hmwp.host_uuid > ?")
stmt := fmt.Sprintf(`
SELECT host_uuid FROM (
SELECT host_uuid FROM (%s) AS install_set
UNION
SELECT host_uuid FROM (%s) AS remove_set
) AS combined
ORDER BY host_uuid
LIMIT %d
`, toInstall, toRemove, batchSize)
// Placeholder order in stmt:
// install branches: 4 cursor (h.uuid > ?), 2 op-type (install, remove)
// remove branches: 1 cursor (hmwp.host_uuid > ?)
var hostUUIDs []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hostUUIDs, stmt,
afterHostUUID, afterHostUUID, afterHostUUID, afterHostUUID,
fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove,
afterHostUUID,
); err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing next pending MDM windows host UUIDs")
}
return hostUUIDs, nil
}
// GetMDMWindowsReconcileCursor returns the persisted host_uuid cursor
// used by the Windows MDM reconciliation cron to bound per-tick work.
// Returns "" if no cursor is set or if the underlying datastore does not
// support cursor persistence (the bare mysql.Datastore in unit tests
// returns "" here; the mysqlredis wrapper backs it with Redis).
//
// See ReconcileWindowsProfiles.
func (ds *Datastore) GetMDMWindowsReconcileCursor(_ context.Context) (string, error) {
return "", nil
}
// SetMDMWindowsReconcileCursor persists the host_uuid cursor used by the
// Windows MDM reconciliation cron. The bare mysql.Datastore is a no-op
// here; the mysqlredis wrapper writes to Redis. See
// GetMDMWindowsReconcileCursor.
func (ds *Datastore) SetMDMWindowsReconcileCursor(_ context.Context, _ string) error {
return nil
}
// The query below is a set difference between:
//
// - Set A (ds), the desired state, can be obtained from a JOIN between
// mdm_windows_configuration_profiles and hosts.
// - Set B, the current state given by host_mdm_windows_profiles.
//
// # B - A gives us the profiles that need to be removed
//
// Any other case are profiles that are in both B and A, and as such are
// processed by the ListMDMWindowsProfilesToInstall method (since they are
// in both, their desired state is necessarily to be installed).
//
// Note that for label-based profiles, only those that are fully-satisfied
// by the host are considered for install (are part of the desired state used
// to compute the ones to remove). However, as a special case, a broken
// label-based profile will NOT be removed from a host where it was
// previously installed. However, if a host used to satisfy a label-based
// profile but no longer does (and that label-based profile is not "broken"),
// the profile will be removed from the host.
const windowsProfilesToRemoveQuery = `
SELECT
hmwp.profile_uuid,
hmwp.host_uuid,
hmwp.profile_name,
hmwp.operation_type,
COALESCE(hmwp.detail, '') as detail,
hmwp.status,
hmwp.command_uuid
FROM ( ` + windowsMDMProfilesDesiredStateQuery + ` ) as ds
RIGHT JOIN host_mdm_windows_profiles hmwp
ON hmwp.profile_uuid = ds.profile_uuid AND hmwp.host_uuid = ds.host_uuid
WHERE
-- profiles that are in B but not in A
ds.profile_uuid IS NULL AND ds.host_uuid IS NULL AND
-- only target hosts that still have a valid Windows MDM enrollment;
-- orphaned host_mdm_windows_profiles rows (where the enrollment was
-- deleted) cannot receive MDM commands and must be skipped.
EXISTS (
SELECT 1 FROM mdm_windows_enrollments mwe
WHERE mwe.host_uuid = hmwp.host_uuid
) AND
-- exclude remove operations with non-NULL status (already processed;
-- matches the pattern used by Fleet's Apple MDM profile removal)
(hmwp.operation_type != 'remove' OR hmwp.status IS NULL) AND
-- except "would be removed" profiles if they are a broken label-based profile
-- (regardless of if it is an include-all or exclude-any label)
NOT EXISTS (
SELECT 1
FROM mdm_configuration_profile_labels mcpl
WHERE
mcpl.windows_profile_uuid = hmwp.profile_uuid AND
mcpl.label_id IS NULL
) AND
(%s)
`
func (ds *Datastore) listAllMDMWindowsProfilesToRemoveDB(ctx context.Context, tx sqlx.ExtContext) (profiles []*fleet.MDMWindowsProfilePayload, err error) {
err = sqlx.SelectContext(ctx, tx, &profiles, fmt.Sprintf(windowsProfilesToRemoveQuery, "TRUE", "TRUE", "TRUE", "TRUE", "TRUE"))
if err != nil {
return nil, ctxerr.Wrapf(ctx, err, "selecting windows MDM profiles to remove")
}
return profiles, nil
}
func (ds *Datastore) listMDMWindowsProfilesToRemoveDB(
ctx context.Context,
tx sqlx.QueryerContext,
hostUUIDs []string,
onlyProfileUUIDs []string,
) (profiles []*fleet.MDMWindowsProfilePayload, err error) {
if len(hostUUIDs) == 0 {
return profiles, nil
}
hostFilter := "hmwp.host_uuid IN (?)"
if len(onlyProfileUUIDs) > 0 {
hostFilter = "hmwp.profile_uuid IN (?) AND hmwp.host_uuid IN (?)"
}
toRemoveQuery := fmt.Sprintf(windowsProfilesToRemoveQuery, "TRUE", "TRUE", "TRUE", "TRUE", hostFilter)
// use a 10k host batch size to match what we do on the macOS side.
selectProfilesBatchSize := 10_000
if ds.testSelectMDMProfilesBatchSize > 0 {
selectProfilesBatchSize = ds.testSelectMDMProfilesBatchSize
}
selectProfilesTotalBatches := int(math.Ceil(float64(len(hostUUIDs)) / float64(selectProfilesBatchSize)))
for i := range selectProfilesTotalBatches {
start := i * selectProfilesBatchSize
end := min(start+selectProfilesBatchSize, len(hostUUIDs))
batchUUIDs := hostUUIDs[start:end]
var err error
var args []any
var stmt string
if len(onlyProfileUUIDs) > 0 {
stmt, args, err = sqlx.In(toRemoveQuery, onlyProfileUUIDs, batchUUIDs)
} else {
stmt, args, err = sqlx.In(toRemoveQuery, batchUUIDs)
}
if err != nil {
return nil, ctxerr.Wrapf(ctx, err, "building sqlx.In for list MDM windows profiles to remove, batch %d of %d", i, selectProfilesTotalBatches)
}
var partialResult []*fleet.MDMWindowsProfilePayload
err = sqlx.SelectContext(ctx, tx, &partialResult, stmt, args...)
if err != nil {
return nil, ctxerr.Wrapf(ctx, err, "selecting windows MDM profiles to remove, batch %d of %d", i, selectProfilesTotalBatches)
}
profiles = append(profiles, partialResult...)
}
return profiles, nil
}
func (ds *Datastore) BulkUpsertMDMWindowsHostProfiles(ctx context.Context, payload []*fleet.MDMWindowsBulkUpsertHostProfilePayload) error {
if len(payload) == 0 {
return nil
}
executeUpsertBatch := func(valuePart string, args []any) error {
stmt := fmt.Sprintf(`
INSERT INTO host_mdm_windows_profiles (
profile_uuid,
host_uuid,
status,
operation_type,
detail,
command_uuid,
profile_name,
checksum
)
VALUES %s
ON DUPLICATE KEY UPDATE
status = VALUES(status),
operation_type = VALUES(operation_type),
detail = VALUES(detail),
profile_name = VALUES(profile_name),
checksum = VALUES(checksum),
command_uuid = VALUES(command_uuid)`,
strings.TrimSuffix(valuePart, ","),
)
_, err := ds.writer(ctx).ExecContext(ctx, stmt, args...)
return err
}
var (
args []any
sb strings.Builder
batchCount int
)
const defaultBatchSize = 1000 // results in this times 9 placeholders
batchSize := defaultBatchSize
if ds.testUpsertMDMDesiredProfilesBatchSize > 0 {
batchSize = ds.testUpsertMDMDesiredProfilesBatchSize
}
resetBatch := func() {
batchCount = 0
args = args[:0]
sb.Reset()
}
for _, p := range payload {
args = append(args, p.ProfileUUID, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID, p.ProfileName, p.Checksum)
sb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?),")
batchCount++
if batchCount >= batchSize {
if err := executeUpsertBatch(sb.String(), args); err != nil {
return err
}
resetBatch()
}
}
if batchCount > 0 {
if err := executeUpsertBatch(sb.String(), args); err != nil {
return err
}
}
return nil
}
// GetExistingMDMWindowsProfileUUIDs returns a set of the given profile UUIDs
// that still exist in mdm_windows_configuration_profiles. The cron
// reconciler uses this just before upserting host_mdm_windows_profiles rows
// to skip profiles that an admin deleted between the initial list and the
// upsert; without this guard a <Delete> command could never be built later
// (SyncML is gone), leaving a zombie install row.
func (ds *Datastore) GetExistingMDMWindowsProfileUUIDs(ctx context.Context, profileUUIDs []string) (map[string]struct{}, error) {
if len(profileUUIDs) == 0 {
return map[string]struct{}{}, nil
}
stmt, args, err := sqlx.In(
`SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE profile_uuid IN (?)`,
profileUUIDs,
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building IN for existing Windows profile UUIDs")
}
var rows []string
// Force a primary read: the guard exists to catch admin deletes that
// happened seconds ago (between the cron's initial list and the upsert).
// Replica lag could show a just-deleted profile as still present and
// defeat the guard.
ctx = ctxdb.RequirePrimary(ctx, true)
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting existing Windows profile UUIDs")
}
result := make(map[string]struct{}, len(rows))
for _, u := range rows {
result[u] = struct{}{}
}
return result, nil
}
func (ds *Datastore) GetMDMWindowsProfilesContents(ctx context.Context, uuids []string) (map[string]fleet.MDMWindowsProfileContents, error) {
if len(uuids) == 0 {
return nil, nil
}
stmt := `
SELECT profile_uuid, syncml, checksum
FROM mdm_windows_configuration_profiles WHERE profile_uuid IN (?)
`
query, args, err := sqlx.In(stmt, uuids)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building in statement")
}
var profs []struct {
ProfileUUID string `db:"profile_uuid"`
SyncML []byte `db:"syncml"`
Checksum []byte `db:"checksum"`
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profs, query, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "running query")
}
results := make(map[string]fleet.MDMWindowsProfileContents, len(profs))
for _, p := range profs {
results[p.ProfileUUID] = fleet.MDMWindowsProfileContents{
SyncML: p.SyncML,
Checksum: p.Checksum,
}
}
return results, nil
}
func (ds *Datastore) BulkDeleteMDMWindowsHostsConfigProfiles(ctx context.Context, profs []*fleet.MDMWindowsProfilePayload) error {
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
return ds.bulkDeleteMDMWindowsHostsConfigProfilesDB(ctx, tx, profs)
})
}
func (ds *Datastore) bulkDeleteMDMWindowsHostsConfigProfilesDB(
ctx context.Context,
tx sqlx.ExtContext,
profs []*fleet.MDMWindowsProfilePayload,
) error {
if len(profs) == 0 {
return nil
}
executeDeleteBatch := func(valuePart string, args []any) error {
stmt := fmt.Sprintf(`DELETE FROM host_mdm_windows_profiles WHERE (profile_uuid, host_uuid) IN (%s)`, strings.TrimSuffix(valuePart, ","))
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "error deleting host_mdm_windows_profiles")
}
return nil
}
var (
args []any
sb strings.Builder
batchCount int
)
const defaultBatchSize = 1000 // results in this times 2 placeholders
batchSize := defaultBatchSize
if ds.testDeleteMDMProfilesBatchSize > 0 {
batchSize = ds.testDeleteMDMProfilesBatchSize
}
resetBatch := func() {
batchCount = 0
args = args[:0]
sb.Reset()
}
for _, p := range profs {
args = append(args, p.ProfileUUID, p.HostUUID)
sb.WriteString("(?, ?),")
batchCount++
if batchCount >= batchSize {
if err := executeDeleteBatch(sb.String(), args); err != nil {
return err
}
resetBatch()
}
}
if batchCount > 0 {
if err := executeDeleteBatch(sb.String(), args); err != nil {
return err
}
}
return nil
}
func (ds *Datastore) NewMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDMWindowsConfigProfile, usesFleetVars []fleet.FleetVarName) (*fleet.MDMWindowsConfigProfile, error) {
profileUUID := "w" + uuid.New().String()
insertProfileStmt := `
INSERT INTO
mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml, uploaded_at)
(SELECT ?, ?, ?, ?, CURRENT_TIMESTAMP() FROM DUAL WHERE
NOT EXISTS (
SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ?
) AND NOT EXISTS (
SELECT 1 FROM mdm_apple_declarations WHERE name = ? AND team_id = ?
) AND NOT EXISTS (
SELECT 1 FROM mdm_android_configuration_profiles WHERE name = ? AND team_id = ?
)
)`
var teamID uint
if cp.TeamID != nil {
teamID = *cp.TeamID
}
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
res, err := tx.ExecContext(ctx, insertProfileStmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID, cp.Name, teamID, cp.Name, teamID)
if err != nil {
switch {
case IsDuplicate(err):
return &existsError{
ResourceType: "MDMWindowsConfigProfile.Name",
Identifier: cp.Name,
TeamID: cp.TeamID,
}
default:
return ctxerr.Wrap(ctx, err, "creating new windows mdm config profile")
}
}
aff, _ := res.RowsAffected()
if aff == 0 {
return &existsError{
ResourceType: "MDMWindowsConfigProfile.Name",
Identifier: cp.Name,
TeamID: cp.TeamID,
}
}
labels := make([]fleet.ConfigurationProfileLabel, 0, len(cp.LabelsIncludeAll)+len(cp.LabelsIncludeAny)+len(cp.LabelsExcludeAny))
for i := range cp.LabelsIncludeAll {
cp.LabelsIncludeAll[i].ProfileUUID = profileUUID
cp.LabelsIncludeAll[i].RequireAll = true
cp.LabelsIncludeAll[i].Exclude = false
labels = append(labels, cp.LabelsIncludeAll[i])
}
for i := range cp.LabelsIncludeAny {
cp.LabelsIncludeAny[i].ProfileUUID = profileUUID
cp.LabelsIncludeAny[i].RequireAll = false
cp.LabelsIncludeAny[i].Exclude = false
labels = append(labels, cp.LabelsIncludeAny[i])
}
for i := range cp.LabelsExcludeAny {
cp.LabelsExcludeAny[i].ProfileUUID = profileUUID
cp.LabelsExcludeAny[i].RequireAll = false
cp.LabelsExcludeAny[i].Exclude = true
labels = append(labels, cp.LabelsExcludeAny[i])
}
var profsWithoutLabel []string
if len(labels) == 0 {
profsWithoutLabel = append(profsWithoutLabel, profileUUID)
}
if _, err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, profsWithoutLabel, "windows"); err != nil {
return ctxerr.Wrap(ctx, err, "inserting windows profile label associations")
}
// Save Fleet variables associated with this Windows profile
if len(usesFleetVars) > 0 {
profilesVarsToUpsert := []fleet.MDMProfileUUIDFleetVariables{
{
ProfileUUID: profileUUID,
FleetVariables: usesFleetVars,
},
}
if _, err := batchSetProfileVariableAssociationsDB(ctx, tx, profilesVarsToUpsert, "windows", false); err != nil {
return ctxerr.Wrap(ctx, err, "inserting windows profile variable associations")
}
}
return nil
})
if err != nil {
return nil, err
}
return &fleet.MDMWindowsConfigProfile{
ProfileUUID: profileUUID,
Name: cp.Name,
SyncML: cp.SyncML,
TeamID: cp.TeamID,
}, nil
}
func (ds *Datastore) SetOrUpdateMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
profileUUID := fleet.MDMWindowsProfileUUIDPrefix + uuid.New().String()
stmt := `
INSERT INTO
mdm_windows_configuration_profiles (profile_uuid, team_id, name, syncml, uploaded_at)
(SELECT ?, ?, ?, ?, CURRENT_TIMESTAMP() FROM DUAL WHERE
NOT EXISTS (
SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ?
) AND NOT EXISTS (
SELECT 1 FROM mdm_apple_declarations WHERE name = ? AND team_id = ?
) AND NOT EXISTS (
SELECT 1 FROM mdm_android_configuration_profiles WHERE name = ? AND team_id = ?
)
)
ON DUPLICATE KEY UPDATE
uploaded_at = IF(syncml = VALUES(syncml), uploaded_at, CURRENT_TIMESTAMP()),
syncml = VALUES(syncml)
`
var teamID uint
if cp.TeamID != nil {
teamID = *cp.TeamID
}
res, err := ds.writer(ctx).ExecContext(ctx, stmt, profileUUID, teamID, cp.Name, cp.SyncML, cp.Name, teamID, cp.Name, teamID, cp.Name, teamID)
if err != nil {
switch {
case IsDuplicate(err):
return &existsError{
ResourceType: "MDMWindowsConfigProfile.Name",
Identifier: cp.Name,
TeamID: cp.TeamID,
}
default:
return ctxerr.Wrap(ctx, err, "creating new windows mdm config profile")
}
}
aff, _ := res.RowsAffected()
if aff == 0 {
return &existsError{
ResourceType: "MDMWindowsConfigProfile.Name",
Identifier: cp.Name,
TeamID: cp.TeamID,
}
}
return nil
}
// batchSetMDMWindowsProfilesDB must be called from inside a transaction.
func (ds *Datastore) batchSetMDMWindowsProfilesDB(
ctx context.Context,
tx sqlx.ExtContext,
tmID *uint,
profiles []*fleet.MDMWindowsConfigProfile,
profilesVariablesByIdentifier []fleet.MDMProfileIdentifierFleetVariables,
) (updatedDB bool, err error) {
const loadExistingProfiles = `
SELECT
name,
profile_uuid,
syncml
FROM
mdm_windows_configuration_profiles
WHERE
team_id = ? AND
name IN (?)
`
const deleteProfilesNotInList = `
DELETE FROM
mdm_windows_configuration_profiles
WHERE
team_id = ? AND
name NOT IN (?)
`
const loadToBeDeletedProfilesNotInList = `
SELECT
profile_uuid
FROM
mdm_windows_configuration_profiles
WHERE
team_id = ? AND
name NOT IN (?)
`
const deleteAllProfilesForTeam = `
DELETE FROM
mdm_windows_configuration_profiles
WHERE
team_id = ?
`
const loadToBeDeletedProfiles = `
SELECT
profile_uuid
FROM
mdm_windows_configuration_profiles
WHERE
team_id = ?
`
// For Windows profiles, if team_id and name are the same, we do an update. Otherwise, we do an insert.
const insertNewOrEditedProfile = `
INSERT INTO
mdm_windows_configuration_profiles (
profile_uuid, team_id, name, syncml, uploaded_at
)
VALUES
-- see https://stackoverflow.com/a/51393124/1094941
( CONCAT('` + fleet.MDMWindowsProfileUUIDPrefix + `', CONVERT(UUID() USING utf8mb4)), ?, ?, ?, CURRENT_TIMESTAMP() )
ON DUPLICATE KEY UPDATE
uploaded_at = IF(syncml = VALUES(syncml) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()),
name = VALUES(name),
syncml = VALUES(syncml)
`
// use a profile team id of 0 if no-team
var profTeamID uint
if tmID != nil {
profTeamID = *tmID
}
// build a list of names for the incoming profiles, will keep the
// existing ones if there's a match and no change
incomingNames := make([]string, len(profiles))
// at the same time, index the incoming profiles keyed by name for ease
// or processing
incomingProfs := make(map[string]*fleet.MDMWindowsConfigProfile, len(profiles))
for i, p := range profiles {
incomingNames[i] = p.Name
incomingProfs[p.Name] = p
}
var existingProfiles []*fleet.MDMWindowsConfigProfile
if len(incomingNames) > 0 {
// load existing profiles that match the incoming profiles by name
stmt, args, err := sqlx.In(loadExistingProfiles, profTeamID, incomingNames)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "build query to load existing profiles")
}
if err := sqlx.SelectContext(ctx, tx, &existingProfiles, stmt, args...); err != nil {
return false, ctxerr.Wrap(ctx, err, "load existing profiles")
}
}
// figure out if we need to delete any profiles
keepNames := make([]string, 0, len(incomingNames))
for _, p := range existingProfiles {
if newP := incomingProfs[p.Name]; newP != nil {
keepNames = append(keepNames, p.Name)
}
}
for n := range mdm.FleetReservedProfileNames() {
if _, ok := incomingProfs[n]; !ok {
// always keep reserved profiles even if they're not incoming
keepNames = append(keepNames, n)
}
}
// Identify, read SyncML for, delete, and handle host cleanup for obsolete
// profiles in a single sequential flow.
var (
stmt string
args []any
result sql.Result
deletedProfileUUIDs []string
deletedProfileContents = make(map[string][]byte)
)
// Step 1: Load UUIDs of profiles to be deleted.
if len(keepNames) > 0 {
stmt, args, err = sqlx.In(loadToBeDeletedProfilesNotInList, profTeamID, keepNames)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "build statement to load obsolete profiles")
}
} else {
stmt, args = loadToBeDeletedProfiles, []any{profTeamID}
}
if err = sqlx.SelectContext(ctx, tx, &deletedProfileUUIDs, stmt, args...); err != nil {
return false, ctxerr.Wrap(ctx, err, "load obsolete profiles")
}
// Step 2: Read SyncML bytes before deletion (needed to generate <Delete> commands).
if len(deletedProfileUUIDs) > 0 {
const readSyncMLStmt = `SELECT profile_uuid, syncml FROM mdm_windows_configuration_profiles WHERE profile_uuid IN (?)`
rdStmt, rdArgs, rdErr := sqlx.In(readSyncMLStmt, deletedProfileUUIDs)
if rdErr != nil {
return false, ctxerr.Wrap(ctx, rdErr, "building IN to read deleted profile syncml")
}
var profileRows []struct {
ProfileUUID string `db:"profile_uuid"`
SyncML []byte `db:"syncml"`
}
if err := sqlx.SelectContext(ctx, tx, &profileRows, rdStmt, rdArgs...); err != nil {
return false, ctxerr.Wrap(ctx, err, "reading deleted profile syncml")
}
for _, r := range profileRows {
deletedProfileContents[r.ProfileUUID] = r.SyncML
}
}
// Step 3: Delete the config profile rows.
if len(keepNames) > 0 {
stmt, args, err = sqlx.In(deleteProfilesNotInList, profTeamID, keepNames)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "build statement to delete obsolete profiles")
}
} else {
stmt, args = deleteAllProfilesForTeam, []any{profTeamID}
}
if result, err = tx.ExecContext(ctx, stmt, args...); err != nil {
return false, ctxerr.Wrap(ctx, err, "delete obsolete profiles")
}
rows, _ := result.RowsAffected()
updatedDB = rows > 0
// Step 4: Cancel pending installs and enqueue <Delete> commands for delivered profiles.
if len(deletedProfileUUIDs) > 0 {
if err := ds.cancelWindowsHostInstallsForDeletedMDMProfiles(ctx, tx, profTeamID, deletedProfileUUIDs, deletedProfileContents); err != nil {
return false, ctxerr.Wrap(ctx, err, "cancel installs of deleted profiles")
}
}
// For profiles being updated (same name, different content), diff the old
// and new LocURIs. Generate <Delete> commands for LocURIs that were removed
// so the device reverts those settings.
//
// This is an edge case (most edits change values, not remove LocURIs).
// The delete commands are best-effort and currently not visible to the
// IT admin in the UI or API. They are fire-and-forget MDM commands
// with no corresponding host_mdm_windows_profiles status entry.
// Two-pass LocURI protection for edited profiles:
// Pass 1 (team-wide): Build protection set from all retained profiles.
// Pass 2 (per-host): For protected LocURIs where the protector is
// label-scoped, check per-host if it actually applies.
//
// Known limitation: pass 2 runs before the INSERT (line ~2967) and
// batchSetLabelAndVariableAssociations (line ~2987), so:
// (a) Brand-new profiles don't have UUIDs yet (generated by MySQL on
// INSERT), so they appear in allRetainedURIs (pass 1 protects their
// LocURIs) but NOT in editLocURIProtectors. Pass 2 can't check their
// label scope.
// (b) Existing profiles whose label associations change in the same batch
// are checked against stale mdm_configuration_profile_labels rows and
// stale host_mdm_windows_profiles install rows.
// In both cases the result is over-protection: the delete is suppressed on
// all hosts even if the protector doesn't apply. The setting stays enforced
// on hosts outside the protector's label scope. Fixing this requires
// restructuring so pass 2 runs after the INSERT and label association.
allRetainedURIs := make(map[string]struct{})
// Track which profile UUID protects which LocURI for pass 2.
editLocURIProtectors := make(map[string][]string) // uri -> []profileUUID
// Build name-to-UUID lookup for incoming profiles.
incomingNameToUUID := make(map[string]string)
for _, ep := range existingProfiles {
incomingNameToUUID[ep.Name] = ep.ProfileUUID
}
for _, p := range incomingProfs {
// Normalize SCEP placeholders so LocURIs are compared on resolved
// paths, consistent with the delete path in cancelWindowsHostInstallsForDeletedMDMProfiles.
resolvedSyncML := p.SyncML
if puuid, ok := incomingNameToUUID[p.Name]; ok {
resolvedSyncML = fleet.FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll(p.SyncML, []byte(puuid))
}
for _, uri := range fleet.ExtractLocURIsFromProfileBytes(resolvedSyncML) {
allRetainedURIs[uri] = struct{}{}
if puuid, ok := incomingNameToUUID[p.Name]; ok {
editLocURIProtectors[uri] = append(editLocURIProtectors[uri], puuid)
}
}
}
// Include LocURIs from reserved profiles that are always kept. Reserved
// profiles may not be in existingProfiles (which only loads profiles
// matching incomingNames), so query them separately.
reservedNames := mdm.ListFleetReservedWindowsProfileNames()
if len(reservedNames) > 0 {
rpStmt, rpArgs, rpErr := sqlx.In(
`SELECT profile_uuid, syncml FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name IN (?)`,
profTeamID, reservedNames)
if rpErr != nil {
return false, ctxerr.Wrap(ctx, rpErr, "building IN for reserved profiles query")
}
var reservedProfiles []struct {
ProfileUUID string `db:"profile_uuid"`
SyncML []byte `db:"syncml"`
}
if err := sqlx.SelectContext(ctx, tx, &reservedProfiles, rpStmt, rpArgs...); err != nil {
return false, ctxerr.Wrap(ctx, err, "querying reserved profiles for LocURI protection")
}
for _, rp := range reservedProfiles {
resolved := fleet.FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll(rp.SyncML, []byte(rp.ProfileUUID))
for _, uri := range fleet.ExtractLocURIsFromProfileBytes(resolved) {
allRetainedURIs[uri] = struct{}{}
editLocURIProtectors[uri] = append(editLocURIProtectors[uri], rp.ProfileUUID)
}
}
}
for _, existing := range existingProfiles {
incoming := incomingProfs[existing.Name]
if incoming == nil || bytes.Equal(existing.SyncML, incoming.SyncML) {
continue
}
// Normalize SCEP placeholders for consistent LocURI comparison.
resolvedOld := fleet.FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll(existing.SyncML, []byte(existing.ProfileUUID))
resolvedNew := fleet.FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll(incoming.SyncML, []byte(existing.ProfileUUID))
oldURIs := fleet.ExtractLocURIsFromProfileBytes(resolvedOld)
newURIs := fleet.ExtractLocURIsFromProfileBytes(resolvedNew)
newSet := make(map[string]bool, len(newURIs))
for _, u := range newURIs {
newSet[u] = true
}
// Pass 1: team-wide protection.
var removedURIs []string
var protectedURIs []string
for _, u := range oldURIs {
if newSet[u] {
continue // still in updated profile
}
if _, ok := allRetainedURIs[u]; ok {
protectedURIs = append(protectedURIs, u)
} else {
removedURIs = append(removedURIs, u)
}
}
// Find hosts that have this profile installed (not pending removal).
var hostUUIDs []string
if len(removedURIs) > 0 || len(protectedURIs) > 0 {
if err := sqlx.SelectContext(ctx, tx, &hostUUIDs,
`SELECT host_uuid FROM host_mdm_windows_profiles WHERE profile_uuid = ? AND operation_type = ? AND status IS NOT NULL`,
existing.ProfileUUID, fleet.MDMOperationTypeInstall); err != nil {
return false, ctxerr.Wrap(ctx, err, "selecting hosts for edited profile LocURI cleanup")
}
}
// Send deletes for unprotected LocURIs (applies to all hosts).
if len(removedURIs) > 0 && len(hostUUIDs) > 0 {
cmdUUID := uuid.NewString()
deleteCmd, err := fleet.BuildDeleteCommandFromLocURIs(removedURIs, cmdUUID)
if err == nil && deleteCmd != nil {
ds.logger.InfoContext(ctx, "sending delete commands for LocURIs removed from edited profile",
"profile.name", existing.Name, "profile.uuid", existing.ProfileUUID, "removed_loc_uris", len(removedURIs))
if err := ds.mdmWindowsInsertCommandForHostUUIDsDB(ctx, tx, hostUUIDs, deleteCmd); err != nil {
return false, ctxerr.Wrap(ctx, err, "inserting delete commands for removed LocURIs")
}
}
}
// Pass 2: for protected LocURIs where the protector is label-scoped,
// check per-host if the protector actually applies.
if len(protectedURIs) > 0 && len(hostUUIDs) > 0 {
if err := ds.checkAndEnqueueLabelScopedDeletes(
ctx, tx,
[]locURIProtectionParams{{
protectedURIs: protectedURIs,
hostUUIDs: hostUUIDs,
}},
editLocURIProtectors,
); err != nil {
return false, ctxerr.Wrap(ctx, err, "label-scoped LocURI protection check for edited profile")
}
}
}
// insert the new profiles and the ones that have changed
for _, p := range incomingProfs {
if result, err = tx.ExecContext(ctx, insertNewOrEditedProfile, profTeamID, p.Name,
p.SyncML); err != nil {
return false, ctxerr.Wrapf(ctx, err, "insert new/edited profile with name %q", p.Name)
}
updatedDB = updatedDB || insertOnDuplicateDidInsertOrUpdate(result)
}
var mappedIncomingProfiles []*BatchSetAssociationIncomingProfile
for _, p := range profiles {
mappedIncomingProfiles = append(mappedIncomingProfiles, &BatchSetAssociationIncomingProfile{
Name: p.Name,
ProfileUUID: p.ProfileUUID,
LabelsIncludeAll: p.LabelsIncludeAll,
LabelsIncludeAny: p.LabelsIncludeAny,
LabelsExcludeAny: p.LabelsExcludeAny,
})
}
updatedLabels, err := ds.batchSetLabelAndVariableAssociations(ctx, tx, "windows", tmID, mappedIncomingProfiles, profilesVariablesByIdentifier)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "setting labels and variable associations")
}
return updatedDB || updatedLabels, nil
}
func (ds *Datastore) GetHostMDMWindowsProfiles(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error) {
stmt := fmt.Sprintf(`
SELECT
profile_uuid,
profile_name AS name,
-- internally, a NULL status implies that the cron needs to pick up
-- this profile, for the user that difference doesn't exist, the
-- profile is effectively pending. This is consistent with all our
-- aggregation functions.
COALESCE(status, '%s') AS status,
COALESCE(operation_type, '') AS operation_type,
COALESCE(detail, '') AS detail,
command_uuid
FROM
host_mdm_windows_profiles
WHERE
host_uuid = ? AND profile_name NOT IN(?) AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', '%s'))`,
fleet.MDMDeliveryPending,
fleet.MDMOperationTypeRemove,
fleet.MDMDeliveryPending,
fleet.MDMDeliveryVerifying,
fleet.MDMDeliveryVerified,
)
stmt, args, err := sqlx.In(stmt, hostUUID, mdm.ListFleetReservedWindowsProfileNames())
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building in statement")
}
var profiles []fleet.HostMDMWindowsProfile
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, args...); err != nil {
return nil, err
}
return profiles, nil
}
func (ds *Datastore) WipeHostViaWindowsMDM(ctx context.Context, host *fleet.Host, cmd *fleet.MDMWindowsCommand) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if err := ds.mdmWindowsInsertCommandForHostsDB(ctx, tx, []string{host.UUID}, cmd); err != nil {
return err
}
stmt := `
INSERT INTO host_mdm_actions (
host_id,
wipe_ref,
fleet_platform
)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
wipe_ref = VALUES(wipe_ref)`
if _, err := tx.ExecContext(ctx, stmt, host.ID, cmd.CommandUUID, host.FleetPlatform()); err != nil {
return ctxerr.Wrap(ctx, err, "modifying host_mdm_actions for wipe_ref")
}
return nil
})
}
func (ds *Datastore) GetWindowsHostMDMCertificateProfile(ctx context.Context, hostUUID string,
profileUUID string, caName string,
) (*fleet.HostMDMCertificateProfile, error) {
stmt := `
SELECT
hmwp.host_uuid,
hmwp.profile_uuid,
hmwp.status,
hmmc.challenge_retrieved_at,
hmmc.not_valid_before,
hmmc.not_valid_after,
hmmc.type,
hmmc.ca_name,
hmmc.serial
FROM
host_mdm_windows_profiles hmwp
JOIN host_mdm_managed_certificates hmmc
ON hmwp.host_uuid = hmmc.host_uuid AND hmwp.profile_uuid = hmmc.profile_uuid
WHERE
hmmc.host_uuid = ? AND hmmc.profile_uuid = ? AND hmmc.ca_name = ?`
var profile fleet.HostMDMCertificateProfile
if err := sqlx.GetContext(ctx, ds.reader(ctx), &profile, stmt, hostUUID, profileUUID, caName); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &profile, nil
}
func (ds *Datastore) GetWindowsMDMCommandsForResending(ctx context.Context, deviceID string, failedCommandIds []string) ([]*fleet.MDMWindowsCommand, error) {
if len(failedCommandIds) == 0 {
return []*fleet.MDMWindowsCommand{}, nil
}
stmt := `SELECT wmc.command_uuid, wmc.raw_command, wmc.target_loc_uri, wmc.created_at, wmc.updated_at
FROM windows_mdm_commands wmc INNER JOIN windows_mdm_command_queue wmcq ON wmcq.enrollment_id = (SELECT id from mdm_windows_enrollments WHERE mdm_device_id = ? ORDER BY created_at DESC, id DESC LIMIT 1) AND wmcq.command_uuid = wmc.command_uuid WHERE`
args := []any{deviceID}
for idx, commandId := range failedCommandIds {
if commandId == "" {
continue
}
stmt += " wmc.raw_command LIKE ? OR "
args = append(args, "%<CmdID>"+commandId+"</CmdID>%")
if idx == len(failedCommandIds)-1 {
stmt = strings.TrimSuffix(stmt, " OR ")
}
}
if len(args) == 1 {
// No valid command IDs were provided, return empty result to avoid returning all commands for the device.
return []*fleet.MDMWindowsCommand{}, nil
}
stmt += fmt.Sprintf(" ORDER BY created_at DESC LIMIT %d", len(failedCommandIds))
var commands []*fleet.MDMWindowsCommand
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &commands, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting windows mdm commands for resending")
}
return commands, nil
}
func (ds *Datastore) ResendWindowsMDMCommand(ctx context.Context, mdmDeviceId string, newCmd *fleet.MDMWindowsCommand, oldCmd *fleet.MDMWindowsCommand) error {
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
// First clear out any existing command queue references for the host
_, err := tx.ExecContext(ctx, `
DELETE FROM windows_mdm_command_queue WHERE enrollment_id = (
SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ? ORDER BY created_at DESC, id DESC LIMIT 1
) AND command_uuid = ?`, mdmDeviceId, oldCmd.CommandUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting existing command queue entries for old command")
}
if err := ds.mdmWindowsInsertCommandForHostsDB(ctx, tx, []string{mdmDeviceId}, newCmd); err != nil {
return ctxerr.Wrap(ctx, err, "inserting new windows mdm command for hosts")
}
updateStmt := fmt.Sprintf(`
UPDATE host_mdm_windows_profiles
SET command_uuid = ?,
status = '%s',
retries = retries, -- Keep retries the same to avoid endlessly resending.
detail = ''
WHERE host_uuid = (SELECT host_uuid FROM mdm_windows_enrollments WHERE mdm_device_id = ? ORDER BY created_at DESC, id DESC LIMIT 1) AND command_uuid = ?`, fleet.MDMDeliveryPending)
// Keep the profile in pending while we resend with Replace.
_, err = tx.ExecContext(ctx, updateStmt, newCmd.CommandUUID, mdmDeviceId, oldCmd.CommandUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "updating host_mdm_windows_profiles with new command uuid")
}
return nil
})
}
func (ds *Datastore) MDMWindowsUpdateEnrolledDeviceCredentials(ctx context.Context, deviceId string, credentialsHash []byte) error {
if deviceId == "" {
return nil
}
_, err := ds.writer(ctx).ExecContext(ctx, `
UPDATE mdm_windows_enrollments
SET credentials_hash = ?
WHERE mdm_device_id = ?`,
credentialsHash, deviceId,
)
return err
}
func (ds *Datastore) MDMWindowsAcknowledgeEnrolledDeviceCredentials(ctx context.Context, deviceId string) error {
if deviceId == "" {
return nil
}
_, err := ds.writer(ctx).ExecContext(ctx, `
UPDATE mdm_windows_enrollments
SET credentials_acknowledged = TRUE
WHERE mdm_device_id = ?`,
deviceId,
)
return err
}
func (ds *Datastore) CleanupWindowsMDMCommandQueue(ctx context.Context) error {
const batchSize = 1000
// Multi-table DELETE does not support LIMIT directly, so we use a
// subquery to select the rows to delete in batches.
const stmt = `
DELETE q FROM windows_mdm_command_queue q
INNER JOIN (
SELECT q2.enrollment_id, q2.command_uuid
FROM windows_mdm_command_queue q2
INNER JOIN windows_mdm_command_results r
ON r.enrollment_id = q2.enrollment_id AND r.command_uuid = q2.command_uuid
WHERE r.created_at < NOW() - INTERVAL 1 HOUR
LIMIT ?
) batch ON batch.enrollment_id = q.enrollment_id AND batch.command_uuid = q.command_uuid`
const maxBatches = 500 // cap total work per cron tick (500k rows)
var totalDeleted int64
exhausted := true
for range maxBatches {
res, err := ds.writer(ctx).ExecContext(ctx, stmt, batchSize)
if err != nil {
return ctxerr.Wrap(ctx, err, "cleanup windows mdm command queue")
}
n, _ := res.RowsAffected()
totalDeleted += n
if n < int64(batchSize) {
exhausted = false
break
}
}
if exhausted {
ds.logger.WarnContext(ctx, "cleanup windows mdm command queue did not finish, remaining rows will be cleaned on next run",
"deleted", totalDeleted, "max_batches", maxBatches)
}
return nil
}