mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 07:58:31 +00:00
2586 lines
83 KiB
Go
2586 lines
83 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"
|
|
"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, "combined_commands")
|
|
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, androidProfiles []*fleet.MDMAndroidConfigProfile, 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")
|
|
}
|
|
|
|
if updates.AndroidConfigProfile, err = ds.batchSetMDMAndroidProfiles(ctx, tx, tmID, androidProfiles); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "batch set android profiles")
|
|
}
|
|
|
|
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 (?)
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
profile_uuid,
|
|
team_id,
|
|
name,
|
|
'' AS scope,
|
|
'android' AS platform,
|
|
'' AS identifier,
|
|
'' AS checksum,
|
|
created_at,
|
|
uploaded_at
|
|
FROM mdm_android_configuration_profiles
|
|
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, 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, androidProfUUIDs, macDeclUUIDs []string
|
|
for _, prof := range profs {
|
|
switch prof.Platform {
|
|
case "windows":
|
|
winProfUUIDs = append(winProfUUIDs, prof.ProfileUUID)
|
|
case "android":
|
|
androidProfUUIDs = append(androidProfUUIDs, prof.ProfileUUID)
|
|
default:
|
|
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, androidProfUUIDs, 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, androidProfUUIDs, macDeclUUIDs []string) ([]fleet.ConfigurationProfileLabel, error) {
|
|
// load the labels associated with those profiles
|
|
const labelsStmt = `
|
|
SELECT
|
|
COALESCE(apple_profile_uuid, windows_profile_uuid, android_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 (?) OR
|
|
mcpl.android_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(androidProfUUIDs) == 0 {
|
|
androidProfUUIDs = []string{"-"}
|
|
}
|
|
if len(macDeclUUIDs) == 0 {
|
|
macDeclUUIDs = []string{"-"}
|
|
}
|
|
|
|
stmt, args, err := sqlx.In(labelsStmt, macProfUUIDs, winProfUUIDs, androidProfUUIDs, 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
|
|
androidProfUUIDs []string
|
|
hasAppleDecls bool
|
|
)
|
|
|
|
if len(hostIDs) > 0 {
|
|
countArgs++
|
|
}
|
|
if len(teamIDs) > 0 {
|
|
countArgs++
|
|
}
|
|
if len(profileUUIDs) > 0 {
|
|
countArgs++
|
|
|
|
// split into mac, win and android 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 if strings.HasPrefix(puid, fleet.MDMAndroidProfileUUIDPrefix) {
|
|
androidProfUUIDs = append(androidProfUUIDs, puid)
|
|
} 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 len(androidProfUUIDs) > 0 {
|
|
countProfUUIDs++
|
|
}
|
|
if hasAppleDecls {
|
|
countProfUUIDs++
|
|
}
|
|
if countProfUUIDs > 1 {
|
|
return updates, errors.New("profile uuids must be all Apple profiles, all Apple declarations, all Windows profiles, or all Android 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)
|
|
|
|
case len(androidProfUUIDs) > 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_android_configuration_profiles maap
|
|
ON h.team_id = maap.team_id OR (h.team_id IS NULL AND maap.team_id = 0)
|
|
LEFT JOIN host_mdm_android_profiles hmap
|
|
ON h.uuid = hmap.host_uuid
|
|
WHERE
|
|
maap.profile_uuid IN (?) AND h.platform = 'android'
|
|
OR
|
|
hmap.profile_uuid IN (?) AND h.platform = 'android'`
|
|
args = append(args, androidProfUUIDs, androidProfUUIDs)
|
|
}
|
|
|
|
// 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
|
|
var androidHosts []string
|
|
for _, h := range hosts {
|
|
switch h.Platform {
|
|
case "darwin", "ios", "ipados":
|
|
appleHosts = append(appleHosts, h.UUID)
|
|
case "windows":
|
|
winHosts = append(winHosts, h.UUID)
|
|
case "android":
|
|
androidHosts = append(androidHosts, 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")
|
|
}
|
|
|
|
updates.AndroidConfigProfile, err = ds.bulkSetPendingMDMAndroidHostProfilesDB(ctx, androidHosts)
|
|
if err != nil {
|
|
return updates, ctxerr.Wrap(ctx, err, "bulk set pending android 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"
|
|
case "android":
|
|
platformPrefix = "android"
|
|
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
|
|
case strings.HasPrefix(profUUID, fleet.MDMAndroidProfileUUIDPrefix):
|
|
return "host_mdm_android_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,
|
|
) (didUpdate bool, err error) {
|
|
if len(profileVariablesByUUID) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
var platformPrefix string
|
|
switch platform {
|
|
case "darwin":
|
|
platformPrefix = "apple"
|
|
case "windows":
|
|
platformPrefix = "windows"
|
|
case "android":
|
|
return false, nil // Early return here, to avoid failing but still utilizing the shared batchSet method.
|
|
default:
|
|
return false, 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 false, ctxerr.Wrap(ctx, err, "sqlx.In delete variables for profiles")
|
|
}
|
|
var res sql.Result
|
|
if res, err = tx.ExecContext(ctx, clearVarsForProfilesStmt, args...); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "deleting variables for profiles")
|
|
}
|
|
rowsDeleted, err := res.RowsAffected()
|
|
if err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "rows affected for deleting variables for profiles")
|
|
}
|
|
|
|
if !varsToSet {
|
|
return rowsDeleted > 0, 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 false, 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 false, ctxerr.Wrap(ctx, err, "upserting profile variables")
|
|
}
|
|
return true, 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)
|
|
case strings.HasPrefix(profileUUID, fleet.MDMAndroidProfileUUIDPrefix):
|
|
return ds.getAndroidMDMConfigProfileStatus(ctx, profileUUID)
|
|
default:
|
|
return fleet.MDMConfigProfileStatus{}, ctxerr.Wrap(ctx, notFound("ConfigurationProfile").WithName(profileUUID))
|
|
}
|
|
}
|
|
|
|
func (ds *Datastore) getAndroidMDMConfigProfileStatus(ctx context.Context, profileUUID string) (fleet.MDMConfigProfileStatus, error) {
|
|
var counts fleet.MDMConfigProfileStatus
|
|
|
|
stmt := `
|
|
SELECT
|
|
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'
|
|
ELSE
|
|
''
|
|
END AS final_status,
|
|
SUM(1) AS count
|
|
FROM
|
|
hosts h
|
|
JOIN host_mdm hmdm ON h.id = hmdm.host_id
|
|
JOIN android_devices ad ON h.id=ad.host_id
|
|
JOIN host_mdm_android_profiles hmap ON hmap.host_uuid = h.uuid
|
|
WHERE
|
|
h.platform = 'android' AND
|
|
hmdm.enrolled = 1 AND
|
|
hmap.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,
|
|
"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
|
|
}
|
|
|
|
for _, row := range rows {
|
|
switch row.Status {
|
|
case string(fleet.MDMDeliveryFailed):
|
|
counts.Failed = row.Count
|
|
case string(fleet.MDMDeliveryPending):
|
|
counts.Pending += row.Count
|
|
case string(fleet.MDMDeliveryVerifying):
|
|
counts.Verifying = row.Count
|
|
case string(fleet.MDMDeliveryVerified):
|
|
counts.Verified = row.Count
|
|
case "":
|
|
level.Debug(ds.logger).Log("msg", fmt.Sprintf("counted %d android hosts for profile %s with mdm turned on but no profiles", row.Count, profileUUID))
|
|
default:
|
|
return counts, ctxerr.New(ctx, fmt.Sprintf("unexpected mdm android status count: status=%s, count=%d", row.Status, row.Count))
|
|
}
|
|
}
|
|
return counts, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type batchSetAssociationCurrentProfile struct {
|
|
ProfileUUID string `db:"profile_uuid"`
|
|
Name string `db:"name"`
|
|
}
|
|
|
|
type BatchSetAssociationIncomingProfile struct {
|
|
Name string // Identifier or name depending on Platform.
|
|
ProfileUUID string
|
|
LabelsIncludeAll []fleet.ConfigurationProfileLabel
|
|
LabelsIncludeAny []fleet.ConfigurationProfileLabel
|
|
LabelsExcludeAny []fleet.ConfigurationProfileLabel
|
|
}
|
|
|
|
// batchSetAssociations handles setting label and variable associations for all types of platform profiles.
|
|
func (ds *Datastore) batchSetLabelAndVariableAssociations(ctx context.Context, tx sqlx.ExtContext, platform string, teamID *uint, incomingProfiles []*BatchSetAssociationIncomingProfile, profilesVariablesByIdentifier []fleet.MDMProfileIdentifierFleetVariables) (updatedLabels bool, err error) {
|
|
if len(incomingProfiles) == 0 {
|
|
return false, nil // Do nothing on empty incoming profiles.
|
|
}
|
|
|
|
var currentProfilesQuery string
|
|
|
|
// Set platform specific variables here to use later.
|
|
switch platform {
|
|
case "darwin":
|
|
currentProfilesQuery = `SELECT profile_uuid, identifier as name FROM mdm_apple_configuration_profiles WHERE team_id = ? AND identifier IN (?)`
|
|
case "windows":
|
|
currentProfilesQuery = `SELECT profile_uuid, name FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name IN (?)`
|
|
case "android":
|
|
currentProfilesQuery = `SELECT profile_uuid, name FROM mdm_android_configuration_profiles WHERE team_id = ? AND name IN (?)`
|
|
default:
|
|
return false, ctxerr.Errorf(ctx, "unsupported platform %q", platform)
|
|
}
|
|
|
|
var profTeamID uint
|
|
if teamID != nil {
|
|
profTeamID = *teamID
|
|
}
|
|
|
|
var incomingNames []string
|
|
incomingProfilesMap := make(map[string]*BatchSetAssociationIncomingProfile)
|
|
for _, p := range incomingProfiles {
|
|
incomingNames = append(incomingNames, p.Name)
|
|
incomingProfilesMap[p.Name] = p
|
|
}
|
|
stmt, args, err := sqlx.In(currentProfilesQuery, profTeamID, incomingNames)
|
|
if err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "prepare current profiles query")
|
|
}
|
|
|
|
// A list of all new and updated profiles
|
|
var currentProfiles []*batchSetAssociationCurrentProfile
|
|
if err := sqlx.SelectContext(ctx, tx, ¤tProfiles, stmt, args...); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "select current profiles")
|
|
}
|
|
|
|
incomingLabels := []fleet.ConfigurationProfileLabel{}
|
|
var profsWithoutLabels []string
|
|
|
|
for _, currentProfile := range currentProfiles {
|
|
incomingProf, ok := incomingProfilesMap[currentProfile.Name]
|
|
if !ok {
|
|
return false, ctxerr.Errorf(ctx, "profile %q is in the database but was not incoming", currentProfile.Name)
|
|
}
|
|
|
|
var profHasLabel bool
|
|
for _, label := range incomingProf.LabelsIncludeAll {
|
|
label.ProfileUUID = currentProfile.ProfileUUID
|
|
label.Exclude = false
|
|
label.RequireAll = true
|
|
incomingLabels = append(incomingLabels, label)
|
|
profHasLabel = true
|
|
}
|
|
for _, label := range incomingProf.LabelsIncludeAny {
|
|
label.ProfileUUID = currentProfile.ProfileUUID
|
|
label.Exclude = false
|
|
label.RequireAll = false
|
|
incomingLabels = append(incomingLabels, label)
|
|
profHasLabel = true
|
|
}
|
|
for _, label := range incomingProf.LabelsExcludeAny {
|
|
label.ProfileUUID = currentProfile.ProfileUUID
|
|
label.Exclude = true
|
|
label.RequireAll = false
|
|
incomingLabels = append(incomingLabels, label)
|
|
profHasLabel = true
|
|
}
|
|
if !profHasLabel {
|
|
profsWithoutLabels = append(profsWithoutLabels, currentProfile.ProfileUUID)
|
|
}
|
|
}
|
|
|
|
var didUpdateLabels bool
|
|
if didUpdateLabels, err = batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels, profsWithoutLabels,
|
|
platform); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, fmt.Sprintf("inserting %s profile label associations", platform))
|
|
}
|
|
|
|
// save fleet variables associated with Windows profiles (both new and updated)
|
|
// Note: currentProfiles contains all incoming profiles (new AND updated), not just new ones
|
|
// Process ALL profiles to ensure stale variable associations are cleared for profiles that no longer have variables
|
|
profileVariablesByName := make(map[string][]fleet.FleetVarName, len(profilesVariablesByIdentifier))
|
|
for _, pv := range profilesVariablesByIdentifier {
|
|
profileVariablesByName[pv.Identifier] = pv.FleetVariables
|
|
}
|
|
|
|
// collect ALL profile UUIDs, including those without variables (to clear stale associations)
|
|
var profilesVarsToUpsert []fleet.MDMProfileUUIDFleetVariables
|
|
for _, p := range currentProfiles {
|
|
vars := profileVariablesByName[p.Name] // defaults to nil/empty slice if not found
|
|
// Include every profile, even those without variables, so the helper can clear old associations
|
|
profilesVarsToUpsert = append(profilesVarsToUpsert, fleet.MDMProfileUUIDFleetVariables{
|
|
ProfileUUID: p.ProfileUUID,
|
|
FleetVariables: vars, // may be empty/nil, which will clear associations
|
|
})
|
|
}
|
|
|
|
if len(profilesVarsToUpsert) > 0 {
|
|
var didUpdateVariableAssociations bool
|
|
if didUpdateVariableAssociations, err = batchSetProfileVariableAssociationsDB(ctx, tx, profilesVarsToUpsert, platform); err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, fmt.Sprintf("inserting %s profile variable associations", platform))
|
|
}
|
|
|
|
return didUpdateVariableAssociations, nil
|
|
}
|
|
|
|
return didUpdateLabels, nil
|
|
}
|
|
|
|
func reconcileHostEmailsFromMdmIdpAccountsDB(ctx context.Context, tx sqlx.ExtContext, logger log.Logger, hostID uint) (*fleet.MDMIdPAccount, error) {
|
|
idp, err := getMDMIdPAccountByHostID(ctx, tx, logger, hostID)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get host mdm idp account email")
|
|
}
|
|
|
|
var hostEmails []fleet.HostDeviceMapping
|
|
selectStmt := `SELECT id, host_id, email, source FROM host_emails WHERE host_id = ? AND source = ?`
|
|
if err := sqlx.SelectContext(ctx, tx, &hostEmails, selectStmt, hostID, fleet.DeviceMappingMDMIdpAccounts); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get host_emails")
|
|
}
|
|
|
|
// TODO: discuss email vs. username with Victor
|
|
var idpEmail, idpAccountUUID string
|
|
if idp != nil {
|
|
idpEmail = idp.Email
|
|
idpAccountUUID = idp.UUID
|
|
}
|
|
|
|
// if we don't have idp info, we can just delete any prior host mdm idp account emails
|
|
if idpEmail == "" {
|
|
if len(hostEmails) == 0 {
|
|
// nothing to do
|
|
level.Info(logger).Log("msg", "reconcile host emails: no mdm idp account and no host emails", "host_id", hostID, "account_uuid", idpAccountUUID)
|
|
return nil, nil
|
|
}
|
|
// delete any prior host mdm idp account emails
|
|
delStmt := "DELETE FROM host_emails WHERE host_id = ? AND source = ?"
|
|
if _, err := tx.ExecContext(ctx, delStmt, hostID, fleet.DeviceMappingMDMIdpAccounts); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "delete host_emails")
|
|
}
|
|
return idp, nil
|
|
}
|
|
|
|
// analyze existing host emails to see if we have a match; we also want to handle potential
|
|
// duplicates because we don't have good constraints on the host_emails table
|
|
hits, misses := []fleet.HostDeviceMapping{}, []fleet.HostDeviceMapping{}
|
|
for _, he := range hostEmails {
|
|
if he.Email == idp.Email {
|
|
hits = append(hits, he)
|
|
} else {
|
|
misses = append(misses, he)
|
|
}
|
|
}
|
|
|
|
maxCapacity := len(misses)
|
|
if len(hits) > 1 {
|
|
maxCapacity += len(hits) - 1
|
|
}
|
|
idsToDelete := make([]uint, 0, maxCapacity)
|
|
if len(misses) > 0 {
|
|
// log the emails we'll be deleting
|
|
msg := "reconcile host emails: deleting emails"
|
|
for _, m := range misses {
|
|
idsToDelete = append(idsToDelete, m.ID)
|
|
msg += fmt.Sprintf(" %s", m.Email)
|
|
}
|
|
level.Info(logger).Log("msg", msg, "host_id", hostID)
|
|
}
|
|
|
|
var idToUpdate uint
|
|
if len(hits) > 0 {
|
|
// if we have more than one hit, we'll just update the first one
|
|
idToUpdate = hits[0].ID
|
|
}
|
|
if len(hits) > 1 {
|
|
// this should not happen, but if it does we want to know about it
|
|
level.Info(logger).Log("msg", "reconcile host emails: found duplicate mdm idp account host email", "host_id", hostID, "email", idp.Email, "account_uuid", idp.UUID)
|
|
for _, h := range hits[1:] {
|
|
idsToDelete = append(idsToDelete, h.ID) // we'll delete all but the first
|
|
}
|
|
}
|
|
|
|
if len(idsToDelete) > 0 {
|
|
// perform the delete
|
|
stmt, args, err := sqlx.In("DELETE FROM host_emails WHERE id IN (?)", idsToDelete)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "prepare delete host_emails arguments")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "delete host_emails")
|
|
}
|
|
}
|
|
|
|
if idToUpdate != 0 {
|
|
// perform the update
|
|
level.Info(logger).Log("msg", "reconcile host emails: update host mdm idp account email", "host_id", hostID, "email", idpEmail)
|
|
updateStmt := "UPDATE host_emails SET email = ? WHERE id = ?"
|
|
if _, err := tx.ExecContext(ctx, updateStmt, idpEmail, idToUpdate); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "update host_emails")
|
|
}
|
|
return idp, nil
|
|
}
|
|
|
|
// insert new email
|
|
level.Info(logger).Log("msg", "reconcile host emails: insert host mdm idp account email", "host_id", hostID, "email", idpEmail, "account_uuid", idp.UUID)
|
|
insStmt := "INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?)"
|
|
if _, err := tx.ExecContext(ctx, insStmt, idpEmail, hostID, fleet.DeviceMappingMDMIdpAccounts); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "insert host_emails")
|
|
}
|
|
|
|
return idp, nil
|
|
}
|
|
|
|
func getMDMIdPAccountByHostID(ctx context.Context, q sqlx.QueryerContext, logger log.Logger, hostID uint) (*fleet.MDMIdPAccount, error) {
|
|
stmt := `SELECT account_uuid FROM host_mdm_idp_accounts WHERE host_uuid = (SELECT uuid FROM hosts WHERE id = ?)`
|
|
var dest []string
|
|
if err := sqlx.SelectContext(ctx, q, &dest, stmt, hostID); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "select host_mdm_idp_accounts")
|
|
}
|
|
|
|
var acctUUID string
|
|
switch {
|
|
case len(dest) == 0:
|
|
// TODO: consider falling back to the legacy enroll ref
|
|
level.Info(logger).Log("msg", "get host mdm idp accounts: no account found", "host_id", hostID)
|
|
default:
|
|
if len(dest) > 1 {
|
|
// this should not happen, but if it does we want to know about it
|
|
level.Info(logger).Log("msg", "get host mdm idp accounts: found multiple accounts", "host_id", hostID, "acct_uuids", fmt.Sprintf("%+v", dest))
|
|
}
|
|
acctUUID = dest[0]
|
|
}
|
|
|
|
if acctUUID == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
var idp fleet.MDMIdPAccount
|
|
stmt = `SELECT uuid, username, fullname, email FROM mdm_idp_accounts WHERE uuid = ?`
|
|
if err := sqlx.GetContext(ctx, q, &idp, stmt, acctUUID); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil // TODO: maybe return a not found error?
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err, "get host mdm idp account")
|
|
}
|
|
|
|
return &idp, nil
|
|
}
|