mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #42327 We're not doing Windows because we're missing the failed activity for Windows profiles, which we do have for Apple. The actual code change is small. This PR is mostly test changes. ## Demo video and docs https://www.youtube.com/watch?v=YKNguaQQs_E https://github.com/fleetdm/fleet/pull/42332/changes # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Apple device configuration profiles (macOS, iOS, iPadOS) now automatically retry failed deliveries up to 3 times instead of once. * Windows configuration profiles maintain their existing single retry limit. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2569 lines
85 KiB
Go
2569 lines
85 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"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"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
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,
|
|
credentials_hash,
|
|
credentials_acknowledged,
|
|
created_at,
|
|
updated_at,
|
|
host_uuid
|
|
FROM mdm_windows_enrollments WHERE mdm_device_id = ? ORDER BY created_at 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,
|
|
credentials_hash,
|
|
credentials_acknowledged,
|
|
created_at,
|
|
updated_at,
|
|
host_uuid
|
|
FROM mdm_windows_enrollments WHERE host_uuid = ? ORDER BY created_at 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,
|
|
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),
|
|
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.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.
|
|
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)"
|
|
)
|
|
|
|
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:
|
|
// found the host uuid, clear its lock/wipe status
|
|
if hostUUID.Valid {
|
|
if _, err := tx.ExecContext(ctx, delActionsStmt, hostUUID.String); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "delete host_mdm_actions 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.
|
|
func (ds *Datastore) MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) error {
|
|
stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_device_id = ?"
|
|
|
|
res, err := ds.writer(ctx).ExecContext(ctx, stmt, mdmDeviceID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "delete MDMWindowsDeleteEnrolledDeviceWithDeviceID")
|
|
}
|
|
|
|
deleted, _ := res.RowsAffected()
|
|
if deleted == 1 {
|
|
return nil
|
|
}
|
|
|
|
return ctxerr.Wrap(ctx, notFound("MDMWindowsDeleteEnrolledDeviceWithDeviceID"))
|
|
}
|
|
|
|
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.ExecerContext, hostUUIDsOrDeviceIDs []string, cmd *fleet.MDMWindowsCommand) error {
|
|
// first, 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")
|
|
}
|
|
|
|
// create the command execution queue entries, one per host
|
|
for _, hostUUIDOrDeviceID := range hostUUIDsOrDeviceIDs {
|
|
if err := ds.mdmWindowsInsertHostCommandDB(ctx, tx, hostUUIDOrDeviceID, cmd.CommandUUID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ds *Datastore) mdmWindowsInsertHostCommandDB(ctx context.Context, tx sqlx.ExecerContext, hostUUIDOrDeviceID, commandUUID string) error {
|
|
stmt := `
|
|
INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid)
|
|
VALUES ((SELECT id FROM mdm_windows_enrollments WHERE host_uuid = ? OR mdm_device_id = ? ORDER BY created_at DESC LIMIT 1), ?)
|
|
`
|
|
|
|
if _, err := tx.ExecContext(ctx, stmt, hostUUIDOrDeviceID, hostUUIDOrDeviceID, commandUUID); err != nil {
|
|
if IsDuplicate(err) {
|
|
return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsCommandQueue", commandUUID))
|
|
}
|
|
return ctxerr.Wrap(ctx, err, "inserting MDMWindowsCommandQueue", "host_uuid_or_device_id", hostUUIDOrDeviceID, "command_uuid", commandUUID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MDMWindowsGetPendingCommands retrieves all commands awaiting execution for a
|
|
// given device ID.
|
|
func (ds *Datastore) MDMWindowsGetPendingCommands(ctx context.Context, deviceID string) ([]*fleet.MDMWindowsCommand, error) {
|
|
var commands []*fleet.MDMWindowsCommand
|
|
|
|
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
|
|
mdm_windows_enrollments mwe
|
|
ON
|
|
mwe.id = wmcq.enrollment_id
|
|
INNER JOIN
|
|
windows_mdm_commands wmc
|
|
ON
|
|
wmc.command_uuid = wmcq.command_uuid
|
|
WHERE
|
|
mwe.mdm_device_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
|
|
)
|
|
`
|
|
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &commands, query, deviceID); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get pending Windows MDM commands by device id")
|
|
}
|
|
|
|
return commands, nil
|
|
}
|
|
|
|
func (ds *Datastore) MDMWindowsSaveResponse(ctx context.Context, deviceID string, enrichedSyncML fleet.EnrichedSyncML, commandIDsBeingResent []string) error {
|
|
if len(enrichedSyncML.Raw) == 0 {
|
|
return ctxerr.New(ctx, "empty raw response")
|
|
}
|
|
|
|
enrolledDevice, err := ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "getting enrolled device with device ID")
|
|
}
|
|
|
|
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
// 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",
|
|
deviceID)
|
|
}
|
|
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
|
|
)
|
|
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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 != "" {
|
|
if err := updateHostLockWipeStatusFromResultAndHostUUID(ctx, tx, enrolledDevice.HostUUID,
|
|
"wipe_ref", wipeCmdUUID, strings.HasPrefix(wipeCmdStatus, "2"), false,
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "updating wipe command result in host_mdm_actions")
|
|
}
|
|
}
|
|
|
|
// dequeue the commands
|
|
var matchingUUIDs []string
|
|
for _, cmd := range matchingCmds {
|
|
matchingUUIDs = append(matchingUUIDs, cmd.CommandUUID)
|
|
}
|
|
const dequeueCommandsStmt = `DELETE FROM windows_mdm_command_queue WHERE enrollment_id = ? AND command_uuid IN (?)`
|
|
stmt, params, err = sqlx.In(dequeueCommandsStmt, enrolledDevice.ID, matchingUUIDs)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "building IN to dequeue commands")
|
|
}
|
|
if _, err = tx.ExecContext(ctx, stmt, params...); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "dequeuing commands")
|
|
}
|
|
|
|
return 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
|
|
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")
|
|
}
|
|
|
|
// batch-update the matching entries with the desired detail and status
|
|
var sb strings.Builder
|
|
args = args[:0]
|
|
for _, hp := range matchingHostProfiles {
|
|
payload := uuidsToPayloads[hp.CommandUUID]
|
|
if payload.Status != nil && *payload.Status == fleet.MDMDeliveryFailed {
|
|
if 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++
|
|
}
|
|
}
|
|
args = append(args, hp.HostUUID, hp.ProfileUUID, payload.Detail, payload.Status, hp.Retries, hp.Checksum)
|
|
sb.WriteString("(?, ?, ?, ?, ?, command_uuid, ?),")
|
|
}
|
|
|
|
values := strings.TrimSuffix(sb.String(), ",")
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
stmt = fmt.Sprintf(updateHostProfilesStmt, values)
|
|
_, err = tx.ExecContext(ctx, stmt, args...)
|
|
return ctxerr.Wrap(ctx, err, "updating host profiles")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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))`
|
|
)
|
|
|
|
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:
|
|
return whereNotServer + `
|
|
AND NOT ` + whereClientError + `
|
|
AND ` + whereKeyAvailable + `
|
|
AND ` + whereEncrypted + `
|
|
AND ` + whereHostDisksUpdated + `
|
|
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
|
|
return whereNotServer + `
|
|
AND NOT ` + whereClientError + `
|
|
AND ` + whereKeyAvailable + `
|
|
AND (
|
|
(` + whereEncrypted + ` AND NOT ` + whereHostDisksUpdated + `)
|
|
OR (NOT ` + whereEncrypted + ` AND ` + whereHostDisksUpdated + ` AND ` + withinGracePeriod + `)
|
|
)
|
|
AND ` + whereBitLockerPINSet
|
|
|
|
case fleet.DiskEncryptionActionRequired:
|
|
// Action required means we _would_ be in verified / verifying,
|
|
// but we require a PIN to be set and it's not.
|
|
return whereNotServer + `
|
|
AND NOT ` + whereClientError + `
|
|
AND ` + whereKeyAvailable + `
|
|
AND (
|
|
` + whereEncrypted + `
|
|
OR (NOT ` + whereEncrypted + ` AND ` + whereHostDisksUpdated + ` AND ` + withinGracePeriod + `)
|
|
)
|
|
AND NOT ` + whereBitLockerPINSet
|
|
|
|
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 action_required and removing_enforcement are 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
|
|
}
|
|
|
|
// Note action_required and removing_enforcement are not applicable to Windows hosts
|
|
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
|
|
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"`
|
|
}
|
|
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
|
|
}
|
|
|
|
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 {
|
|
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
if err := deleteMDMWindowsConfigProfile(ctx, tx, profileUUID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// cancel any pending host installs immediately for this profile
|
|
if err := cancelWindowsHostInstallsForDeletedMDMProfiles(ctx, tx, []string{profileUUID}); 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
|
|
}
|
|
|
|
func cancelWindowsHostInstallsForDeletedMDMProfiles(ctx context.Context, tx sqlx.ExtContext, profileUUIDs []string) error {
|
|
// For Windows, we currently don't support sending a command to remove a
|
|
// profile that was installed, so all we need to do here is delete any
|
|
// host-profile tuple that had this profile (whether with operation install
|
|
// or remove, does not matter).
|
|
const delStmt = `
|
|
DELETE FROM
|
|
host_mdm_windows_profiles
|
|
WHERE profile_uuid IN (?)`
|
|
|
|
if len(profileUUIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
stmt, args, err := sqlx.In(delStmt, profileUUIDs)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "building IN to delete host_mdm_windows_profiles")
|
|
}
|
|
|
|
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "deleting host_mdm_windows_profiles for deleted profile")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ds *Datastore) DeleteMDMWindowsConfigProfileByTeamAndName(ctx context.Context, teamID *uint, profileName string) error {
|
|
var globalOrTeamID uint
|
|
if teamID != nil {
|
|
globalOrTeamID = *teamID
|
|
}
|
|
_, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM mdm_windows_configuration_profiles WHERE team_id=? AND name=?`, globalOrTeamID, profileName)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func subqueryHostsMDMWindowsOSSettingsStatusFailed() (string, []interface{}, error) {
|
|
sql := `
|
|
SELECT
|
|
1 FROM host_mdm_windows_profiles hmwp
|
|
WHERE
|
|
h.uuid = hmwp.host_uuid
|
|
AND hmwp.status = ?
|
|
AND hmwp.profile_name NOT IN(?)`
|
|
args := []interface{}{
|
|
fleet.MDMDeliveryFailed,
|
|
mdm.ListFleetReservedWindowsProfileNames(),
|
|
}
|
|
|
|
return sqlx.In(sql, args...)
|
|
}
|
|
|
|
func subqueryHostsMDMWindowsOSSettingsStatusPending() (string, []interface{}, error) {
|
|
sql := `
|
|
SELECT
|
|
1 FROM host_mdm_windows_profiles hmwp
|
|
WHERE
|
|
h.uuid = hmwp.host_uuid
|
|
AND (hmwp.status IS NULL OR hmwp.status = ?)
|
|
AND hmwp.profile_name NOT IN(?)
|
|
AND NOT EXISTS (
|
|
SELECT
|
|
1 FROM host_mdm_windows_profiles hmwp2
|
|
WHERE (h.uuid = hmwp2.host_uuid
|
|
AND hmwp2.status = ?
|
|
AND hmwp2.profile_name NOT IN(?)))`
|
|
args := []interface{}{
|
|
fleet.MDMDeliveryPending,
|
|
mdm.ListFleetReservedWindowsProfileNames(),
|
|
fleet.MDMDeliveryFailed,
|
|
mdm.ListFleetReservedWindowsProfileNames(),
|
|
}
|
|
return sqlx.In(sql, args...)
|
|
}
|
|
|
|
func subqueryHostsMDMWindowsOSSettingsStatusVerifying() (string, []interface{}, error) {
|
|
sql := `
|
|
SELECT
|
|
1 FROM host_mdm_windows_profiles hmwp
|
|
WHERE
|
|
h.uuid = hmwp.host_uuid
|
|
AND hmwp.operation_type = ?
|
|
AND hmwp.status = ?
|
|
AND hmwp.profile_name NOT IN(?)
|
|
AND NOT EXISTS (
|
|
SELECT
|
|
1 FROM host_mdm_windows_profiles hmwp2
|
|
WHERE (h.uuid = hmwp2.host_uuid
|
|
AND hmwp2.operation_type = ?
|
|
AND hmwp2.profile_name NOT IN(?)
|
|
AND(hmwp2.status IS NULL
|
|
OR hmwp2.status NOT IN(?))))`
|
|
|
|
args := []interface{}{
|
|
fleet.MDMOperationTypeInstall,
|
|
fleet.MDMDeliveryVerifying,
|
|
mdm.ListFleetReservedWindowsProfileNames(),
|
|
fleet.MDMOperationTypeInstall,
|
|
mdm.ListFleetReservedWindowsProfileNames(),
|
|
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified},
|
|
}
|
|
return sqlx.In(sql, args...)
|
|
}
|
|
|
|
func subqueryHostsMDMWindowsOSSettingsStatusVerified() (string, []interface{}, error) {
|
|
sql := `
|
|
SELECT
|
|
1 FROM host_mdm_windows_profiles hmwp
|
|
WHERE
|
|
h.uuid = hmwp.host_uuid
|
|
AND hmwp.operation_type = ?
|
|
AND hmwp.status = ?
|
|
AND hmwp.profile_name NOT IN(?)
|
|
AND NOT EXISTS (
|
|
SELECT
|
|
1 FROM host_mdm_windows_profiles hmwp2
|
|
WHERE (h.uuid = hmwp2.host_uuid
|
|
AND hmwp2.operation_type = ?
|
|
AND hmwp2.profile_name NOT IN(?)
|
|
AND(hmwp2.status IS NULL
|
|
OR hmwp2.status != ?)))`
|
|
args := []interface{}{
|
|
fleet.MDMOperationTypeInstall,
|
|
fleet.MDMDeliveryVerified,
|
|
mdm.ListFleetReservedWindowsProfileNames(),
|
|
fleet.MDMOperationTypeInstall,
|
|
mdm.ListFleetReservedWindowsProfileNames(),
|
|
fleet.MDMDeliveryVerified,
|
|
}
|
|
return sqlx.In(sql, 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) {
|
|
var args []interface{}
|
|
subqueryFailed, subqueryFailedArgs, err := subqueryHostsMDMWindowsOSSettingsStatusFailed()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusFailed")
|
|
}
|
|
args = append(args, subqueryFailedArgs...)
|
|
subqueryPending, subqueryPendingArgs, err := subqueryHostsMDMWindowsOSSettingsStatusPending()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusPending")
|
|
}
|
|
args = append(args, subqueryPendingArgs...)
|
|
subqueryVerifying, subqueryVeryingingArgs, err := subqueryHostsMDMWindowsOSSettingsStatusVerifying()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusVerifying")
|
|
}
|
|
args = append(args, subqueryVeryingingArgs...)
|
|
subqueryVerified, subqueryVerifiedArgs, err := subqueryHostsMDMWindowsOSSettingsStatusVerified()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusVerified")
|
|
}
|
|
args = append(args, subqueryVerifiedArgs...)
|
|
|
|
teamFilter := "h.team_id IS NULL"
|
|
if teamID != nil && *teamID > 0 {
|
|
teamFilter = "h.team_id = ?"
|
|
args = append(args, *teamID)
|
|
}
|
|
|
|
stmt := fmt.Sprintf(`
|
|
SELECT
|
|
CASE
|
|
WHEN EXISTS (%s) THEN
|
|
'failed'
|
|
WHEN EXISTS (%s) THEN
|
|
'pending'
|
|
WHEN EXISTS (%s) THEN
|
|
'verifying'
|
|
WHEN EXISTS (%s) THEN
|
|
'verified'
|
|
ELSE
|
|
''
|
|
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
|
|
WHERE
|
|
mwe.device_state = '%s' AND
|
|
h.platform = 'windows' AND
|
|
hmdm.is_server = 0 AND
|
|
hmdm.enrolled = 1 AND
|
|
%s
|
|
GROUP BY
|
|
final_status`,
|
|
subqueryFailed,
|
|
subqueryPending,
|
|
subqueryVerifying,
|
|
subqueryVerified,
|
|
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) {
|
|
var args []interface{}
|
|
subqueryFailed, subqueryFailedArgs, err := subqueryHostsMDMWindowsOSSettingsStatusFailed()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusFailed")
|
|
}
|
|
args = append(args, subqueryFailedArgs...)
|
|
subqueryPending, subqueryPendingArgs, err := subqueryHostsMDMWindowsOSSettingsStatusPending()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusPending")
|
|
}
|
|
args = append(args, subqueryPendingArgs...)
|
|
subqueryVerifying, subqueryVeryingingArgs, err := subqueryHostsMDMWindowsOSSettingsStatusVerifying()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusVerifying")
|
|
}
|
|
args = append(args, subqueryVeryingingArgs...)
|
|
subqueryVerified, subqueryVerifiedArgs, err := subqueryHostsMDMWindowsOSSettingsStatusVerified()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusVerified")
|
|
}
|
|
args = append(args, subqueryVerifiedArgs...)
|
|
|
|
profilesStatus := fmt.Sprintf(`
|
|
CASE WHEN EXISTS (%s) THEN
|
|
'profiles_failed'
|
|
WHEN EXISTS (%s) THEN
|
|
'profiles_pending'
|
|
WHEN EXISTS (%s) THEN
|
|
'profiles_verifying'
|
|
WHEN EXISTS (%s) THEN
|
|
'profiles_verified'
|
|
ELSE
|
|
''
|
|
END`,
|
|
subqueryFailed,
|
|
subqueryPending,
|
|
subqueryVerifying,
|
|
subqueryVerified,
|
|
)
|
|
|
|
teamFilter := "h.team_id IS NULL"
|
|
if teamID != nil && *teamID > 0 {
|
|
teamFilter = "h.team_id = ?"
|
|
args = append(args, *teamID)
|
|
}
|
|
bitlockerJoin := `
|
|
LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
|
|
LEFT JOIN host_disks hd ON hd.host_id = h.id`
|
|
|
|
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),
|
|
)
|
|
|
|
stmt := fmt.Sprintf(`
|
|
SELECT
|
|
CASE (SELECT (%s) FROM hosts h2 WHERE h2.id = h.id)
|
|
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'
|
|
ELSE
|
|
'verifying'
|
|
END)
|
|
WHEN 'profiles_verified' THEN (
|
|
CASE (%s)
|
|
WHEN 'bitlocker_failed' THEN
|
|
'failed'
|
|
WHEN 'bitlocker_pending' 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
|
|
%s
|
|
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,
|
|
bitlockerJoin,
|
|
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 )
|
|
`
|
|
|
|
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)
|
|
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.ExtContext,
|
|
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,
|
|
)
|
|
} else {
|
|
stmt, args, err = sqlx.In(toInstallQuery, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeInstall)
|
|
}
|
|
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) 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
|
|
}
|
|
|
|
// 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.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
|
|
-- TODO(mna): why don't we have the same exception for "remove" operations as for Apple
|
|
|
|
-- 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.ExtContext,
|
|
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
|
|
}
|
|
|
|
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"); 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)
|
|
}
|
|
}
|
|
|
|
var (
|
|
stmt string
|
|
args []interface{}
|
|
)
|
|
// delete the obsolete profiles (all those that are not in keepNames)
|
|
var result sql.Result
|
|
var deletedProfileUUIDs []string
|
|
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")
|
|
}
|
|
if err = sqlx.SelectContext(ctx, tx, &deletedProfileUUIDs, stmt, args...); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "load obsolete profiles")
|
|
}
|
|
|
|
stmt, args, err = sqlx.In(deleteProfilesNotInList, profTeamID, keepNames)
|
|
if err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "build statement to delete obsolete profiles")
|
|
}
|
|
if result, err = tx.ExecContext(ctx, stmt, args...); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "delete obsolete profiles")
|
|
}
|
|
} else {
|
|
if err = sqlx.SelectContext(ctx, tx, &deletedProfileUUIDs, loadToBeDeletedProfiles, profTeamID); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "load obsolete profiles")
|
|
}
|
|
|
|
if result, err = tx.ExecContext(ctx, deleteAllProfilesForTeam,
|
|
profTeamID); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "delete all profiles for team")
|
|
}
|
|
}
|
|
if result != nil {
|
|
rows, _ := result.RowsAffected()
|
|
updatedDB = rows > 0
|
|
}
|
|
if len(deletedProfileUUIDs) > 0 {
|
|
// cancel installs of the deleted profiles immediately
|
|
if err := cancelWindowsHostInstallsForDeletedMDMProfiles(ctx, tx, deletedProfileUUIDs); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "cancel installs of deleted profiles")
|
|
}
|
|
}
|
|
|
|
// 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) bulkSetPendingMDMWindowsHostProfilesDB(
|
|
ctx context.Context,
|
|
tx sqlx.ExtContext,
|
|
hostUUIDs []string,
|
|
onlyProfileUUIDs []string,
|
|
) (updatedDB bool, err error) {
|
|
if len(hostUUIDs) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
profilesToInstall, err := ds.listMDMWindowsProfilesToInstallDB(ctx, tx, hostUUIDs, onlyProfileUUIDs)
|
|
if err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "list profiles to install")
|
|
}
|
|
|
|
profilesToRemove, err := ds.listMDMWindowsProfilesToRemoveDB(ctx, tx, hostUUIDs, onlyProfileUUIDs)
|
|
if err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "list profiles to remove")
|
|
}
|
|
|
|
if len(profilesToInstall) == 0 && len(profilesToRemove) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
if len(profilesToRemove) > 0 {
|
|
if err := ds.bulkDeleteMDMWindowsHostsConfigProfilesDB(ctx, tx, profilesToRemove); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "bulk delete profiles to remove")
|
|
}
|
|
updatedDB = true
|
|
}
|
|
if len(profilesToInstall) == 0 {
|
|
return updatedDB, nil
|
|
}
|
|
|
|
var (
|
|
pargs []any
|
|
profilesToInsert = make(map[string]*fleet.MDMWindowsProfilePayload)
|
|
psb strings.Builder
|
|
batchCount int
|
|
)
|
|
|
|
const defaultBatchSize = 1000
|
|
batchSize := defaultBatchSize
|
|
if ds.testUpsertMDMDesiredProfilesBatchSize > 0 {
|
|
batchSize = ds.testUpsertMDMDesiredProfilesBatchSize
|
|
}
|
|
|
|
resetBatch := func() {
|
|
batchCount = 0
|
|
pargs = pargs[:0]
|
|
clear(profilesToInsert)
|
|
psb.Reset()
|
|
}
|
|
|
|
executeUpsertBatch := func(valuePart string, args []any) error {
|
|
// Check if the update needs to be done at all.
|
|
selectStmt := fmt.Sprintf(`
|
|
SELECT
|
|
profile_uuid,
|
|
host_uuid,
|
|
status,
|
|
checksum,
|
|
COALESCE(operation_type, '') AS operation_type,
|
|
COALESCE(detail, '') AS detail,
|
|
COALESCE(command_uuid, '') AS command_uuid,
|
|
COALESCE(profile_name, '') AS profile_name
|
|
FROM host_mdm_windows_profiles WHERE (profile_uuid, host_uuid) IN (%s)`,
|
|
strings.TrimSuffix(strings.Repeat("(?,?),", len(profilesToInsert)), ","))
|
|
var selectArgs []any
|
|
for _, p := range profilesToInsert {
|
|
selectArgs = append(selectArgs, p.ProfileUUID, p.HostUUID)
|
|
}
|
|
var existingProfiles []fleet.MDMWindowsProfilePayload
|
|
if err := sqlx.SelectContext(ctx, tx, &existingProfiles, selectStmt, selectArgs...); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "bulk set pending profile status select existing")
|
|
}
|
|
var updateNeeded bool
|
|
if len(existingProfiles) == len(profilesToInsert) {
|
|
for _, exist := range existingProfiles {
|
|
insert, ok := profilesToInsert[fmt.Sprintf("%s\n%s", exist.ProfileUUID, exist.HostUUID)]
|
|
if !ok || !exist.Equal(*insert) {
|
|
updateNeeded = true
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
updateNeeded = true
|
|
}
|
|
if !updateNeeded {
|
|
// All profiles are already in the database, no need to update.
|
|
return nil
|
|
}
|
|
|
|
baseStmt := fmt.Sprintf(`
|
|
INSERT INTO host_mdm_windows_profiles (
|
|
profile_uuid,
|
|
host_uuid,
|
|
profile_name,
|
|
operation_type,
|
|
status,
|
|
command_uuid,
|
|
checksum
|
|
)
|
|
VALUES %s
|
|
ON DUPLICATE KEY UPDATE
|
|
operation_type = VALUES(operation_type),
|
|
status = NULL,
|
|
command_uuid = VALUES(command_uuid),
|
|
detail = '',
|
|
checksum = VALUES(checksum)
|
|
`, strings.TrimSuffix(valuePart, ","))
|
|
|
|
_, err := tx.ExecContext(ctx, baseStmt, args...)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute batch")
|
|
}
|
|
updatedDB = true
|
|
return nil
|
|
}
|
|
|
|
for _, p := range profilesToInstall {
|
|
profilesToInsert[fmt.Sprintf("%s\n%s", p.ProfileUUID, p.HostUUID)] = &fleet.MDMWindowsProfilePayload{
|
|
ProfileUUID: p.ProfileUUID,
|
|
ProfileName: p.ProfileName,
|
|
HostUUID: p.HostUUID,
|
|
Status: nil,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Detail: p.Detail,
|
|
CommandUUID: p.CommandUUID,
|
|
Retries: p.Retries,
|
|
Checksum: p.Checksum,
|
|
}
|
|
pargs = append(
|
|
pargs, p.ProfileUUID, p.HostUUID, p.ProfileName,
|
|
fleet.MDMOperationTypeInstall, p.Checksum)
|
|
psb.WriteString("(?, ?, ?, ?, NULL, '', ?),")
|
|
batchCount++
|
|
if batchCount >= batchSize {
|
|
if err := executeUpsertBatch(psb.String(), pargs); err != nil {
|
|
return false, err
|
|
}
|
|
resetBatch()
|
|
}
|
|
}
|
|
|
|
if batchCount > 0 {
|
|
if err := executeUpsertBatch(psb.String(), pargs); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
return updatedDB, 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) UpdateOrDeleteHostMDMWindowsProfile(ctx context.Context, profile *fleet.HostMDMWindowsProfile) error {
|
|
// Delete the host profile if it's remove and verified/verifying.
|
|
if profile.OperationType == fleet.MDMOperationTypeRemove && profile.Status != nil &&
|
|
(*profile.Status == fleet.MDMDeliveryVerifying || *profile.Status == fleet.MDMDeliveryVerified) {
|
|
_, err := ds.writer(ctx).ExecContext(ctx, `
|
|
DELETE FROM host_mdm_windows_profiles
|
|
WHERE host_uuid = ? AND command_uuid = ?
|
|
`, profile.HostUUID, profile.CommandUUID)
|
|
return err
|
|
}
|
|
|
|
detail := profile.Detail
|
|
|
|
if profile.OperationType == fleet.MDMOperationTypeRemove && profile.Status != nil && *profile.Status == fleet.MDMDeliveryFailed {
|
|
detail = fmt.Sprintf("Failed to remove: %s", detail)
|
|
}
|
|
|
|
status := profile.Status
|
|
// We need to run with retry due to potential deadlocks with BulkSetPendingMDMHostProfiles.
|
|
// Deadlock seen in 2024/12/12 loadtest: https://docs.google.com/document/d/1-Q6qFTd7CDm-lh7MVRgpNlNNJijk6JZ4KO49R1fp80U
|
|
|
|
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
_, err := tx.ExecContext(ctx, `
|
|
UPDATE host_mdm_windows_profiles
|
|
SET status = ?, operation_type = ?, detail = ?
|
|
WHERE host_uuid = ? AND command_uuid = ?
|
|
`, status, profile.OperationType, detail, profile.HostUUID, profile.CommandUUID)
|
|
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
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 = ?) 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 = ?
|
|
) 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 = ?) 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
|
|
}
|