fleet/server/datastore/mysql/mdm.go
Victor Lyuboslavsky 9d24f20c98
Added support of $FLEET_VAR_HOST_UUID in Windows MDM configuration profiles (#31695)
Fixes #30879 

Demo video: https://www.youtube.com/watch?v=jVyh5x8EMnc

I added a `FleetVarName` type, which should improve
safety/maintainability, but that resulted in a lot of files touched.

I also added the following. However, these are not strictly needed for
this feature (only useful for debug right now). But we are following the
pattern created by MDM team.

  1. Add the migration to insert HOST_UUID into fleet_variables
2. Update the Windows profile save logic to populate
mdm_configuration_profile_variables


# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host isolation]
- [x] QA'd all new/changed functionality manually



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

## Summary by CodeRabbit

* **New Features**
* Added support for the `$FLEET_VAR_HOST_UUID` variable in Windows MDM
configuration profiles, enabling per-host customization during profile
deployment.
* Enhanced profile delivery by substituting Fleet variables with actual
host data in Windows profiles.
* Introduced a database migration to register the new Fleet variable for
host UUID.

* **Bug Fixes**
* Improved validation and error handling to reject unsupported Fleet
variables in Windows MDM profiles with detailed messages.
* Ensured robust handling of errors during profile command insertion
without aborting the entire reconciliation process.

* **Tests**
* Added extensive tests covering validation, substitution, error
handling, and reconciliation workflows for Windows MDM profiles using
Fleet variables.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-10 12:24:38 +02:00

2182 lines
68 KiB
Go

package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/go-kit/log/level"
"github.com/google/go-cmp/cmp"
"github.com/jmoiron/sqlx"
)
func (ds *Datastore) GetMDMCommandPlatform(ctx context.Context, commandUUID string) (string, error) {
stmt := `
SELECT CASE
WHEN EXISTS (SELECT 1 FROM nano_commands WHERE command_uuid = ?) THEN 'darwin'
WHEN EXISTS (SELECT 1 FROM windows_mdm_commands WHERE command_uuid = ?) THEN 'windows'
ELSE ''
END AS platform
`
var p string
if err := sqlx.GetContext(ctx, ds.reader(ctx), &p, stmt, commandUUID, commandUUID); err != nil {
return "", err
}
if p == "" {
return "", ctxerr.Wrap(ctx, notFound("MDMCommand").WithName(commandUUID))
}
return p, nil
}
func getCombinedMDMCommandsQuery(ds *Datastore, hostFilter string) (string, []interface{}) {
appleStmt := `
SELECT
nvq.id as host_uuid,
nvq.command_uuid,
COALESCE(NULLIF(nvq.status, ''), 'Pending') as status,
COALESCE(nvq.result_updated_at, nvq.created_at) as updated_at,
nvq.request_type as request_type,
h.hostname,
h.team_id
FROM
nano_view_queue nvq
INNER JOIN
hosts h
ON
nvq.id = h.uuid
WHERE
nvq.active = 1
`
windowsStmt := `
SELECT
mwe.host_uuid,
wmc.command_uuid,
COALESCE(NULLIF(wmcr.status_code, ''), 'Pending') as status,
COALESCE(wmc.updated_at, wmc.created_at) as updated_at,
wmc.target_loc_uri as request_type,
h.hostname,
h.team_id
FROM windows_mdm_commands wmc
LEFT JOIN windows_mdm_command_queue wmcq ON wmcq.command_uuid = wmc.command_uuid
LEFT JOIN windows_mdm_command_results wmcr ON wmc.command_uuid = wmcr.command_uuid
INNER JOIN mdm_windows_enrollments mwe ON wmcq.enrollment_id = mwe.id OR wmcr.enrollment_id = mwe.id
INNER JOIN hosts h ON h.uuid = mwe.host_uuid
WHERE TRUE
`
var params []interface{}
appleStmtWithFilter, params := ds.whereFilterHostsByIdentifier(hostFilter, appleStmt, params)
windowsStmtWithFilter, params := ds.whereFilterHostsByIdentifier(hostFilter, windowsStmt, params)
stmt := fmt.Sprintf(
`SELECT * FROM ((%s) UNION ALL (%s)) as combined_commands WHERE `,
appleStmtWithFilter, windowsStmtWithFilter,
)
return stmt, params
}
func (ds *Datastore) ListMDMCommands(
ctx context.Context,
tmFilter fleet.TeamFilter,
listOpts *fleet.MDMCommandListOptions,
) ([]*fleet.MDMCommand, error) {
if listOpts != nil && listOpts.Filters.HostIdentifier != "" {
// separate codepath for more performant query by host identifier
return ds.listMDMCommandsByHostIdentifier(ctx, tmFilter, listOpts)
}
jointStmt, params := getCombinedMDMCommandsQuery(ds, listOpts.Filters.HostIdentifier)
jointStmt += ds.whereFilterHostsByTeams(tmFilter, "h")
jointStmt, params = addRequestTypeFilter(jointStmt, &listOpts.Filters, params)
jointStmt, params = appendListOptionsWithCursorToSQL(jointStmt, params, &listOpts.ListOptions)
var results []*fleet.MDMCommand
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, jointStmt, params...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "list commands")
}
return results, nil
}
// listMDMCommandsByHostIdentifier retrieves MDM commands by host identifier. It is implemented as a
// distinct code path to optimize the query for use cases where a client may be polling for the
// status of commands for a specific host.
//
// TODO: Additional optimizations not implemented yet:
// - restrict ordering by date to a new sorted index (probably `nano_enrollment_queue (id,
// created_at DESC)` would be a good candidate)
// - only search by hostname as a fallback if no results are found for UUID or hardware serial
func (ds *Datastore) listMDMCommandsByHostIdentifier(
ctx context.Context,
teamFilter fleet.TeamFilter,
listOpts *fleet.MDMCommandListOptions,
) ([]*fleet.MDMCommand, error) {
if listOpts == nil || listOpts.Filters.HostIdentifier == "" {
return nil, ctxerr.Wrap(ctx, errors.New("listMDMCommandsByHostIdentifier requires non-empty listOpts.Filters.HostIdentifier"))
}
// First, search for host by identifier (hostname, uuid, or hardware_serial).
//
// NOTE: We're not using existing methods like ds.whereFilterHostsByIdentifier,
// ds.HostIDsByIdentifier, ds.HostLiteByIdentifier because those methods are poorly
// optimized for the indexes we currently have on the hosts table.
// They filter with disjunctive conditions like `hostname = ? OR uuid = ?` as well as
// `? IN(hostname, uuid)`. These existing queries aren't really suited for either composite
// indexes or indexes on individual columns, and the optimizer ends up with executions that
// resort full table scans or minimally filtered results (when the optimizer is using
// indexes on team id and the like. Full-text indexes might be an option, but we've had
// difficulties managing those for the hosts table in the past.
//
// So we're writing a custom query here that uses a UNION with three subqueries, each targeting
// a specific column index: hostname, uuid, and hardware_serial.
identifier := listOpts.Filters.HostIdentifier
whereTeam := ds.whereFilterHostsByTeams(teamFilter, "h")
columns := "id, uuid, hardware_serial, hostname, platform, team_id"
// TODO: Add index for `hostname` or remove query? If removing, we'd need to update API
// documentation? Breaking change? For now, adding a secondary team filter inside hostname part
// of the union subquery to narrow the scope somewhat
stmt := `
SELECT ` + columns + ` FROM (
SELECT ` + columns + ` FROM hosts h WHERE hostname = ? AND ` + whereTeam + `
UNION SELECT ` + columns + ` FROM hosts WHERE uuid = ?
UNION SELECT ` + columns + ` FROM hosts WHERE hardware_serial = ? ) h
WHERE ` + whereTeam
var dest []fleet.Host // NOTE: we're using the hosts struct for convenience, but it will not be fully populated
args := []any{identifier, identifier, identifier}
err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, args...)
switch {
case err != nil:
return nil, ctxerr.Wrap(ctx, err, "get host by identifier for mdm")
case len(dest) == 0:
// TODO: should we return an empty slice or an error?
return []*fleet.MDMCommand{}, nil
case len(dest) > 1:
// TODO: how should we handle this unexpected case?
level.Debug(ds.logger).Log("msg", "list mdm commands: multiple hosts found for identifier",
"identifier", identifier, "count", len(dest),
)
}
// Next, build the query to list MDM commands. If the found host(s) are on the same platform,
// we can optimize the query by skipping the UNION ALL and using a single query targeted to the
// platform.
var appleStmt, winStmt string
var appleParams, winParams []any
var appleUUIDs, winUUIDs []string
byUUID := make(map[string]fleet.Host, len(dest)) // map UUID to host so that we can loop over command results to add hostname and team info and avoid joining hosts to commands in DB
for _, h := range dest {
if prev, ok := byUUID[h.UUID]; ok {
// TODO: how should we handle this unexpected case?
level.Debug(ds.logger).Log("msg", "list mdm commands: multiple hosts found for identifier",
"keeping", fmt.Sprintf("id: %d uuid: %s serial: %s hostname: %s platform: %s team: %+v", h.ID, h.UUID, h.HardwareSerial, h.Hostname, h.Platform, h.TeamID),
"skipping", fmt.Sprintf("id: %d uuid: %s serial: %s hostname: %s platform: %s team: %+v", prev.ID, prev.UUID, prev.HardwareSerial, prev.Hostname, prev.Platform, prev.TeamID),
)
}
byUUID[h.UUID] = h
switch fleet.MDMPlatform(h.Platform) {
case "darwin":
appleUUIDs = append(appleUUIDs, h.UUID)
case "windows":
winUUIDs = append(winUUIDs, h.UUID)
}
}
if len(appleUUIDs) > 0 {
appleParams = []any{appleUUIDs}
appleStmt = `
SELECT
nq.id AS host_uuid,
nc.command_uuid,
COALESCE(ncr.updated_at, nc.created_at) AS updated_at,
COALESCE(NULLIF(ncr.status, ''), 'Pending') AS status,
request_type
FROM
nano_enrollment_queue nq
JOIN nano_commands nc ON nq.command_uuid = nc.command_uuid
LEFT JOIN nano_command_results ncr ON nq.id = ncr.id
AND nc.command_uuid = ncr.command_uuid
WHERE
nq.id IN(?) AND nq.active = 1`
appleStmt, appleParams = addRequestTypeFilter(appleStmt, &listOpts.Filters, appleParams)
appleStmt, appleParams, err = sqlx.In(appleStmt, appleParams...)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "prepare query to list MDM commands for Apple devices")
}
}
if len(winUUIDs) > 0 {
winParams = []any{winUUIDs}
winStmt = `
SELECT
mwe.host_uuid,
wq.command_uuid,
COALESCE(wcr.updated_at, wc.created_at) AS updated_at,
COALESCE(NULLIF(wcr.status_code, ''), 'Pending') AS status,
wc.target_loc_uri AS request_type
FROM
windows_mdm_command_queue wq
JOIN mdm_windows_enrollments mwe ON mwe.id = wq.enrollment_id
JOIN windows_mdm_commands wc ON wc.command_uuid = wq.command_uuid
LEFT JOIN windows_mdm_command_results wcr ON wcr.command_uuid = wq.command_uuid
AND wcr.enrollment_id = wq.enrollment_id
WHERE
mwe.host_uuid IN (?)`
winStmt, winParams = addRequestTypeFilter(winStmt, &listOpts.Filters, winParams)
winStmt, winParams, err = sqlx.In(winStmt, winParams...)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "prepare query to list MDM commands for Windows devices")
}
}
var listStmt string
var params []any
switch {
case len(appleUUIDs) > 0 && len(winUUIDs) > 0:
listStmt = fmt.Sprintf(`SELECT * FROM ((%s) UNION ALL (%s)) u`,
appleStmt, winStmt)
params = append(params, appleParams...)
params = append(params, winParams...)
case len(appleUUIDs) > 0:
listStmt = appleStmt
params = appleParams
case len(winUUIDs) > 0:
listStmt = winStmt
params = winParams
}
// TODO: Maybe move this to the service method? What about pagination metadata?
if listOpts.OrderKey == "" {
listOpts.OrderKey = "updated_at"
}
// // FIXME: We probably ought to modify how listOptionsFromRequest in transport.go applies the
// // default order direction. Defaulting to ascending doesn't make sense for date fields like
// // updated_at. List options are decoded by transport before the specific gets the request
// // struct so there's no way apply a different default because at that point we can't tell if
// // the direction was set by the user or not. One approach would be to have listOptionsFromRequest
// // check if the order key is a date field (i.e. it ends with "_at") and default to descending
// // in those cases.
// if listOpts.OrderDirection == "" {
// listOpts.OrderDirection = fleet.OrderDescending
// }
if listOpts.PerPage == 0 {
listOpts.PerPage = 10
}
listStmt, params = appendListOptionsWithCursorToSQL(listStmt, params, &listOpts.ListOptions)
var results []*fleet.MDMCommand
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, listStmt, params...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "list commands")
}
// Add hostname and team info to the results based on the host UUIDs.
for i := range results {
if host, ok := byUUID[results[i].HostUUID]; ok {
results[i].Hostname = host.Hostname
results[i].TeamID = host.TeamID
}
}
return results, nil
}
func addRequestTypeFilter(stmt string, filter *fleet.MDMCommandFilters, params []interface{}) (string, []interface{}) {
if filter.RequestType != "" {
stmt += " AND request_type = ?"
params = append(params, filter.RequestType)
}
return stmt, params
}
func (ds *Datastore) getMDMCommand(ctx context.Context, q sqlx.QueryerContext, cmdUUID string) (*fleet.MDMCommand, error) {
stmt, _ := getCombinedMDMCommandsQuery(ds, "")
stmt += "command_uuid = ?"
var cmd fleet.MDMCommand
if err := sqlx.GetContext(ctx, q, &cmd, stmt, cmdUUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get mdm command by UUID")
}
return &cmd, nil
}
func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration, profilesVariablesByIdentifier []fleet.MDMProfileIdentifierFleetVariables,
) (updates fleet.MDMProfilesUpdates, err error) {
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
var err error
// Pass profilesVariablesByIdentifier to Windows profiles to save variable associations
if updates.WindowsConfigProfile, err = ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, winProfiles, profilesVariablesByIdentifier); err != nil {
return ctxerr.Wrap(ctx, err, "batch set windows profiles")
}
// Apple profiles also support Fleet variables
if updates.AppleConfigProfile, err = ds.batchSetMDMAppleProfilesDB(ctx, tx, tmID, macProfiles, profilesVariablesByIdentifier); err != nil {
return ctxerr.Wrap(ctx, err, "batch set apple profiles")
}
if updates.AppleDeclaration, err = ds.batchSetMDMAppleDeclarations(ctx, tx, tmID, macDeclarations); err != nil {
return ctxerr.Wrap(ctx, err, "batch set apple declarations")
}
return nil
})
return updates, err
}
func (ds *Datastore) ListMDMConfigProfiles(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
// this lists custom profiles, it explicitly filters out the fleet-reserved
// ones (reserved identifiers for Apple profiles, reserved names for Windows).
var profs []*fleet.MDMConfigProfilePayload
const selectStmt = `
SELECT
profile_uuid,
team_id,
name,
scope,
platform,
identifier,
checksum,
created_at,
uploaded_at
FROM (
SELECT
profile_uuid,
team_id,
name,
scope,
'darwin' as platform,
identifier,
checksum,
created_at,
uploaded_at
FROM
mdm_apple_configuration_profiles
WHERE
team_id = ? AND
identifier NOT IN (?)
UNION ALL
SELECT
profile_uuid,
team_id,
name,
'' as scope,
'windows' as platform,
'' as identifier,
'' as checksum,
created_at,
uploaded_at
FROM
mdm_windows_configuration_profiles
WHERE
team_id = ? AND
name NOT IN (?)
UNION ALL
SELECT
declaration_uuid AS profile_uuid,
team_id,
name,
scope,
'darwin' AS platform,
identifier,
token AS checksum,
created_at,
uploaded_at
FROM mdm_apple_declarations
WHERE team_id = ? AND
name NOT IN (?)
) as combined_profiles
`
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = *teamID
}
fleetIdentsMap := mobileconfig.FleetPayloadIdentifiers()
fleetIdentifiers := make([]string, 0, len(fleetIdentsMap))
for k := range fleetIdentsMap {
fleetIdentifiers = append(fleetIdentifiers, k)
}
fleetNamesMap := mdm.FleetReservedProfileNames()
fleetNames := make([]string, 0, len(fleetNamesMap))
for k := range fleetNamesMap {
fleetNames = append(fleetNames, k)
}
args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames, globalOrTeamID, fleetNames}
stmt, args := appendListOptionsWithCursorToSQL(selectStmt, args, &opt)
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "sqlx.In ListMDMConfigProfiles")
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profs, stmt, args...); err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "select profiles")
}
var metaData *fleet.PaginationMetadata
if opt.IncludeMetadata {
metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0}
if len(profs) > int(opt.PerPage) { //nolint:gosec // dismiss G115
metaData.HasNextResults = true
profs = profs[:len(profs)-1]
}
}
// load the labels associated with those profiles
var winProfUUIDs, macProfUUIDs, macDeclUUIDs []string
for _, prof := range profs {
if prof.Platform == "windows" {
winProfUUIDs = append(winProfUUIDs, prof.ProfileUUID)
} else {
if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
macDeclUUIDs = append(macDeclUUIDs, prof.ProfileUUID)
continue
}
macProfUUIDs = append(macProfUUIDs, prof.ProfileUUID)
}
}
labels, err := ds.listProfileLabelsForProfiles(ctx, winProfUUIDs, macProfUUIDs, macDeclUUIDs)
if err != nil {
return nil, nil, err
}
// match the labels with their profiles
profMap := make(map[string]*fleet.MDMConfigProfilePayload, len(profs))
for _, prof := range profs {
profMap[prof.ProfileUUID] = prof
}
for _, label := range labels {
if prof, ok := profMap[label.ProfileUUID]; ok {
switch {
case label.Exclude && label.RequireAll:
// this should never happen so log it for debugging
level.Debug(ds.logger).Log("msg", "unsupported profile label: cannot be both exclude and require all",
"profile_uuid", label.ProfileUUID,
"label_name", label.LabelName,
)
case label.Exclude && !label.RequireAll:
prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, label)
case !label.Exclude && !label.RequireAll:
prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, label)
default:
// default include all
prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, label)
}
}
}
return profs, metaData, nil
}
func (ds *Datastore) listProfileLabelsForProfiles(ctx context.Context, winProfUUIDs, macProfUUIDs, macDeclUUIDs []string) ([]fleet.ConfigurationProfileLabel, error) {
// load the labels associated with those profiles
const labelsStmt = `
SELECT
COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid,
label_name,
COALESCE(label_id, 0) as label_id,
IF(label_id IS NULL, 1, 0) as broken,
exclude,
require_all
FROM
mdm_configuration_profile_labels mcpl
WHERE
mcpl.apple_profile_uuid IN (?) OR
mcpl.windows_profile_uuid IN (?)
UNION ALL
SELECT
apple_declaration_uuid as profile_uuid,
label_name,
COALESCE(label_id, 0) as label_id,
IF(label_id IS NULL, 1, 0) as broken,
exclude,
require_all
FROM
mdm_declaration_labels mdl
WHERE
mdl.apple_declaration_uuid IN (?)
ORDER BY
profile_uuid, label_name
`
// ensure there's at least one (non-matching) value in the slice so the IN
// clause is valid
if len(winProfUUIDs) == 0 {
winProfUUIDs = []string{"-"}
}
if len(macProfUUIDs) == 0 {
macProfUUIDs = []string{"-"}
}
if len(macDeclUUIDs) == 0 {
macDeclUUIDs = []string{"-"}
}
stmt, args, err := sqlx.In(labelsStmt, macProfUUIDs, winProfUUIDs, macDeclUUIDs)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "sqlx.In to list labels for profiles")
}
var labels []fleet.ConfigurationProfileLabel
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &labels, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "select profiles labels")
}
return labels, nil
}
func (ds *Datastore) BulkSetPendingMDMHostProfiles(
ctx context.Context,
hostIDs, teamIDs []uint,
profileUUIDs, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
updates, err = ds.bulkSetPendingMDMHostProfilesDB(ctx, tx, hostIDs, teamIDs, profileUUIDs, hostUUIDs)
return err
})
return updates, err
}
// Note that team ID 0 is used for profiles that apply to hosts in no team
// (i.e. pass 0 in that case as part of the teamIDs slice). Only one of the
// slice arguments can have values.
func (ds *Datastore) bulkSetPendingMDMHostProfilesDB(
ctx context.Context,
tx sqlx.ExtContext,
hostIDs, teamIDs []uint,
profileUUIDs, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
var (
countArgs int
macProfUUIDs []string
winProfUUIDs []string
hasAppleDecls bool
)
if len(hostIDs) > 0 {
countArgs++
}
if len(teamIDs) > 0 {
countArgs++
}
if len(profileUUIDs) > 0 {
countArgs++
// split into mac and win profiles
for _, puid := range profileUUIDs {
if strings.HasPrefix(puid, fleet.MDMAppleProfileUUIDPrefix) { //nolint:gocritic // ignore ifElseChain
macProfUUIDs = append(macProfUUIDs, puid)
} else if strings.HasPrefix(puid, fleet.MDMAppleDeclarationUUIDPrefix) {
hasAppleDecls = true
} else {
// Note: defaulting to windows profiles without checking the prefix as
// many tests fail otherwise and it's a whole rabbit hole that I can't
// address at the moment.
winProfUUIDs = append(winProfUUIDs, puid)
}
}
}
if len(hostUUIDs) > 0 {
countArgs++
}
if countArgs > 1 {
return updates, errors.New("only one of hostIDs, teamIDs, profileUUIDs or hostUUIDs can be provided")
}
if countArgs == 0 {
return updates, nil
}
var countProfUUIDs int
if len(macProfUUIDs) > 0 {
countProfUUIDs++
}
if len(winProfUUIDs) > 0 {
countProfUUIDs++
}
if hasAppleDecls {
countProfUUIDs++
}
if countProfUUIDs > 1 {
return updates, errors.New("profile uuids must be all Apple profiles, all Apple declarations, or all Windows profiles")
}
var (
hosts []fleet.Host
args []any
uuidStmt string
)
switch {
case len(hostUUIDs) > 0:
// TODO: if a very large number (~65K) of uuids was provided, could
// result in too many placeholders (not an immediate concern).
uuidStmt = `SELECT uuid, platform FROM hosts WHERE uuid IN (?)`
args = append(args, hostUUIDs)
case len(hostIDs) > 0:
// TODO: if a very large number (~65K) of uuids was provided, could
// result in too many placeholders (not an immediate concern).
uuidStmt = `SELECT uuid, platform FROM hosts WHERE id IN (?)`
args = append(args, hostIDs)
case len(teamIDs) > 0:
// TODO: if a very large number (~65K) of team IDs was provided, could
// result in too many placeholders (not an immediate concern).
uuidStmt = `SELECT uuid, platform FROM hosts WHERE `
if len(teamIDs) == 1 && teamIDs[0] == 0 {
uuidStmt += `team_id IS NULL`
} else {
uuidStmt += `team_id IN (?)`
args = append(args, teamIDs)
for _, tmID := range teamIDs {
if tmID == 0 {
uuidStmt += ` OR team_id IS NULL`
break
}
}
}
case len(macProfUUIDs) > 0:
// TODO: if a very large number (~65K/2) of profile UUIDs was provided, could
// result in too many placeholders (not an immediate concern).
uuidStmt = `
SELECT DISTINCT h.uuid, h.platform
FROM hosts h
JOIN mdm_apple_configuration_profiles macp
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
LEFT JOIN host_mdm_apple_profiles hmap
ON h.uuid = hmap.host_uuid
WHERE
macp.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')
OR
hmap.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')`
args = append(args, macProfUUIDs, macProfUUIDs)
case len(winProfUUIDs) > 0:
// TODO: if a very large number (~65K/2) of profile IDs was provided, could
// result in too many placeholders (not an immediate concern).
uuidStmt = `
SELECT DISTINCT h.uuid, h.platform
FROM hosts h
JOIN mdm_windows_configuration_profiles mawp
ON h.team_id = mawp.team_id OR (h.team_id IS NULL AND mawp.team_id = 0)
LEFT JOIN host_mdm_windows_profiles hmwp
ON h.uuid = hmwp.host_uuid
WHERE
mawp.profile_uuid IN (?) AND h.platform = 'windows'
OR
hmwp.profile_uuid IN (?) AND h.platform = 'windows'`
args = append(args, winProfUUIDs, winProfUUIDs)
}
// TODO: this could be optimized to avoid querying for platform when
// profileIDs or profileUUIDs are provided.
if len(hosts) == 0 && !hasAppleDecls {
uuidStmt, args, err := sqlx.In(uuidStmt, args...)
if err != nil {
return updates, ctxerr.Wrap(ctx, err, "prepare query to load host UUIDs")
}
if err := sqlx.SelectContext(ctx, tx, &hosts, uuidStmt, args...); err != nil {
return updates, ctxerr.Wrap(ctx, err, "execute query to load host UUIDs")
}
}
var appleHosts []string
var winHosts []string
for _, h := range hosts {
switch h.Platform {
case "darwin", "ios", "ipados":
appleHosts = append(appleHosts, h.UUID)
case "windows":
winHosts = append(winHosts, h.UUID)
default:
level.Debug(ds.logger).Log(
"msg", "tried to set profile status for a host with unsupported platform",
"platform", h.Platform,
"host_uuid", h.UUID,
)
}
}
updates.AppleConfigProfile, err = ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts, profileUUIDs)
if err != nil {
return updates, ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles")
}
updates.WindowsConfigProfile, err = ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts, profileUUIDs)
if err != nil {
return updates, ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles")
}
const defaultBatchSize = 1000
batchSize := defaultBatchSize
if ds.testUpsertMDMDesiredProfilesBatchSize > 0 {
batchSize = ds.testUpsertMDMDesiredProfilesBatchSize
}
// TODO(roberto): this method currently sets the state of all
// declarations for all hosts. I don't see an immediate concern
// (and my hunch is that we could even do the same for
// profiles) but this could be optimized to use only a provided
// set of host uuids.
//
// Note(victor): Why is the status being set to nil? Shouldn't it be set to pending?
// Or at least pending for install and nil for remove profiles. Please update this comment if you know.
// This method is called bulkSetPendingMDMHostProfilesDB, so it is confusing that the status is NOT explicitly set to pending.
_, updates.AppleDeclaration, err = mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, nil)
if err != nil {
return updates, ctxerr.Wrap(ctx, err, "bulk set pending apple declarations")
}
return updates, nil
}
func (ds *Datastore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if err := setMDMProfilesVerifiedDB(ctx, tx, host, toVerify); err != nil {
return err
}
if err := setMDMProfilesFailedDB(ctx, tx, host, toFail); err != nil {
return err
}
if err := setMDMProfilesRetryDB(ctx, tx, host, toRetry); err != nil {
return err
}
return nil
})
}
// setMDMProfilesRetryDB sets the status of the given identifiers to retry (nil) and increments the retry count
func setMDMProfilesRetryDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
status = NULL,
detail = '',
retries = retries + 1
WHERE
host_uuid = ?
AND operation_type = ?
-- do not increment retry unnecessarily if the status is already null, no MDM command was sent
AND status IS NOT NULL
AND %s IN(?)`
args := []interface{}{
host.UUID,
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
var stmt string
switch host.Platform {
case "darwin", "ios", "ipados":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set retry host profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting retry host profiles")
}
return nil
}
// setMDMProfilesFailedDB sets the status of the given identifiers to failed if the current status
// is verifying or verified. It also sets the detail to a message indicating that the profile was
// either verifying or verified. Only profiles with the install operation type are updated.
func setMDMProfilesFailedDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
detail = if(status = ?, ?, ?),
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND %s IN(?)`
var stmt string
switch host.Platform {
case "darwin", "ios", "ipados":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
args := []interface{}{
fleet.MDMDeliveryVerifying,
fleet.HostMDMProfileDetailFailedWasVerifying,
fleet.HostMDMProfileDetailFailedWasVerified,
fleet.MDMDeliveryFailed,
host.UUID,
[]interface{}{
fleet.MDMDeliveryVerifying,
fleet.MDMDeliveryVerified,
},
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set failed host profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting failed host profiles")
}
return nil
}
// setMDMProfilesVerifiedDB sets the status of the given identifiers to verified if the current
// status is verifying. Only profiles with the install operation type are updated.
func setMDMProfilesVerifiedDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
detail = '',
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND %s IN(?)`
var stmt string
switch host.Platform {
case "darwin", "ios", "ipados":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
args := []interface{}{
fleet.MDMDeliveryVerified,
host.UUID,
[]interface{}{
fleet.MDMDeliveryPending,
fleet.MDMDeliveryVerifying,
fleet.MDMDeliveryFailed,
},
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set verified host macOS profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting verified host profiles")
}
return nil
}
func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
var teamID uint
if host.TeamID != nil {
teamID = *host.TeamID
}
switch host.Platform {
case "darwin", "ios", "ipados":
return ds.getHostMDMAppleProfilesExpectedForVerification(ctx, teamID, host)
case "windows":
return ds.getHostMDMWindowsProfilesExpectedForVerification(ctx, teamID, host.ID)
default:
return nil, fmt.Errorf("unsupported platform: %s", host.Platform)
}
}
func (ds *Datastore) getHostMDMWindowsProfilesExpectedForVerification(ctx context.Context, teamID, hostID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
stmt := `
-- profiles without labels
SELECT
mwcp.profile_uuid AS profile_uuid,
name,
syncml AS raw_profile,
min(mwcp.uploaded_at) AS earliest_install_date,
0 AS count_profile_labels,
0 AS count_non_broken_labels,
0 AS count_host_labels
FROM
mdm_windows_configuration_profiles mwcp
WHERE
mwcp.team_id = ? AND
NOT EXISTS (
SELECT
1
FROM
mdm_configuration_profile_labels mcpl
WHERE
mcpl.windows_profile_uuid = mwcp.profile_uuid
)
GROUP BY profile_uuid, name, syncml
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 AS profile_uuid,
name,
syncml AS raw_profile,
min(mwcp.uploaded_at) AS earliest_install_date,
COUNT(*) AS count_profile_labels,
COUNT(mcpl.label_id) as count_non_broken_labels,
COUNT(lm.label_id) AS count_host_labels
FROM
mdm_windows_configuration_profiles mwcp
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 = ?
WHERE
mwcp.team_id = ?
GROUP BY
profile_uuid, name, syncml
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.
SELECT
mwcp.profile_uuid AS profile_uuid,
name,
syncml AS raw_profile,
min(mwcp.uploaded_at) AS earliest_install_date,
COUNT(*) AS count_profile_labels,
COUNT(mcpl.label_id) as count_non_broken_labels,
COUNT(lm.label_id) AS count_host_labels
FROM
mdm_windows_configuration_profiles mwcp
JOIN mdm_configuration_profile_labels mcpl
ON mcpl.windows_profile_uuid = mwcp.profile_uuid AND mcpl.exclude = 1
LEFT OUTER JOIN label_membership lm
ON lm.label_id = mcpl.label_id AND lm.host_id = ?
WHERE
mwcp.team_id = ?
GROUP BY
profile_uuid, name, syncml
HAVING
-- considers only the profiles with labels, without any broken label, and with the host not in any label
count_profile_labels > 0 AND
count_profile_labels = count_non_broken_labels AND
count_host_labels = 0
UNION
-- label-based profiles where the host is a member of at least one of the labels (include-any)
SELECT
mwcp.profile_uuid AS profile_uuid,
name,
syncml AS raw_profile,
min(mwcp.uploaded_at) AS earliest_install_date,
COUNT(*) AS count_profile_labels,
COUNT(mcpl.label_id) as count_non_broken_labels,
COUNT(lm.label_id) AS count_host_labels
FROM
mdm_windows_configuration_profiles mwcp
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 = ?
WHERE
mwcp.team_id = ?
GROUP BY
profile_uuid, name, syncml
HAVING
count_profile_labels > 0 AND
count_host_labels > 0
`
var profiles []*fleet.ExpectedMDMProfile
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, teamID, hostID, teamID, hostID, teamID, hostID, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "running query for windows profiles")
}
byName := make(map[string]*fleet.ExpectedMDMProfile, len(profiles))
for _, r := range profiles {
byName[r.Name] = r
}
return byName, nil
}
func (ds *Datastore) getHostMDMAppleProfilesExpectedForVerification(ctx context.Context, teamID uint, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
// TODO This will need to be updated to support scopes
stmt := `
-- profiles without labels
SELECT
macp.profile_uuid AS profile_uuid,
macp.identifier AS identifier,
0 AS count_profile_labels,
0 AS count_non_broken_labels,
0 AS count_host_labels,
earliest_install_date
FROM
mdm_apple_configuration_profiles macp
JOIN (
SELECT
checksum,
min(uploaded_at) AS earliest_install_date
FROM
mdm_apple_configuration_profiles
GROUP BY checksum
) cs ON macp.checksum = cs.checksum
WHERE
macp.team_id = ? AND
NOT EXISTS (
SELECT
1
FROM
mdm_configuration_profile_labels mcpl
WHERE
mcpl.apple_profile_uuid = macp.profile_uuid
)
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
macp.profile_uuid AS profile_uuid,
macp.identifier AS identifier,
COUNT(*) AS count_profile_labels,
COUNT(mcpl.label_id) AS count_non_broken_labels,
COUNT(lm.label_id) AS count_host_labels,
min(earliest_install_date) AS earliest_install_date
FROM
mdm_apple_configuration_profiles macp
JOIN (
SELECT
checksum,
min(uploaded_at) AS earliest_install_date
FROM
mdm_apple_configuration_profiles
GROUP BY checksum
) cs ON macp.checksum = cs.checksum
JOIN mdm_configuration_profile_labels mcpl
ON mcpl.apple_profile_uuid = macp.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 = ?
WHERE
macp.team_id = ?
GROUP BY
profile_uuid, identifier
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.
SELECT
macp.profile_uuid AS profile_uuid,
macp.identifier AS identifier,
COUNT(*) AS count_profile_labels,
COUNT(mcpl.label_id) AS count_non_broken_labels,
COUNT(lm.label_id) AS count_host_labels,
min(earliest_install_date) AS earliest_install_date
FROM
mdm_apple_configuration_profiles macp
JOIN (
SELECT
checksum,
min(uploaded_at) AS earliest_install_date
FROM
mdm_apple_configuration_profiles
GROUP BY checksum
) cs ON macp.checksum = cs.checksum
JOIN mdm_configuration_profile_labels mcpl
ON mcpl.apple_profile_uuid = macp.profile_uuid AND mcpl.exclude = 1
LEFT OUTER JOIN label_membership lm
ON lm.label_id = mcpl.label_id AND lm.host_id = ?
WHERE
macp.team_id = ?
GROUP BY
profile_uuid, identifier
HAVING
-- considers only the profiles with labels, without any broken label, and with the host not in any label
count_profile_labels > 0 AND
count_profile_labels = count_non_broken_labels AND
count_host_labels = 0
UNION
-- label-based profiles where the host is a member of at least one of the labels (include-any)
SELECT
macp.profile_uuid AS profile_uuid,
macp.identifier AS identifier,
COUNT(*) AS count_profile_labels,
COUNT(mcpl.label_id) AS count_non_broken_labels,
COUNT(lm.label_id) AS count_host_labels,
min(earliest_install_date) AS earliest_install_date
FROM
mdm_apple_configuration_profiles macp
JOIN (
SELECT
checksum,
min(uploaded_at) AS earliest_install_date
FROM
mdm_apple_configuration_profiles
GROUP BY checksum
) cs ON macp.checksum = cs.checksum
JOIN mdm_configuration_profile_labels mcpl
ON mcpl.apple_profile_uuid = macp.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 = ?
WHERE
macp.team_id = ?
GROUP BY
profile_uuid, identifier
HAVING
count_profile_labels > 0 AND
count_host_labels > 0
`
var rows []*fleet.ExpectedMDMProfile
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID, host.ID, teamID, host.ID, teamID, host.ID, teamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host in team %d", teamID))
}
// Fetch variables_updated_at for host profiles that have it set and override
// earliest_install_date if it's older than variables_updated_at.
variableUpdateTimes := []struct {
ProfileUUID string `db:"profile_uuid"`
VariablesUpdatedAt time.Time `db:"variables_updated_at"`
}{}
variableUpdateTimesStmt := `
SELECT profile_uuid, variables_updated_at AS variables_updated_at
FROM host_mdm_apple_profiles
WHERE host_uuid = ? AND variables_updated_at IS NOT NULL
`
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &variableUpdateTimes, variableUpdateTimesStmt, host.UUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host in team %d", teamID))
}
variableUpdateTimesByProfileUUID := make(map[string]time.Time, len(variableUpdateTimes))
for _, r := range variableUpdateTimes {
variableUpdateTimesByProfileUUID[r.ProfileUUID] = r.VariablesUpdatedAt
}
expectedProfilesByIdentifier := make(map[string]*fleet.ExpectedMDMProfile, len(rows))
for _, r := range rows {
if variableUpdateTime, ok := variableUpdateTimesByProfileUUID[r.ProfileUUID]; ok && variableUpdateTime.After(r.EarliestInstallDate) {
r.EarliestInstallDate = variableUpdateTime
}
expectedProfilesByIdentifier[r.Identifier] = r
}
return expectedProfilesByIdentifier, nil
}
func (ds *Datastore) GetHostMDMProfilesRetryCounts(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error) {
const darwinStmt = `
SELECT
profile_identifier,
retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?`
const windowsStmt = `
SELECT
profile_name,
retries
FROM
host_mdm_windows_profiles hmwp
WHERE
hmwp.host_uuid = ?`
var stmt string
switch host.Platform {
case "darwin", "ios", "ipados":
stmt = darwinStmt
case "windows":
stmt = windowsStmt
default:
return nil, fmt.Errorf("unsupported platform %s", host.Platform)
}
var dest []fleet.HostMDMProfileRetryCount
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, host.UUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry counts for host %s", host.UUID))
}
return dest, nil
}
func (ds *Datastore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
const darwinStmt = `
SELECT
profile_identifier, retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?
AND hmap.command_uuid = ?`
const windowsStmt = `
SELECT
profile_uuid, retries
FROM
host_mdm_windows_profiles hmwp
WHERE
hmwp.host_uuid = ?
AND hmwp.command_uuid = ?`
var stmt string
switch host.Platform {
case "darwin", "ios", "ipados":
stmt = darwinStmt
case "windows":
stmt = windowsStmt
default:
return fleet.HostMDMProfileRetryCount{}, fmt.Errorf("unsupported platform %s", host.Platform)
}
var dest fleet.HostMDMProfileRetryCount
if err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, host.UUID, cmdUUID); err != nil {
if err == sql.ErrNoRows {
return dest, notFound("HostMDMCommand").WithMessage(fmt.Sprintf("command uuid %s not found for host uuid %s", cmdUUID, host.UUID))
}
return dest, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry count for host %s command uuid %s", host.UUID, cmdUUID))
}
return dest, nil
}
func batchSetProfileLabelAssociationsDB(
ctx context.Context,
tx sqlx.ExtContext,
profileLabels []fleet.ConfigurationProfileLabel,
profileUUIDsWithoutLabels []string,
platform string,
) (updatedDB bool, err error) {
if len(profileLabels)+len(profileUUIDsWithoutLabels) == 0 {
return false, nil
}
var platformPrefix string
switch platform {
case "darwin":
// map "darwin" to "apple" to be consistent with other
// "platform-agnostic" datastore methods. We initially used "darwin"
// because that's what hosts use (as the data is reported by osquery)
// and sometimes we want to dynamically select a table based on host
// data.
platformPrefix = "apple"
case "windows":
platformPrefix = "windows"
default:
return false, fmt.Errorf("unsupported platform %s", platform)
}
// delete any profile+label tuple that is NOT in the list of provided tuples
// but are associated with the provided profiles (so we don't delete
// unrelated profile+label tuples)
deleteStmt := `
DELETE FROM mdm_configuration_profile_labels
WHERE (%s_profile_uuid, label_id) NOT IN (%s) AND
%s_profile_uuid IN (?)
`
// used when only profileUUIDsWithoutLabels is provided, there are no
// labels to keep, delete all labels for profiles in this list.
deleteNoLabelStmt := `
DELETE FROM mdm_configuration_profile_labels
WHERE %s_profile_uuid IN (?)
`
upsertStmt := `
INSERT INTO mdm_configuration_profile_labels
(%s_profile_uuid, label_id, label_name, exclude, require_all)
VALUES
%s
ON DUPLICATE KEY UPDATE
label_id = VALUES(label_id),
exclude = VALUES(exclude),
require_all = VALUES(require_all)
`
selectStmt := `
SELECT %s_profile_uuid as profile_uuid, label_id, label_name, exclude, require_all FROM mdm_configuration_profile_labels
WHERE (%s_profile_uuid, label_name) IN (%s)
`
if len(profileLabels) == 0 {
deleteNoLabelStmt = fmt.Sprintf(deleteNoLabelStmt, platformPrefix)
deleteNoLabelStmt, args, err := sqlx.In(deleteNoLabelStmt, profileUUIDsWithoutLabels)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "sqlx.In delete labels for profiles without labels")
}
var result sql.Result
if result, err = tx.ExecContext(ctx, deleteNoLabelStmt, args...); err != nil {
return false, ctxerr.Wrap(ctx, err, "deleting labels for profiles without labels")
}
if result != nil {
rows, _ := result.RowsAffected()
updatedDB = rows > 0
}
return updatedDB, nil
}
var (
insertBuilder strings.Builder
selectOrDeleteBuilder strings.Builder
selectParams []any
insertParams []any
deleteParams []any
setProfileUUIDs = make(map[string]struct{})
)
labelsToInsert := make(map[string]*fleet.ConfigurationProfileLabel, len(profileLabels))
for i, pl := range profileLabels {
labelsToInsert[fmt.Sprintf("%s\n%s", pl.ProfileUUID, pl.LabelName)] = &profileLabels[i]
if i > 0 {
insertBuilder.WriteString(",")
selectOrDeleteBuilder.WriteString(",")
}
insertBuilder.WriteString("(?, ?, ?, ?, ?)")
selectOrDeleteBuilder.WriteString("(?, ?)")
selectParams = append(selectParams, pl.ProfileUUID, pl.LabelName)
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude, pl.RequireAll)
deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID)
setProfileUUIDs[pl.ProfileUUID] = struct{}{}
}
// Determine if we need to update the database
var existingProfileLabels []fleet.ConfigurationProfileLabel
err = sqlx.SelectContext(ctx, tx, &existingProfileLabels,
fmt.Sprintf(selectStmt, platformPrefix, platformPrefix, selectOrDeleteBuilder.String()), selectParams...)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "selecting existing profile labels")
}
updateNeeded := false
if len(existingProfileLabels) == len(labelsToInsert) {
for _, existing := range existingProfileLabels {
toInsert, ok := labelsToInsert[fmt.Sprintf("%s\n%s", existing.ProfileUUID, existing.LabelName)]
// The fleet.ConfigurationProfileLabel struct has no pointers, so we can use standard cmp.Equal
if !ok || !cmp.Equal(existing, *toInsert) {
updateNeeded = true
break
}
}
} else {
updateNeeded = true
}
if updateNeeded {
_, err := tx.ExecContext(ctx, fmt.Sprintf(upsertStmt, platformPrefix, insertBuilder.String()), insertParams...)
if err != nil {
if isChildForeignKeyError(err) {
// one of the provided labels doesn't exist
return false, foreignKey("mdm_configuration_profile_labels", fmt.Sprintf("(profile, label)=(%v)", insertParams))
}
return false, ctxerr.Wrap(ctx, err, "setting label associations for profile")
}
updatedDB = true
}
deleteStmt = fmt.Sprintf(deleteStmt, platformPrefix, selectOrDeleteBuilder.String(), platformPrefix)
profUUIDs := make([]string, 0, len(setProfileUUIDs)+len(profileUUIDsWithoutLabels))
for k := range setProfileUUIDs {
profUUIDs = append(profUUIDs, k)
}
profUUIDs = append(profUUIDs, profileUUIDsWithoutLabels...)
deleteArgs := deleteParams
deleteArgs = append(deleteArgs, profUUIDs)
deleteStmt, args, err := sqlx.In(deleteStmt, deleteArgs...)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "sqlx.In delete labels for profiles")
}
var result sql.Result
if result, err = tx.ExecContext(ctx, deleteStmt, args...); err != nil {
return false, ctxerr.Wrap(ctx, err, "deleting labels for profiles")
}
if result != nil {
rows, _ := result.RowsAffected()
updatedDB = updatedDB || rows > 0
}
return updatedDB, nil
}
func (ds *Datastore) MDMGetEULAMetadata(ctx context.Context) (*fleet.MDMEULA, error) {
// Currently, there can only be one EULA in the database, and we're
// hardcoding it's id to be 1 in order to enforce this restriction.
stmt := "SELECT name, created_at, token, sha256 FROM eulas WHERE id = 1"
var eula fleet.MDMEULA
if err := sqlx.GetContext(ctx, ds.reader(ctx), &eula, stmt); err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMEULA"))
}
return nil, ctxerr.Wrap(ctx, err, "get EULA metadata")
}
return &eula, nil
}
func (ds *Datastore) MDMGetEULABytes(ctx context.Context, token string) (*fleet.MDMEULA, error) {
stmt := "SELECT name, bytes FROM eulas WHERE token = ?"
var eula fleet.MDMEULA
if err := sqlx.GetContext(ctx, ds.reader(ctx), &eula, stmt, token); err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMEULA"))
}
return nil, ctxerr.Wrap(ctx, err, "get EULA bytes")
}
return &eula, nil
}
func (ds *Datastore) MDMInsertEULA(ctx context.Context, eula *fleet.MDMEULA) error {
// We're intentionally hardcoding the id to be 1 because we only want to
// allow one EULA.
stmt := `
INSERT INTO eulas (id, name, bytes, token, sha256)
VALUES (1, ?, ?, ?, ?)
`
_, err := ds.writer(ctx).ExecContext(ctx, stmt, eula.Name, eula.Bytes, eula.Token, eula.Sha256)
if err != nil {
if IsDuplicate(err) {
return ctxerr.Wrap(ctx, alreadyExists("MDMEULA", eula.Token))
}
return ctxerr.Wrap(ctx, err, "create EULA")
}
return nil
}
func (ds *Datastore) MDMDeleteEULA(ctx context.Context, token string) error {
stmt := "DELETE FROM eulas WHERE token = ?"
res, err := ds.writer(ctx).ExecContext(ctx, stmt, token)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete EULA")
}
deleted, _ := res.RowsAffected()
if deleted != 1 {
return ctxerr.Wrap(ctx, notFound("MDMEULA"))
}
return nil
}
func (ds *Datastore) GetHostCertAssociationsToExpire(ctx context.Context, expiryDays, limit int) ([]fleet.SCEPIdentityAssociation, error) {
// TODO(roberto): this is not good because we don't have any indexes on
// h.uuid, due to time constraints, I'm assuming that this
// function is called with a relatively low amount of shas
//
// Note that we use GROUP BY because we can't guarantee unique entries
// based on uuid in the hosts table.
stmt, args, err := sqlx.In(`
SELECT
h.uuid AS host_uuid,
ncaa.sha256 AS sha256,
COALESCE(MAX(hm.fleet_enroll_ref), '') AS enroll_reference,
ne.enrolled_from_migration,
ne.type
FROM (
-- grab only the latest certificate associated with this device
SELECT
n1.id,
n1.sha256,
n1.cert_not_valid_after,
n1.renew_command_uuid
FROM
nano_cert_auth_associations n1
WHERE
n1.sha256 = (
SELECT
n2.sha256
FROM
nano_cert_auth_associations n2
WHERE
n1.id = n2.id
ORDER BY
n2.created_at DESC,
n2.sha256 ASC
LIMIT 1
)
) ncaa
JOIN
hosts h ON h.uuid = ncaa.id
LEFT JOIN
host_mdm hm ON hm.host_id = h.id
LEFT JOIN
nano_enrollments ne ON ne.id = ncaa.id
WHERE
ncaa.cert_not_valid_after BETWEEN '0000-00-00' AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
AND ncaa.renew_command_uuid IS NULL
AND ne.enabled = 1
GROUP BY
host_uuid, ncaa.sha256, ncaa.cert_not_valid_after
ORDER BY
cert_not_valid_after ASC
LIMIT ?`, expiryDays, limit)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building sqlx.In query")
}
var uuids []fleet.SCEPIdentityAssociation
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &uuids, stmt, args...); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, ctxerr.Wrap(ctx, err, "get identity certs close to expiry")
}
return uuids, nil
}
func (ds *Datastore) SetCommandForPendingSCEPRenewal(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
if len(assocs) == 0 {
return nil
}
var sb strings.Builder
args := make([]any, len(assocs)*3)
for i, assoc := range assocs {
sb.WriteString("(?, ?, ?),")
args[i*3] = assoc.HostUUID
args[i*3+1] = assoc.SHA256
args[i*3+2] = cmdUUID
}
stmt := fmt.Sprintf(`
INSERT INTO nano_cert_auth_associations (id, sha256, renew_command_uuid) VALUES %s
ON DUPLICATE KEY UPDATE
renew_command_uuid = VALUES(renew_command_uuid)
`, strings.TrimSuffix(sb.String(), ","))
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
res, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return fmt.Errorf("failed to update cert associations: %w", err)
}
// NOTE: we can't use insertOnDuplicateDidInsert because the
// LastInsertId check only works tables that have an
// auto-incrementing primary key. See notes in that function
// and insertOnDuplicateDidUpdate to understand the mechanism.
affected, _ := res.RowsAffected()
if affected == 1 {
return errors.New("this function can only be used to update existing associations")
}
return nil
})
}
func (ds *Datastore) CleanSCEPRenewRefs(ctx context.Context, hostUUID string) error {
stmt := `
UPDATE nano_cert_auth_associations
SET renew_command_uuid = NULL
WHERE id = ?`
res, err := ds.writer(ctx).ExecContext(ctx, stmt, hostUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "cleaning SCEP renew references")
}
if rows, _ := res.RowsAffected(); rows == 0 {
return ctxerr.Errorf(ctx, "nano association for host.uuid %s doesn't exist", hostUUID)
}
return nil
}
func (ds *Datastore) GetHostMDMProfileInstallStatus(ctx context.Context, hostUUID string, profUUID string) (fleet.MDMDeliveryStatus, error) {
table, column, err := getTableAndColumnNameForHostMDMProfileUUID(profUUID)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "getting table and column")
}
selectStmt := fmt.Sprintf(`
SELECT
COALESCE(status, ?) as status
FROM
%s
WHERE
operation_type = ?
AND host_uuid = ?
AND %s = ?
`, table, column)
var status fleet.MDMDeliveryStatus
if err := sqlx.GetContext(ctx, ds.writer(ctx), &status, selectStmt, fleet.MDMDeliveryPending, fleet.MDMOperationTypeInstall, hostUUID, profUUID); err != nil {
if err == sql.ErrNoRows {
return "", notFound("HostMDMProfile").WithMessage("unable to match profile to host")
}
return "", ctxerr.Wrap(ctx, err, "get MDM profile status")
}
return status, nil
}
func (ds *Datastore) ResendHostMDMProfile(ctx context.Context, hostUUID string, profUUID string) error {
table, column, err := getTableAndColumnNameForHostMDMProfileUUID(profUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting table and column")
}
// update the status to NULL to trigger resending on the next cron run
updateStmt := fmt.Sprintf(`UPDATE %s SET status = NULL WHERE host_uuid = ? AND %s = ?`, table, column)
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
res, err := tx.ExecContext(ctx, updateStmt, hostUUID, profUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "resending host MDM profile")
}
if rows, _ := res.RowsAffected(); rows == 0 {
// this should never happen, log for debugging
level.Debug(ds.logger).Log("msg", "resend profile status not updated", "host_uuid", hostUUID, "profile_uuid", profUUID)
}
return nil
})
}
func getTableAndColumnNameForHostMDMProfileUUID(profUUID string) (table, column string, err error) {
switch {
case strings.HasPrefix(profUUID, fleet.MDMAppleDeclarationUUIDPrefix):
return "host_mdm_apple_declarations", "declaration_uuid", nil
case strings.HasPrefix(profUUID, fleet.MDMAppleProfileUUIDPrefix):
return "host_mdm_apple_profiles", "profile_uuid", nil
case strings.HasPrefix(profUUID, fleet.MDMWindowsProfileUUIDPrefix):
return "host_mdm_windows_profiles", "profile_uuid", nil
default:
return "", "", fmt.Errorf("invalid profile UUID prefix %s", profUUID)
}
}
func (ds *Datastore) AreHostsConnectedToFleetMDM(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) {
var (
appleUUIDs []any
winUUIDs []any
)
res := make(map[string]bool, len(hosts))
for _, h := range hosts {
switch h.Platform {
case "darwin", "ipados", "ios":
appleUUIDs = append(appleUUIDs, h.UUID)
case "windows":
winUUIDs = append(winUUIDs, h.UUID)
}
res[h.UUID] = false
}
setConnectedUUIDs := func(stmt string, uuids []any, mp map[string]bool) error {
var res []string
if len(uuids) > 0 {
stmt, args, err := sqlx.In(stmt, uuids)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sqlx.In statement")
}
err = sqlx.SelectContext(ctx, ds.reader(ctx), &res, stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving hosts connected to fleet")
}
}
for _, uuid := range res {
mp[uuid] = true
}
return nil
}
// NOTE: if you change any of the conditions in this query, please
// update the `hostMDMSelect` constant too, which has a
// `connected_to_fleet` condition, any relevant filters, and the
// query used in isAppleHostConnectedToFleetMDM.
const appleStmt = `
SELECT ne.id
FROM nano_enrollments ne
JOIN hosts h ON h.uuid = ne.id
JOIN host_mdm hm ON hm.host_id = h.id
WHERE ne.id IN (?)
AND ne.enabled = 1
AND ne.type IN ('Device', 'User Enrollment (Device)')
AND hm.enrolled = 1
`
if err := setConnectedUUIDs(appleStmt, appleUUIDs, res); err != nil {
return nil, err
}
// NOTE: if you change any of the conditions in this query, please
// update the `hostMDMSelect` constant too, which has a
// `connected_to_fleet` condition, and any relevant filters, and the
// query used in isWindowsHostConnectedToFleetMDM.
const winStmt = `
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 mwe.host_uuid IN (?)
AND mwe.device_state = '` + microsoft_mdm.MDMDeviceStateEnrolled + `'
AND hm.enrolled = 1
`
if err := setConnectedUUIDs(winStmt, winUUIDs, res); err != nil {
return nil, err
}
return res, nil
}
func (ds *Datastore) IsHostConnectedToFleetMDM(ctx context.Context, host *fleet.Host) (bool, error) {
if host.Platform == "windows" {
return isWindowsHostConnectedToFleetMDM(ctx, ds.reader(ctx), host)
} else if host.Platform == "darwin" || host.Platform == "ipados" || host.Platform == "ios" {
return isAppleHostConnectedToFleetMDM(ctx, ds.reader(ctx), host)
}
return false, nil
}
func batchSetProfileVariableAssociationsDB(
ctx context.Context,
tx sqlx.ExtContext,
profileVariablesByUUID []fleet.MDMProfileUUIDFleetVariables,
platform string,
) error {
if len(profileVariablesByUUID) == 0 {
return nil
}
var platformPrefix string
switch platform {
case "darwin":
platformPrefix = "apple"
case "windows":
platformPrefix = "windows"
default:
return fmt.Errorf("unsupported platform %s", platform)
}
// collect the profile uuids to clear
profileUUIDsToDelete := make([]string, 0, len(profileVariablesByUUID))
// small optimization - if there are no variables to insert, we can stop here
var varsToSet bool
for _, profVars := range profileVariablesByUUID {
profileUUIDsToDelete = append(profileUUIDsToDelete, profVars.ProfileUUID)
if len(profVars.FleetVariables) > 0 {
varsToSet = true
}
}
// delete variables associated with those profiles
clearVarsForProfilesStmt := fmt.Sprintf(`DELETE FROM mdm_configuration_profile_variables WHERE %s_profile_uuid IN (?)`, platformPrefix)
clearVarsForProfilesStmt, args, err := sqlx.In(clearVarsForProfilesStmt, profileUUIDsToDelete)
if err != nil {
return ctxerr.Wrap(ctx, err, "sqlx.In delete variables for profiles")
}
if _, err := tx.ExecContext(ctx, clearVarsForProfilesStmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "deleting variables for profiles")
}
if !varsToSet {
return nil
}
// load fleet variables to map them to their IDs
type varDef struct {
ID uint `db:"id"`
Name string `db:"name"`
IsPrefix bool `db:"is_prefix"`
}
var varDefs []varDef
const varsStmt = `SELECT id, name, is_prefix FROM fleet_variables`
if err := sqlx.SelectContext(ctx, tx, &varDefs, varsStmt); err != nil {
return fmt.Errorf("failed to load fleet variables: %w", err)
}
// map the variables to their IDs (this looks terrible with the nested fors
// but those are all very small "n" - single-digit vars by profile and same
// in varDefs, so this is more efficient than building lookup maps).
type profVarTuple struct {
ProfileUUID string
VarID uint
}
profVars := make([]profVarTuple, 0, len(profileVariablesByUUID))
for _, pv := range profileVariablesByUUID {
for _, v := range pv.FleetVariables {
// variables received here do not have the FLEET_VAR_ prefix, but variables
// in the fleet_variables table do.
varWithPrefix := "FLEET_VAR_" + string(v)
for _, def := range varDefs {
if !def.IsPrefix && def.Name == varWithPrefix {
profVars = append(profVars, profVarTuple{pv.ProfileUUID, def.ID})
break
}
if def.IsPrefix && strings.HasPrefix(varWithPrefix, def.Name) {
profVars = append(profVars, profVarTuple{pv.ProfileUUID, def.ID})
break
}
}
}
}
const batchSize = 1000 // number of parameters is this times number of placeholders
generateValueArgs := func(p profVarTuple) (string, []any) {
valuePart := "(?, ?),"
args := []any{p.ProfileUUID, p.VarID}
return valuePart, args
}
executeUpsertBatch := func(valuePart string, args []any) error {
stmt := fmt.Sprintf(`
INSERT INTO mdm_configuration_profile_variables (
%s_profile_uuid,
fleet_variable_id
)
VALUES %s
ON DUPLICATE KEY UPDATE
fleet_variable_id = VALUES(fleet_variable_id)
`, platformPrefix, strings.TrimSuffix(valuePart, ","))
_, err := tx.ExecContext(ctx, stmt, args...)
return err
}
err = batchProcessDB(profVars, batchSize, generateValueArgs, executeUpsertBatch)
if err != nil {
return ctxerr.Wrap(ctx, err, "upserting profile variables")
}
return nil
}
func (ds *Datastore) BatchResendMDMProfileToHosts(ctx context.Context, profileUUID string, filters fleet.BatchResendMDMProfileFilters) (int64, error) {
table, column, err := getTableAndColumnNameForHostMDMProfileUUID(profileUUID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "getting table and column")
}
// update the status to NULL to trigger resending on the next cron run
updateStmt := fmt.Sprintf(`UPDATE %s SET status = NULL WHERE %s = ? AND status = ?`, table, column)
var count int64
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
res, err := tx.ExecContext(ctx, updateStmt, profileUUID, filters.ProfileStatus)
if err != nil {
return ctxerr.Wrap(ctx, err, "resending MDM profile on hosts")
}
count, _ = res.RowsAffected()
return nil
})
return count, err
}
func (ds *Datastore) GetMDMConfigProfileStatus(ctx context.Context, profileUUID string) (fleet.MDMConfigProfileStatus, error) {
switch {
case strings.HasPrefix(profileUUID, fleet.MDMAppleProfileUUIDPrefix):
return ds.getAppleMDMConfigProfileStatus(ctx, profileUUID)
case strings.HasPrefix(profileUUID, fleet.MDMAppleDeclarationUUIDPrefix):
return ds.getAppleMDMDeclarationStatus(ctx, profileUUID)
case strings.HasPrefix(profileUUID, fleet.MDMWindowsProfileUUIDPrefix):
return ds.getWindowsMDMConfigProfileStatus(ctx, profileUUID)
default:
return fleet.MDMConfigProfileStatus{}, ctxerr.Wrap(ctx, notFound("ConfigurationProfile").WithName(profileUUID))
}
}
func (ds *Datastore) getWindowsMDMConfigProfileStatus(ctx context.Context, profileUUID string) (fleet.MDMConfigProfileStatus, error) {
var counts fleet.MDMConfigProfileStatus
stmt := `
SELECT
CASE
WHEN hmwp.status = :status_failed THEN
'failed'
WHEN COALESCE(hmwp.status, :status_pending) = :status_pending THEN
'pending'
WHEN hmwp.status = :status_verifying THEN
'verifying'
WHEN hmwp.status = :status_verified 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
JOIN host_mdm_windows_profiles hmwp ON hmwp.host_uuid = h.uuid
WHERE
mwe.device_state = :device_state_enrolled AND
h.platform = 'windows' AND
hmdm.is_server = 0 AND
hmdm.enrolled = 1 AND
hmwp.profile_uuid = :profile_uuid
GROUP BY
final_status`
stmt, args, err := sqlx.Named(stmt, map[string]any{
"status_failed": fleet.MDMDeliveryFailed,
"status_pending": fleet.MDMDeliveryPending,
"status_verifying": fleet.MDMDeliveryVerifying,
"status_verified": fleet.MDMDeliveryVerified,
"device_state_enrolled": microsoft_mdm.MDMDeviceStateEnrolled,
"profile_uuid": profileUUID,
})
if err != nil {
return counts, ctxerr.Wrap(ctx, err, "prepare arguments with sqlx.Named")
}
var rows []statusCounts
err = sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, args...)
if err != nil {
return counts, err
}
// Note that hosts with "BitLocker action required" are counted as pending.
for _, row := range rows {
switch row.Status {
case "failed":
counts.Failed = row.Count
case "pending":
counts.Pending += row.Count
case "verifying":
counts.Verifying = row.Count
case "verified":
counts.Verified = row.Count
case "":
level.Debug(ds.logger).Log("msg", fmt.Sprintf("counted %d windows hosts for profile %s with mdm turned on but no profiles", row.Count, profileUUID))
default:
return counts, ctxerr.New(ctx, fmt.Sprintf("unexpected mdm windows status count: status=%s, count=%d", row.Status, row.Count))
}
}
return counts, nil
}
func (ds *Datastore) getAppleMDMConfigProfileStatus(ctx context.Context, profileUUID string) (fleet.MDMConfigProfileStatus, error) {
var counts fleet.MDMConfigProfileStatus
// NOTE: the case computation of the status must follow the same logic as in
// sqlJoinMDMAppleProfilesStatus (for non-file-vault, since this is for
// custom settings).
stmt := `
SELECT
COUNT(id) AS count,
CASE
WHEN hmap.status = :status_failed THEN
'failed'
WHEN COALESCE(hmap.status, :status_pending) = :status_pending THEN
'pending'
WHEN hmap.status = :status_verifying THEN
'verifying'
WHEN hmap.status = :status_verified THEN
'verified'
END AS final_status
FROM
hosts h
JOIN host_mdm_apple_profiles hmap ON h.uuid = hmap.host_uuid
WHERE
platform IN ('darwin', 'ios', 'ipados') AND
hmap.profile_uuid = :profile_uuid AND
( hmap.status NOT IN (:status_verified, :status_verifying) OR hmap.operation_type = :operation_install )
GROUP BY
final_status HAVING final_status IS NOT NULL`
stmt, args, err := sqlx.Named(stmt, map[string]any{
"status_failed": fleet.MDMDeliveryFailed,
"status_pending": fleet.MDMDeliveryPending,
"status_verifying": fleet.MDMDeliveryVerifying,
"status_verified": fleet.MDMDeliveryVerified,
"operation_install": fleet.MDMOperationTypeInstall,
"profile_uuid": profileUUID,
})
if err != nil {
return counts, ctxerr.Wrap(ctx, err, "prepare arguments with sqlx.Named")
}
var dest []struct {
Count uint `db:"count"`
Status string `db:"final_status"`
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, args...); err != nil {
return counts, err
}
byStatus := make(map[string]uint)
for _, s := range dest {
if _, ok := byStatus[s.Status]; ok {
return counts, fmt.Errorf("duplicate status %s", s.Status)
}
byStatus[s.Status] = s.Count
}
for s, c := range byStatus {
switch fleet.MDMDeliveryStatus(s) {
case fleet.MDMDeliveryFailed:
counts.Failed = c
case fleet.MDMDeliveryPending:
counts.Pending = c
case fleet.MDMDeliveryVerifying:
counts.Verifying = c
case fleet.MDMDeliveryVerified:
counts.Verified = c
default:
return counts, fmt.Errorf("unknown status %s", s)
}
}
return counts, nil
}
func (ds *Datastore) getAppleMDMDeclarationStatus(ctx context.Context, declUUID string) (fleet.MDMConfigProfileStatus, error) {
var counts fleet.MDMConfigProfileStatus
// NOTE: the case computation of the status must follow the same logic as in
// sqlJoinMDMAppleDeclarationsStatus.
stmt := `
SELECT
COUNT(id) AS count,
CASE
WHEN hmad.status = :status_failed THEN
'failed'
WHEN COALESCE(hmad.status, :status_pending) = :status_pending THEN
'pending'
WHEN hmad.status = :status_verifying THEN
'verifying'
WHEN hmad.status = :status_verified THEN
'verified'
END AS final_status
FROM
hosts h
JOIN host_mdm_apple_declarations hmad ON h.uuid = hmad.host_uuid
WHERE
h.platform IN ('darwin', 'ios', 'ipados') AND
( hmad.status NOT IN (:status_verified, :status_verifying) OR hmad.operation_type = :operation_install ) AND
hmad.declaration_uuid = :declaration_uuid
GROUP BY
final_status HAVING final_status IS NOT NULL`
stmt, args, err := sqlx.Named(stmt, map[string]any{
"status_failed": fleet.MDMDeliveryFailed,
"status_pending": fleet.MDMDeliveryPending,
"status_verifying": fleet.MDMDeliveryVerifying,
"status_verified": fleet.MDMDeliveryVerified,
"operation_install": fleet.MDMOperationTypeInstall,
"declaration_uuid": declUUID,
})
if err != nil {
return counts, ctxerr.Wrap(ctx, err, "prepare arguments with sqlx.Named")
}
var dest []struct {
Count uint `db:"count"`
Status string `db:"final_status"`
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, args...); err != nil {
return counts, err
}
byStatus := make(map[string]uint)
for _, s := range dest {
if _, ok := byStatus[s.Status]; ok {
return counts, fmt.Errorf("duplicate status %s", s.Status)
}
byStatus[s.Status] = s.Count
}
for s, c := range byStatus {
switch fleet.MDMDeliveryStatus(s) {
case fleet.MDMDeliveryFailed:
counts.Failed = c
case fleet.MDMDeliveryPending:
counts.Pending = c
case fleet.MDMDeliveryVerifying:
counts.Verifying = c
case fleet.MDMDeliveryVerified:
counts.Verified = c
default:
return counts, fmt.Errorf("unknown status %s", s)
}
}
return counts, nil
}
func (ds *Datastore) IsHostPendingVPPInstallVerification(ctx context.Context, hostUUID string) (bool, error) {
stmt := `
SELECT EXISTS (
SELECT 1
FROM host_mdm_commands hmc
JOIN hosts h ON hmc.host_id = h.id
WHERE
h.uuid = ? AND
hmc.command_type = ?
) AS exists_flag
`
var exists bool
if err := sqlx.GetContext(ctx, ds.reader(ctx), &exists, stmt, hostUUID, fleet.VerifySoftwareInstallVPPPrefix); err != nil {
return false, ctxerr.Wrap(ctx, err, "check for acknowledged mdm command by host")
}
return exists, nil
}