fleet/server/datastore/mysql/vpp.go

2764 lines
88 KiB
Go

package mysql
import (
"cmp"
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"time"
"github.com/fleetdm/fleet/v4/pkg/automatic_policy"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-sql-driver/mysql"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
func (ds *Datastore) GetVPPAppMetadataByTeamAndTitleID(ctx context.Context, teamID *uint, titleID uint) (*fleet.VPPAppStoreApp, error) {
const query = `
SELECT
vap.adam_id,
vap.platform,
vap.name,
vap.latest_version,
vat.self_service,
vat.id vpp_apps_teams_id,
vat.created_at added_at,
NULLIF(vap.icon_url, '') AS icon_url,
vap.bundle_identifier AS bundle_identifier
FROM
vpp_apps vap
INNER JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform
WHERE
vap.title_id = ? %s`
// when team id is not nil, we need to filter by the global or team id given.
args := []any{titleID}
teamFilter := ""
if teamID != nil {
args = append(args, *teamID)
teamFilter = "AND vat.global_or_team_id = ?"
}
var app fleet.VPPAppStoreApp
err := sqlx.GetContext(ctx, ds.reader(ctx), &app, fmt.Sprintf(query, teamFilter), args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("VPPApp"), "get VPP app metadata")
}
return nil, ctxerr.Wrap(ctx, err, "get VPP app metadata")
}
labels, err := ds.getVPPAppLabels(ctx, app.VPPAppsTeamsID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get vpp app labels")
}
var exclAny, inclAny, inclAll []fleet.SoftwareScopeLabel
for _, l := range labels {
switch {
case l.Exclude && !l.RequireAll:
exclAny = append(exclAny, l)
case !l.Exclude && l.RequireAll:
inclAll = append(inclAll, l)
case !l.Exclude && !l.RequireAll:
inclAny = append(inclAny, l)
default:
ds.logger.WarnContext(ctx, "vpp app has an unsupported label scope", "vpp_apps_teams_id", app.VPPAppsTeamsID, "invalid_label", fmt.Sprintf("%#v", l))
}
}
var count int
for _, set := range [][]fleet.SoftwareScopeLabel{exclAny, inclAny, inclAll} {
if len(set) > 0 {
count++
}
}
if count > 1 {
ds.logger.WarnContext(ctx, "vpp app has more than one scope of labels", "vpp_apps_teams_id", app.VPPAppsTeamsID, "include_any", fmt.Sprintf("%v", inclAny), "exclude_any", fmt.Sprintf("%v", exclAny), "include_all", fmt.Sprintf("%v", inclAll))
}
app.LabelsExcludeAny = exclAny
app.LabelsIncludeAny = inclAny
app.LabelsIncludeAll = inclAll
categories, err := ds.getCategoriesForVPPApp(ctx, app.VPPAppsTeamsID)
if err != nil {
return nil, err
}
app.Categories = categories
var tmID uint
if teamID != nil {
tmID = *teamID
}
displayName, err := ds.getSoftwareTitleDisplayName(ctx, tmID, titleID)
if err != nil && !fleet.IsNotFound(err) {
return nil, ctxerr.Wrap(ctx, err, "get display name for app store app")
}
app.DisplayName = displayName
config, err := ds.GetAndroidAppConfiguration(ctx, app.AdamID, tmID) // tmID can be used as globalOrTeamID
if err != nil && !fleet.IsNotFound(err) {
return nil, ctxerr.Wrap(ctx, err, "get android configuration for app store app")
}
if config != nil {
app.Configuration = *config
}
if teamID != nil {
policies, err := ds.getPoliciesBySoftwareTitleIDs(ctx, []uint{titleID}, *teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get policies by software title ID")
}
app.AutomaticInstallPolicies = policies
icon, err := ds.GetSoftwareTitleIcon(ctx, *teamID, titleID)
if err != nil && !fleet.IsNotFound(err) {
return nil, ctxerr.Wrap(ctx, err, "get software title icon")
}
if icon != nil {
app.IconURL = ptr.String(icon.IconUrl())
}
}
return &app, nil
}
func (ds *Datastore) getCategoriesForVPPApp(ctx context.Context, vppAppsTeamID uint) ([]string, error) {
stmt := `
SELECT
sc.name
FROM software_categories sc
JOIN vpp_app_team_software_categories vatsc ON sc.id = vatsc.software_category_id
WHERE vatsc.vpp_app_team_id = ?`
var categories []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &categories, stmt, vppAppsTeamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get categories for vpp app")
}
return categories, nil
}
func (ds *Datastore) getVPPAppLabels(ctx context.Context, vppAppsTeamsID uint) ([]fleet.SoftwareScopeLabel, error) {
query := `
SELECT
label_id,
exclude,
l.name AS label_name,
va.title_id AS title_id,
require_all
FROM
vpp_app_team_labels vatl
JOIN vpp_apps_teams vat ON vat.id = vatl.vpp_app_team_id
JOIN vpp_apps va ON va.adam_id = vat.adam_id
JOIN labels l ON l.id = vatl.label_id
WHERE
vatl.vpp_app_team_id = ? AND va.platform = vat.platform
`
var labels []fleet.SoftwareScopeLabel
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &labels, query, vppAppsTeamsID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get vpp app labels")
}
return labels, nil
}
func (ds *Datastore) GetSummaryHostVPPAppInstalls(ctx context.Context, teamID *uint, appID fleet.VPPAppID) (*fleet.VPPAppStatusSummary,
error,
) {
var dest fleet.VPPAppStatusSummary
stmt := `
WITH
-- select most recent upcoming activities for each host
upcoming AS (
SELECT
ua.host_id,
:software_status_pending AS status
FROM
upcoming_activities ua
JOIN vpp_app_upcoming_activities vaua ON ua.id = vaua.upcoming_activity_id
JOIN hosts h ON host_id = h.id
LEFT JOIN (
upcoming_activities ua2
INNER JOIN vpp_app_upcoming_activities vaua2
ON ua2.id = vaua2.upcoming_activity_id
) ON ua.host_id = ua2.host_id AND
vaua.adam_id = vaua2.adam_id AND
vaua.platform = vaua2.platform AND
ua.activity_type = ua2.activity_type AND
(ua2.priority < ua.priority OR ua2.created_at > ua.created_at)
WHERE
ua.activity_type = 'vpp_app_install'
AND ua2.id IS NULL
AND vaua.adam_id = :adam_id
AND vaua.platform = :platform
AND (h.team_id = :team_id OR (h.team_id IS NULL AND :team_id = 0))
),
-- select most recent past activities for each host
-- NOTE if you change this logic make sure to change vppAppHostStatusNamedQuery accordingly
past AS (
SELECT
hvsi.host_id,
CASE
WHEN hvsi.verification_at IS NOT NULL THEN
:software_status_installed
WHEN hvsi.verification_failed_at IS NOT NULL THEN
:software_status_failed
WHEN ncr.status = :mdm_status_error OR ncr.status = :mdm_status_format_error THEN
:software_status_failed
WHEN ncr.id IS NULL OR ncr.status = :mdm_status_acknowledged THEN
-- if ncr join is null this is an android vpp app
:software_status_pending
ELSE
NULL -- either pending or not installed via VPP App
END AS status
FROM
host_vpp_software_installs hvsi
JOIN hosts h ON host_id = h.id
LEFT JOIN nano_command_results ncr ON
ncr.id = h.uuid AND
ncr.command_uuid = hvsi.command_uuid
LEFT JOIN host_vpp_software_installs hvsi2
ON hvsi.host_id = hvsi2.host_id AND
hvsi.adam_id = hvsi2.adam_id AND
hvsi.platform = hvsi2.platform AND
hvsi2.removed = 0 AND
hvsi2.canceled = 0 AND
(hvsi.created_at < hvsi2.created_at OR (hvsi.created_at = hvsi2.created_at AND hvsi.id < hvsi2.id))
WHERE
hvsi2.id IS NULL
AND hvsi.adam_id = :adam_id
AND hvsi.platform = :platform
AND (ncr.id IS NOT NULL OR (:platform = 'android' AND ncr.id IS NULL))
AND (h.team_id = :team_id OR (h.team_id IS NULL AND :team_id = 0))
AND hvsi.host_id NOT IN (SELECT host_id FROM upcoming) -- antijoin to exclude hosts with upcoming activities
AND hvsi.removed = 0
AND hvsi.canceled = 0
)
-- count each status
SELECT
COALESCE(SUM( IF(status = :software_status_pending, 1, 0)), 0) AS pending,
COALESCE(SUM( IF(status = :software_status_failed, 1, 0)), 0) AS failed,
COALESCE(SUM( IF(status = :software_status_installed, 1, 0)), 0) AS installed
FROM (
-- union most recent past and upcoming activities after joining to get statuses for most recent activities
SELECT
past.host_id,
past.status
FROM past
UNION
SELECT
upcoming.host_id,
upcoming.status
FROM upcoming
) t`
var tmID uint
if teamID != nil {
tmID = *teamID
}
query, args, err := sqlx.Named(stmt, map[string]interface{}{
"adam_id": appID.AdamID,
"platform": appID.Platform,
"team_id": tmID,
"mdm_status_acknowledged": fleet.MDMAppleStatusAcknowledged,
"mdm_status_error": fleet.MDMAppleStatusError,
"mdm_status_format_error": fleet.MDMAppleStatusCommandFormatError,
"software_status_pending": fleet.SoftwareInstallPending,
"software_status_failed": fleet.SoftwareInstallFailed,
"software_status_installed": fleet.SoftwareInstalled,
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get summary host vpp installs: named query")
}
err = sqlx.GetContext(ctx, ds.reader(ctx), &dest, query, args...)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get summary host vpp install status")
}
return &dest, nil
}
// hvsiAlias is the table alias to use as prefix for the
// host_vpp_software_installs column names, no prefix used if empty.
// ncrAlias is the table alias to use as prefix for the nano_command_results
// column names, no prefix used if empty.
// colAlias is the name to be assigned to the computed status column, pass
// empty to have the value only, no column alias set.
func vppAppHostStatusNamedQuery(hvsiAlias, ncrAlias, colAlias string) string {
if hvsiAlias != "" {
hvsiAlias += "."
}
if ncrAlias != "" {
ncrAlias += "."
}
if colAlias != "" {
colAlias = " AS " + colAlias
}
// NOTE: if you change this logic, make sure to also change
// GetSummaryHostVPPAppInstalls accordingly.
return fmt.Sprintf(`
CASE
WHEN %sverification_at IS NOT NULL THEN
:software_status_installed
WHEN %sverification_failed_at IS NOT NULL THEN
:software_status_failed
WHEN %sstatus = :mdm_status_error OR %sstatus = :mdm_status_format_error THEN
:software_status_failed
ELSE
:software_status_pending
END %s
`, hvsiAlias, hvsiAlias, ncrAlias, ncrAlias, colAlias)
}
func (ds *Datastore) BatchInsertVPPApps(ctx context.Context, apps []*fleet.VPPApp) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
for _, app := range apps {
titleID, err := ds.getOrInsertSoftwareTitleForVPPApp(ctx, tx, app)
if err != nil {
return err
}
app.TitleID = titleID
if err := insertVPPApps(ctx, tx, []*fleet.VPPApp{app}); err != nil {
return ctxerr.Wrap(ctx, err, "BatchInsertVPPApps insertVPPApps transaction")
}
}
return nil
})
}
func (ds *Datastore) getExistingLabels(ctx context.Context, vppAppTeamID uint) (*fleet.LabelIdentsWithScope, error) {
existingLabels, err := ds.getVPPAppLabels(ctx, vppAppTeamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting existing labels")
}
var labels fleet.LabelIdentsWithScope
var exclAny, inclAny, inclAll []fleet.SoftwareScopeLabel
for _, l := range existingLabels {
switch {
case l.Exclude && !l.RequireAll:
exclAny = append(exclAny, l)
case !l.Exclude && l.RequireAll:
inclAll = append(inclAll, l)
case !l.Exclude && !l.RequireAll:
inclAny = append(inclAny, l)
default:
ds.logger.WarnContext(ctx, "vpp app has an unsupported existing label scope", "vpp_apps_teams_id", vppAppTeamID, "invalid_label", fmt.Sprintf("%#v", l))
}
}
var count int
for _, set := range [][]fleet.SoftwareScopeLabel{exclAny, inclAny, inclAll} {
if len(set) > 0 {
count++
}
}
if count > 1 {
// there's a bug somewhere
return nil, ctxerr.New(ctx, "found labels for more than one scope on a vpp app")
}
switch {
case len(exclAny) > 0:
labels.LabelScope = fleet.LabelScopeExcludeAny
labels.ByName = make(map[string]fleet.LabelIdent, len(exclAny))
for _, l := range exclAny {
labels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID}
}
return &labels, nil
case len(inclAny) > 0:
labels.LabelScope = fleet.LabelScopeIncludeAny
labels.ByName = make(map[string]fleet.LabelIdent, len(inclAny))
for _, l := range inclAny {
labels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID}
}
return &labels, nil
case len(inclAll) > 0:
labels.LabelScope = fleet.LabelScopeIncludeAll
labels.ByName = make(map[string]fleet.LabelIdent, len(inclAll))
for _, l := range inclAll {
labels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID}
}
return &labels, nil
default:
return nil, nil
}
}
func (ds *Datastore) getVPPAppTeamCategoryIDs(ctx context.Context, vppAppTeamID uint) ([]uint, error) {
stmt := `SELECT software_category_id FROM vpp_app_team_software_categories WHERE vpp_app_team_id = ?`
var ids []uint
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &ids, stmt, vppAppTeamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting existing software categories for vpp app team")
}
return ids, nil
}
func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, incomingApps []fleet.VPPAppTeam, appStoreAppIDsToTitleIDs map[string]uint) (bool, error) {
existingApps, err := ds.GetAssignedVPPApps(ctx, teamID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "SetTeamVPPApps getting list of existing apps")
}
// if we're batch-setting apps and replacing the ones installed during setup
// in the same go, no need to validate that we don't delete one marked as
// install during setup (since we're overwriting those). This is always
// called from fleetctl gitops, so it should always be the case anyway.
var replacingInstallDuringSetup bool
if len(incomingApps) == 0 || incomingApps[0].InstallDuringSetup != nil {
replacingInstallDuringSetup = true
}
var toAddApps []fleet.VPPAppTeam
var toRemoveApps []fleet.VPPAppID
for existingApp, appTeamInfo := range existingApps {
var found bool
for _, appFleet := range incomingApps {
// Self service value doesn't matter for removing app from team
if existingApp == appFleet.VPPAppID {
found = true
}
}
if !found {
// if app is marked as install during setup, prevent deletion unless we're replacing those.
if !replacingInstallDuringSetup && appTeamInfo.InstallDuringSetup != nil && *appTeamInfo.InstallDuringSetup {
return false, errDeleteInstallerInstalledDuringSetup
}
toRemoveApps = append(toRemoveApps, existingApp)
}
}
appsWithChangedLabels := make(map[uint]map[uint]struct{})
var vppTokenRequired, setupExperienceChanged bool
for _, incomingApp := range incomingApps {
if incomingApp.Platform.IsApplePlatform() {
vppTokenRequired = true
}
// upsert the app if anything changed
existingApp, isExistingApp := existingApps[incomingApp.VPPAppID]
incomingApp.AppTeamID = existingApp.AppTeamID
changed, err := ds.hasAppStoreAppChanged(ctx, teamID, incomingApp, existingApp, isExistingApp)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "checking if app store app changed")
}
// Get the hosts that are NOT in label scope currently (before the update happens)
if changed.Labels {
hostsNotInScope, err := ds.GetExcludedHostIDMapForVPPApp(ctx, incomingApp.AppTeamID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "getting hosts not in scope for vpp app")
}
appsWithChangedLabels[incomingApp.AppTeamID] = hostsNotInScope
}
if changed.InstallDuringSetup {
setupExperienceChanged = true
}
if changed.Any {
toAddApps = append(toAddApps, incomingApp)
}
}
teamName, err := ds.getTeamName(ctx, teamID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "get team name for VPP app conflict error")
}
var vppToken *fleet.VPPTokenDB
if len(incomingApps) > 0 && vppTokenRequired {
vppToken, err = ds.GetVPPTokenByTeamID(ctx, teamID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "SetTeamVPPApps retrieve VPP token ID")
}
}
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
for _, toAdd := range toAddApps {
var tokenID *uint
if vppToken != nil {
tokenID = &vppToken.ID
}
vppAppTeamID, err := insertVPPAppTeams(ctx, tx, toAdd, teamID, tokenID)
if err != nil {
return ctxerr.Wrap(ctx, err, "SetTeamVPPApps inserting vpp app into team")
}
// check if the vpp app conflicts with an existing software installer
// already associated with the software title for the same platform
// (macos) or any in-house app.
err = ds.checkSoftwareConflictsForVPPApp(ctx, tx, teamID, teamName, toAdd.VPPAppID)
if err != nil {
return ctxerr.Wrap(ctx, err, "check for software conflicts")
}
if toAdd.ValidatedLabels != nil {
if err := setOrUpdateSoftwareInstallerLabelsDB(ctx, tx, vppAppTeamID, *toAdd.ValidatedLabels, softwareTypeVPP); err != nil {
return ctxerr.Wrap(ctx, err, "failed to update labels on vpp apps batch operation")
}
}
if toAdd.CategoryIDs != nil {
if err := setOrUpdateSoftwareInstallerCategoriesDB(ctx, tx, vppAppTeamID, toAdd.CategoryIDs, softwareTypeVPP); err != nil {
return ctxerr.Wrap(ctx, err, "failed to update categories on vpp apps batch operation")
}
}
if toAdd.DisplayName != nil {
if err := updateSoftwareTitleDisplayName(ctx, tx, teamID, appStoreAppIDsToTitleIDs[toAdd.VPPAppID.String()], *toAdd.DisplayName); err != nil {
return ctxerr.Wrap(ctx, err, "setting software title display name for vpp app")
}
}
if toAdd.Configuration != nil {
if err := ds.updateAndroidAppConfigurationTx(ctx, tx, ptr.ValOrZero(teamID), toAdd.AdamID, toAdd.Configuration); err != nil {
return ctxerr.Wrap(ctx, err, "setting configuration for android app")
}
}
if hostsNotInScope, ok := appsWithChangedLabels[toAdd.AppTeamID]; ok {
hostsInScope, err := ds.GetIncludedHostIDMapForVPPAppTx(ctx, tx, toAdd.AppTeamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting hosts in scope for vpp app")
}
var hostsToClear []uint
for id := range hostsInScope {
if _, ok := hostsNotInScope[id]; ok {
// it was not in scope but now it is, so we should clear policy status
hostsToClear = append(hostsToClear, id)
}
}
// We clear the policy status here because otherwise the policy automation machinery
// won't pick this up and the software won't install.
if err := ds.ClearVPPAppAutoInstallPolicyStatusForHostsTx(ctx, tx, toAdd.AppTeamID, hostsToClear); err != nil {
return ctxerr.Wrap(ctx, err, "failed to clear auto install policy status for host")
}
}
}
for _, toRemove := range toRemoveApps {
if err := removeVPPAppTeams(ctx, tx, toRemove, teamID); err != nil {
return ctxerr.Wrap(ctx, err, "SetTeamVPPApps removing vpp app from team")
}
}
return nil
})
return setupExperienceChanged, err
}
func (ds *Datastore) checkConflictingSoftwareInstallerForVPPApp(
ctx context.Context, q sqlx.QueryerContext, teamID *uint, appID fleet.VPPAppID,
) (exists bool, title string, err error) {
const stmt = `
SELECT
st.name
FROM
software_titles st
INNER JOIN software_installers si ON si.title_id = st.id AND
si.global_or_team_id = ?
INNER JOIN vpp_apps va ON va.title_id = st.id
INNER JOIN vpp_apps_teams vat ON vat.adam_id = va.adam_id AND
vat.platform = va.platform AND vat.global_or_team_id = ?
WHERE
va.adam_id = ? AND
va.platform = ? AND
si.platform = va.platform
`
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = *teamID
}
err = sqlx.GetContext(ctx, q, &title, stmt, globalOrTeamID, globalOrTeamID, appID.AdamID, appID.Platform)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, "", nil
}
return false, "", ctxerr.Wrap(ctx, err, "checking for conflicting software installer for vpp app")
}
return true, title, nil
}
func (ds *Datastore) InsertVPPAppWithTeam(ctx context.Context, app *fleet.VPPApp, teamID *uint) (*fleet.VPPApp, error) {
var vppTokenID *uint
vppToken, err := ds.GetVPPTokenByTeamID(ctx, teamID)
if err != nil {
if !fleet.IsNotFound(err) || app.Platform != fleet.AndroidPlatform {
return nil, ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam unable to get VPP Token ID")
}
}
if vppToken != nil && app.Platform != fleet.AndroidPlatform {
vppTokenID = &vppToken.ID
}
teamName, err := ds.getTeamName(ctx, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get team for VPP app conflict error")
}
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
titleID, err := ds.getOrInsertSoftwareTitleForVPPApp(ctx, tx, app)
if err != nil {
return err
}
app.TitleID = titleID
if err := insertVPPApps(ctx, tx, []*fleet.VPPApp{app}); err != nil {
return ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam insertVPPApps transaction")
}
vppAppTeamID, err := insertVPPAppTeams(ctx, tx, app.VPPAppTeam, teamID, vppTokenID)
if err != nil {
return ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam insertVPPAppTeams transaction")
}
err = ds.checkSoftwareConflictsForVPPApp(ctx, tx, teamID, teamName, app.VPPAppID)
if err != nil {
return ctxerr.Wrap(ctx, err, "check for software conflicts")
}
app.VPPAppTeam.AppTeamID = vppAppTeamID
if app.ValidatedLabels != nil {
if err := setOrUpdateSoftwareInstallerLabelsDB(ctx, tx, vppAppTeamID, *app.ValidatedLabels, softwareTypeVPP); err != nil {
return ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam setOrUpdateSoftwareInstallerLabelsDB transaction")
}
}
if app.CategoryIDs != nil {
if err := setOrUpdateSoftwareInstallerCategoriesDB(ctx, tx, vppAppTeamID, app.CategoryIDs, softwareTypeVPP); err != nil {
return ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam setOrUpdateSoftwareInstallerCategoriesDB transaction")
}
}
if app.VPPAppTeam.AddAutoInstallPolicy {
generatedPolicyData, err := automatic_policy.Generate(automatic_policy.MacInstallerMetadata{
Title: app.Name,
BundleIdentifier: app.BundleIdentifier,
})
if err != nil {
return ctxerr.Wrap(ctx, err, "generate automatic policy query data")
}
policy, err := ds.createAutomaticPolicy(ctx, tx, *generatedPolicyData, teamID, nil, ptr.Uint(vppAppTeamID))
if err != nil {
return ctxerr.Wrap(ctx, err, "create automatic policy")
}
app.VPPAppTeam.AddedAutomaticInstallPolicy = policy
}
if app.DisplayName != nil {
if err := updateSoftwareTitleDisplayName(ctx, tx, teamID, titleID, *app.DisplayName); err != nil {
return ctxerr.Wrap(ctx, err, "setting software title display name for vpp app")
}
}
if app.Configuration != nil && app.Platform == fleet.AndroidPlatform {
if err := ds.updateAndroidAppConfigurationTx(ctx, tx, ptr.ValOrZero(teamID), app.AdamID, app.Configuration); err != nil {
return ctxerr.Wrap(ctx, err, "setting configuration for android app")
}
}
return nil
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam")
}
return app, nil
}
func (ds *Datastore) GetVPPApps(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
tmID := ptr.ValOrZero(teamID)
var results []fleet.VPPAppResponse
// intentionally using writer as this is called right after batch-setting VPP apps
if err := sqlx.SelectContext(ctx, ds.writer(ctx), &results, `
SELECT vat.id AS app_team_id, vat.team_id, va.title_id, vat.adam_id AS app_store_id, vat.platform,
COALESCE(icons.filename, '') AS icon_filename, COALESCE(icons.storage_id, '') AS icon_hash_sha256
FROM vpp_apps_teams vat
JOIN vpp_apps va ON va.adam_id = vat.adam_id AND va.platform = vat.platform
LEFT JOIN software_title_icons icons ON va.title_id = icons.software_title_id AND vat.global_or_team_id = icons.team_id
WHERE global_or_team_id = ?`, tmID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get VPP apps")
}
return results, nil
}
func (ds *Datastore) GetAssignedVPPApps(ctx context.Context, teamID *uint) (map[fleet.VPPAppID]fleet.VPPAppTeam, error) {
stmt := `
SELECT
adam_id, platform, self_service, install_during_setup, id, created_at added_at
FROM
vpp_apps_teams vat
WHERE
vat.global_or_team_id = ?
`
var tmID uint
if teamID != nil {
tmID = *teamID
}
var results []fleet.VPPAppTeam
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, stmt, tmID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get assigned VPP apps")
}
appSet := make(map[fleet.VPPAppID]fleet.VPPAppTeam)
for _, r := range results {
appSet[r.VPPAppID] = r
}
return appSet, nil
}
func (ds *Datastore) InsertVPPApps(ctx context.Context, apps []*fleet.VPPApp) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
return insertVPPApps(ctx, tx, apps)
})
}
func insertVPPApps(ctx context.Context, tx sqlx.ExtContext, apps []*fleet.VPPApp) error {
stmt := `
INSERT INTO vpp_apps
(adam_id, bundle_identifier, icon_url, name, latest_version, title_id, platform)
VALUES
%s
ON DUPLICATE KEY UPDATE
updated_at = CURRENT_TIMESTAMP,
latest_version = VALUES(latest_version),
icon_url = VALUES(icon_url),
name = VALUES(name),
title_id = VALUES(title_id)
`
var args []any
var insertVals strings.Builder
for _, a := range apps {
insertVals.WriteString(`(?, ?, ?, ?, ?, ?, ?),`)
args = append(args, a.AdamID, a.BundleIdentifier, a.IconURL, a.Name, a.LatestVersion, a.TitleID, a.Platform)
}
stmt = fmt.Sprintf(stmt, strings.TrimSuffix(insertVals.String(), ","))
_, err := tx.ExecContext(ctx, stmt, args...)
return ctxerr.Wrap(ctx, err, "insert VPP apps")
}
func insertVPPAppTeams(ctx context.Context, tx sqlx.ExtContext, appID fleet.VPPAppTeam, teamID *uint, vppTokenID *uint) (uint, error) {
stmt := `
INSERT INTO vpp_apps_teams
(adam_id, global_or_team_id, team_id, platform, self_service, vpp_token_id, install_during_setup)
VALUES
(?, ?, ?, ?, ?, ?, COALESCE(?, false))
ON DUPLICATE KEY UPDATE
self_service = VALUES(self_service),
install_during_setup = COALESCE(?, install_during_setup)
`
var globalOrTmID uint
if teamID != nil {
globalOrTmID = *teamID
if *teamID == 0 {
teamID = nil
}
}
res, err := tx.ExecContext(ctx, stmt, appID.AdamID, globalOrTmID, teamID, appID.Platform, appID.SelfService, vppTokenID, appID.InstallDuringSetup, appID.InstallDuringSetup)
if err != nil {
if IsDuplicate(err) {
err = &existsError{
Identifier: fmt.Sprintf("%s %s self_service: %v", appID.AdamID, appID.Platform, appID.SelfService),
TeamID: teamID,
ResourceType: "VPPAppID",
}
}
return 0, ctxerr.Wrap(ctx, err, "inserting app store app")
}
var id int64
if insertOnDuplicateDidInsertOrUpdate(res) {
id, _ = res.LastInsertId()
} else {
stmt := `SELECT id FROM vpp_apps_teams WHERE adam_id = ? AND platform = ? AND global_or_team_id = ?`
if err := sqlx.GetContext(ctx, tx, &id, stmt, appID.AdamID, appID.Platform, globalOrTmID); err != nil {
return 0, ctxerr.Wrap(ctx, err, "vpp app teams id")
}
}
vatID := uint(id) //nolint:gosec // dismiss G115
return vatID, ctxerr.Wrap(ctx, err, "writing vpp app team mapping to db")
}
func removeVPPAppTeams(ctx context.Context, tx sqlx.ExtContext, appID fleet.VPPAppID, teamID *uint) error {
_, err := tx.ExecContext(ctx, `UPDATE policies p
JOIN vpp_apps_teams vat ON vat.id = p.vpp_apps_teams_id AND vat.adam_id = ? AND vat.team_id = ? AND vat.platform = ?
SET vpp_apps_teams_id = NULL`, appID.AdamID, teamID, appID.Platform)
if err != nil {
return ctxerr.Wrap(ctx, err, "unsetting vpp app policy associations from team")
}
tmID := ptr.ValOrZero(teamID)
_, err = tx.ExecContext(ctx, `DELETE FROM vpp_apps_teams WHERE adam_id = ? AND global_or_team_id = ? AND platform = ?`, appID.AdamID, tmID, appID.Platform)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting vpp app from team")
}
_, err = tx.ExecContext(ctx, `DELETE FROM android_app_configurations WHERE application_id = ? AND global_or_team_id = ?`, appID.AdamID, tmID)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting android app configuration")
}
return nil
}
func (ds *Datastore) getOrInsertSoftwareTitleForVPPApp(ctx context.Context, tx sqlx.ExtContext, app *fleet.VPPApp) (uint, error) {
// NOTE: it was decided to populate "apps" as the source for VPP apps for now, TBD
// if this needs to change to better map to how software titles are reported
// back by osquery. Since it may change, we're using a variable for the source.
var source string
switch app.Platform {
case fleet.IOSPlatform:
source = "ios_apps"
case fleet.IPadOSPlatform:
source = "ipados_apps"
case fleet.AndroidPlatform:
source = "android_apps"
default:
source = "apps"
}
selectStmt := `SELECT id FROM software_titles WHERE name = ? AND source = ? AND extension_for = ''`
selectArgs := []any{app.Name, source}
insertStmt := `INSERT INTO software_titles (name, source, extension_for) VALUES (?, ?, '')`
insertArgs := []any{app.Name, source}
if app.BundleIdentifier != "" {
// match by bundle identifier first, or standard matching if we
// don't have a bundle identifier match
insertStmt = `INSERT INTO software_titles (name, source, bundle_identifier, extension_for) VALUES (?, ?, ?, '')`
insertArgs = append(insertArgs, app.BundleIdentifier)
switch source {
case "ios_apps", "ipados_apps":
selectStmt = `
SELECT id
FROM software_titles
WHERE (bundle_identifier = ? AND source = ?) OR (name = ? AND source = ? AND extension_for = '')
ORDER BY bundle_identifier = ? DESC
LIMIT 1`
selectArgs = []any{app.BundleIdentifier, source, app.Name, source, app.BundleIdentifier}
case "android_apps":
selectStmt = `
SELECT id
FROM software_titles
WHERE application_id = ? AND additional_identifier IS NULL AND source = 'android_apps'`
selectArgs = []any{app.BundleIdentifier}
insertStmt = `INSERT INTO software_titles (name, source, application_id, extension_for) VALUES (?, ?, ?, '')`
default:
selectStmt = `
SELECT id
FROM software_titles
WHERE bundle_identifier = ? AND additional_identifier = 0`
selectArgs = []any{app.BundleIdentifier}
}
}
titleID, err := ds.optimisticGetOrInsertWithWriter(ctx,
tx,
&parameterizedStmt{
Statement: selectStmt,
Args: selectArgs,
},
&parameterizedStmt{
Statement: insertStmt,
Args: insertArgs,
},
)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "optimistic get or insert app store app")
}
return titleID, nil
}
func (ds *Datastore) DeleteVPPAppFromTeam(ctx context.Context, teamID *uint, appID fleet.VPPAppID) error {
// allow delete only if install_during_setup is false
const stmt = `DELETE FROM vpp_apps_teams WHERE global_or_team_id = ? AND adam_id = ? AND platform = ? AND install_during_setup = 0`
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = *teamID
}
tx := ds.writer(ctx) // make sure we're looking at a consistent vision of the world when deleting
res, err := tx.ExecContext(ctx, stmt, globalOrTeamID, appID.AdamID, appID.Platform)
if err != nil {
if isMySQLForeignKey(err) {
// Check if the app is referenced by a policy automation.
var count int
if err := sqlx.GetContext(ctx, tx, &count, `SELECT COUNT(*) FROM policies p JOIN vpp_apps_teams vat
ON vat.id = p.vpp_apps_teams_id AND vat.global_or_team_id = ?
AND vat.adam_id = ? AND vat.platform = ?`, globalOrTeamID, appID.AdamID, appID.Platform); err != nil {
return ctxerr.Wrapf(ctx, err, "getting reference from policies")
}
if count > 0 {
return errDeleteInstallerWithAssociatedInstallPolicy
}
}
return ctxerr.Wrap(ctx, err, "delete VPP app from team")
}
rows, _ := res.RowsAffected()
if rows == 0 {
// could be that the VPP app does not exist, or it is installed during
// setup, do additional check.
var installDuringSetup bool
if err := sqlx.GetContext(ctx, tx, &installDuringSetup,
`SELECT install_during_setup FROM vpp_apps_teams WHERE global_or_team_id = ? AND adam_id = ? AND platform = ?`, globalOrTeamID, appID.AdamID, appID.Platform); err != nil && !errors.Is(err, sql.ErrNoRows) {
return ctxerr.Wrap(ctx, err, "check if vpp app is installed during setup")
}
if installDuringSetup {
return errDeleteInstallerInstalledDuringSetup
}
return notFound("VPPApp").WithMessage(fmt.Sprintf("adam id %s platform %s for team id %d", appID.AdamID, appID.Platform,
globalOrTeamID))
}
_, err = tx.ExecContext(ctx, `DELETE FROM software_title_display_names WHERE team_id = ? AND
software_title_id IN (SELECT title_id FROM vpp_apps WHERE adam_id = ?)`, globalOrTeamID, appID.AdamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete software title display name")
}
if appID.Platform == fleet.AndroidPlatform {
err := ds.DeleteAndroidAppConfiguration(ctx, appID.AdamID, globalOrTeamID)
if err != nil && !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "deleting android app configuration")
}
}
return nil
}
func (ds *Datastore) GetTitleInfoFromVPPAppsTeamsID(ctx context.Context, vppAppsTeamsID uint) (*fleet.PolicySoftwareTitle, error) {
stmt := `
SELECT
va.name AS name,
va.title_id AS title_id,
COALESCE(stdn.display_name, '') AS display_name
FROM vpp_apps va
JOIN vpp_apps_teams vat ON vat.adam_id = va.adam_id AND vat.platform = va.platform AND vat.id = ?
LEFT JOIN software_title_display_names stdn ON stdn.software_title_id = va.title_id AND stdn.team_id = vat.global_or_team_id`
var info fleet.PolicySoftwareTitle
err := sqlx.GetContext(ctx, ds.reader(ctx), &info, stmt, vppAppsTeamsID)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("VPPApp"), "get VPP title info from VPP apps teams ID")
}
return nil, ctxerr.Wrap(ctx, err, "get VPP title info from VPP apps teams ID")
}
return &info, nil
}
func (ds *Datastore) GetVPPAppMetadataByAdamIDPlatformTeamID(ctx context.Context, adamID string, platform fleet.InstallableDevicePlatform, teamID *uint) (*fleet.VPPApp, error) {
stmt := `
SELECT va.adam_id,
va.bundle_identifier,
va.icon_url,
va.name,
va.platform,
vat.self_service,
va.title_id,
va.platform,
va.created_at,
vat.created_at added_at,
va.updated_at,
vat.id
FROM vpp_apps va
JOIN vpp_apps_teams vat ON va.adam_id = vat.adam_id AND va.platform = vat.platform AND vat.global_or_team_id = ?
WHERE va.adam_id = ? AND va.platform = ?
`
// when team id is not nil, we need to filter by the global or team id given.
var tmID uint
if teamID != nil {
tmID = *teamID
}
var dest fleet.VPPApp
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, tmID, adamID, platform)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("VPPApp"), "get VPP app metadata by team")
}
return nil, ctxerr.Wrap(ctx, err, "get VPP app metadata by team")
}
return &dest, nil
}
func (ds *Datastore) GetVPPAppByTeamAndTitleID(ctx context.Context, teamID *uint, titleID uint) (*fleet.VPPApp, error) {
stmt := `
SELECT
vat.id,
va.adam_id,
va.bundle_identifier,
va.icon_url,
va.name,
va.title_id,
va.platform,
va.created_at,
va.updated_at,
vat.self_service,
vat.created_at added_at
FROM vpp_apps va
JOIN vpp_apps_teams vat ON va.adam_id = vat.adam_id AND va.platform = vat.platform
WHERE vat.global_or_team_id = ? AND va.title_id = ?
`
var tmID uint
if teamID != nil {
tmID = *teamID
}
var dest fleet.VPPApp
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, tmID, titleID)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("VPPApp"), "get VPP app")
}
return nil, ctxerr.Wrap(ctx, err, "get VPP app")
}
return &dest, nil
}
func (ds *Datastore) InsertHostVPPSoftwareInstall(ctx context.Context, hostID uint, appID fleet.VPPAppID,
commandUUID, associatedEventID string, opts fleet.HostSoftwareInstallOptions,
) error {
const (
insertUAStmt = `
INSERT INTO upcoming_activities
(host_id, priority, user_id, fleet_initiated, activity_type, execution_id, payload)
VALUES
(?, ?, ?, ?, 'vpp_app_install', ?,
JSON_OBJECT(
'self_service', ?,
'from_auto_update', ?,
'associated_event_id', ?,
'user', (SELECT JSON_OBJECT('name', name, 'email', email, 'gravatar_url', gravatar_url) FROM users WHERE id = ?)
)
)`
insertVAUAStmt = `
INSERT INTO vpp_app_upcoming_activities
(upcoming_activity_id, adam_id, platform, policy_id)
VALUES
(?, ?, ?, ?)`
hostExistsStmt = `SELECT 1 FROM hosts WHERE id = ?`
)
// we need to explicitly do this check here because we can't set a FK constraint on the schema
var hostExists bool
err := sqlx.GetContext(ctx, ds.reader(ctx), &hostExists, hostExistsStmt, hostID)
if err != nil {
if err == sql.ErrNoRows {
return notFound("Host").WithID(hostID)
}
return ctxerr.Wrap(ctx, err, "checking if host exists")
}
var userID *uint
if ctxUser := authz.UserFromContext(ctx); ctxUser != nil && opts.PolicyID == nil {
userID = &ctxUser.ID
}
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
res, err := tx.ExecContext(ctx, insertUAStmt,
hostID,
opts.Priority(),
userID,
opts.IsFleetInitiated(),
commandUUID,
opts.SelfService,
opts.ForScheduledUpdates,
associatedEventID,
userID,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "insert vpp install request")
}
activityID, _ := res.LastInsertId()
_, err = tx.ExecContext(ctx, insertVAUAStmt,
activityID,
appID.AdamID,
appID.Platform,
opts.PolicyID,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "insert vpp install request join table")
}
if _, err := ds.activateNextUpcomingActivity(ctx, tx, hostID, ""); err != nil {
return ctxerr.Wrap(ctx, err, "activate next activity")
}
return nil
})
return err
}
func (ds *Datastore) MapAdamIDsPendingInstall(ctx context.Context, hostID uint) (map[string]struct{}, error) {
var adamIds []string
// this is Apple-only VPP installs, which is fine currently as Android does not support
// policy-based automatic installs (the context in which this is called).
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &adamIds, `SELECT hvsi.adam_id
FROM host_vpp_software_installs hvsi
JOIN nano_view_queue nvq ON nvq.command_uuid = hvsi.command_uuid AND nvq.status IS NULL
WHERE hvsi.host_id = ? AND hvsi.canceled = 0`, hostID); err != nil && err != sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, err, "list pending VPP installs")
}
adamMap := map[string]struct{}{}
for _, id := range adamIds {
adamMap[id] = struct{}{}
}
return adamMap, nil
}
func (ds *Datastore) MapAdamIDsPendingInstallVerification(ctx context.Context, hostID uint) (adamIDs map[string]struct{}, err error) {
var adamIDsList []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &adamIDsList, `SELECT hvsi.adam_id
FROM host_vpp_software_installs hvsi
JOIN nano_view_queue nvq ON nvq.command_uuid = hvsi.command_uuid
WHERE hvsi.host_id = ?
AND hvsi.canceled = 0
AND (
nvq.status IS NULL -- install command not acknowledged yet
OR
(
-- install command acknowledged but still waiting for verification
-- completed/failed installation.
nvq.status = 'Acknowledged'
AND hvsi.verification_at IS NULL
AND hvsi.verification_failed_at IS NULL
)
)`, hostID); err != nil && err != sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, err, "list host pending VPP install verifications")
}
adamIDs = make(map[string]struct{})
for _, id := range adamIDsList {
adamIDs[id] = struct{}{}
}
return adamIDs, nil
}
func (ds *Datastore) MapAdamIDsRecentInstalls(ctx context.Context, hostID uint, seconds int) (adamIDs map[string]struct{}, err error) {
var adamIDsList []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &adamIDsList,
`SELECT DISTINCT(adam_id) FROM host_vpp_software_installs
WHERE host_id = ? AND canceled = 0 AND created_at >= NOW() - INTERVAL ? SECOND`,
hostID, seconds); err != nil && err != sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, err, "list host recent VPP install attempts")
}
adamIDs = make(map[string]struct{})
for _, id := range adamIDsList {
adamIDs[id] = struct{}{}
}
return adamIDs, nil
}
func (ds *Datastore) GetPastActivityDataForAndroidVPPAppInstall(ctx context.Context, cmdUUID string, status fleet.SoftwareInstallerStatus) (*fleet.User, *fleet.ActivityInstalledAppStoreApp, error) {
return ds.getPastActivityDataForAndroidVPPAppInstallDB(ctx, ds.reader(ctx), cmdUUID, status)
}
func (ds *Datastore) getPastActivityDataForAndroidVPPAppInstallDB(ctx context.Context, q sqlx.QueryerContext, cmdUUID string, status fleet.SoftwareInstallerStatus) (*fleet.User, *fleet.ActivityInstalledAppStoreApp, error) {
mdmStatus := fleet.MDMAppleStatusAcknowledged
if status != fleet.SoftwareInstalled {
mdmStatus = fleet.MDMAppleStatusError
}
return ds.getPastActivityDataForVPPAppInstallDB(ctx, q, &mdm.CommandResults{CommandUUID: cmdUUID, Status: mdmStatus})
}
func (ds *Datastore) GetPastActivityDataForVPPAppInstall(ctx context.Context, commandResults *mdm.CommandResults) (*fleet.User, *fleet.ActivityInstalledAppStoreApp, error) {
return ds.getPastActivityDataForVPPAppInstallDB(ctx, ds.reader(ctx), commandResults)
}
func (ds *Datastore) getPastActivityDataForVPPAppInstallDB(ctx context.Context, q sqlx.QueryerContext, commandResults *mdm.CommandResults) (*fleet.User, *fleet.ActivityInstalledAppStoreApp, error) {
if commandResults == nil {
return nil, nil, nil
}
stmt := `
SELECT
u.name AS user_name,
u.id AS user_id,
u.email as user_email,
hvsi.host_id AS host_id,
hdn.display_name AS host_display_name,
st.name AS software_title,
hvsi.adam_id AS app_store_id,
hvsi.command_uuid AS command_uuid,
hvsi.self_service AS self_service,
hvsi.policy_id AS policy_id,
p.name AS policy_name,
h.platform AS platform
FROM
host_vpp_software_installs hvsi
LEFT OUTER JOIN users u ON hvsi.user_id = u.id
LEFT OUTER JOIN hosts h ON h.id = hvsi.host_id
LEFT OUTER JOIN host_display_names hdn ON hdn.host_id = hvsi.host_id
LEFT OUTER JOIN vpp_apps vpa ON hvsi.adam_id = vpa.adam_id
LEFT OUTER JOIN software_titles st ON st.id = vpa.title_id
LEFT OUTER JOIN policies p ON p.id = hvsi.policy_id
WHERE
hvsi.command_uuid = :command_uuid AND
hvsi.canceled = 0
`
type result struct {
HostID uint `db:"host_id"`
HostDisplayName string `db:"host_display_name"`
SoftwareTitle string `db:"software_title"`
AppStoreID string `db:"app_store_id"`
CommandUUID string `db:"command_uuid"`
UserName *string `db:"user_name"`
UserID *uint `db:"user_id"`
UserEmail *string `db:"user_email"`
SelfService bool `db:"self_service"`
PolicyID *uint `db:"policy_id"`
PolicyName *string `db:"policy_name"`
HostPlatform string `db:"platform"`
}
listStmt, args, err := sqlx.Named(stmt, map[string]any{
"command_uuid": commandResults.CommandUUID,
"software_status_failed": string(fleet.SoftwareInstallFailed),
"software_status_installed": string(fleet.SoftwareInstalled),
})
if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "build list query from named args")
}
var res result
if err := sqlx.GetContext(ctx, q, &res, listStmt, args...); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, notFound("install_command")
}
return nil, nil, ctxerr.Wrap(ctx, err, "select past activity data for VPP app install")
}
var user *fleet.User
if res.UserID != nil {
user = &fleet.User{
ID: *res.UserID,
Name: *res.UserName,
Email: *res.UserEmail,
}
}
var status string
switch commandResults.Status {
case fleet.MDMAppleStatusAcknowledged:
status = string(fleet.SoftwareInstalled)
case fleet.MDMAppleStatusCommandFormatError, fleet.MDMAppleStatusError:
status = string(fleet.SoftwareInstallFailed)
default:
// This case shouldn't happen (we should only be doing this check if the command is in a
// "terminal" state, but adding it so we have a default
status = string(fleet.SoftwareInstallPending)
}
act := &fleet.ActivityInstalledAppStoreApp{
HostID: res.HostID,
HostDisplayName: res.HostDisplayName,
SoftwareTitle: res.SoftwareTitle,
AppStoreID: res.AppStoreID,
CommandUUID: res.CommandUUID,
SelfService: res.SelfService,
PolicyID: res.PolicyID,
PolicyName: res.PolicyName,
Status: status,
HostPlatform: res.HostPlatform,
}
return user, act, nil
}
// GetVPPAppInstallStatusByCommandUUID returns whether the VPP app from the given install command
// is currently installed (not at the time the command was issued).
// Because of the UNIQUE constraint on command_uuid in host_vpp_software_installs,
// each command UUID maps to exactly one host. Returns false if the command doesn't exist.
func (ds *Datastore) GetVPPAppInstallStatusByCommandUUID(ctx context.Context, commandUUID string) (bool, error) {
stmt := `
SELECT EXISTS(
SELECT 1
FROM host_vpp_software_installs hvsi
JOIN vpp_apps vpa ON hvsi.adam_id = vpa.adam_id
JOIN software_titles st ON st.id = vpa.title_id
JOIN software s ON s.title_id = st.id
JOIN host_software hs ON hs.software_id = s.id AND hs.host_id = hvsi.host_id
WHERE hvsi.command_uuid = ?
) AS is_installed
`
var isInstalled bool
if err := sqlx.GetContext(ctx, ds.reader(ctx), &isInstalled, stmt, commandUUID); err != nil {
return false, ctxerr.Wrap(ctx, err, "get VPP app install status by command UUID")
}
return isInstalled, nil
}
func (ds *Datastore) GetVPPTokenByLocation(ctx context.Context, loc string) (*fleet.VPPTokenDB, error) {
stmt := `SELECT id FROM vpp_tokens WHERE location = ?`
var tokenID uint
if err := sqlx.GetContext(ctx, ds.reader(ctx), &tokenID, stmt, loc); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ctxerr.Wrap(ctx, notFound("VPPToken"), "retrieve vpp token by location")
}
return nil, ctxerr.Wrap(ctx, err, "retrieve vpp token by location")
}
return ds.GetVPPToken(ctx, tokenID)
}
func (ds *Datastore) InsertVPPToken(ctx context.Context, tok *fleet.VPPTokenData) (*fleet.VPPTokenDB, error) {
insertStmt := `
INSERT INTO
vpp_tokens (
organization_name,
location,
renew_at,
token
)
VALUES (?, ?, ?, ?)
`
vppTokenDB, err := vppTokenDataToVppTokenDB(ctx, tok)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "translating vpp token to db representation")
}
tokEnc, err := encrypt([]byte(vppTokenDB.Token), ds.serverPrivateKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "encrypt token with datastore.serverPrivateKey")
}
res, err := ds.writer(ctx).ExecContext(
ctx,
insertStmt,
vppTokenDB.OrgName,
vppTokenDB.Location,
vppTokenDB.RenewDate,
tokEnc,
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting vpp token")
}
id, _ := res.LastInsertId()
vppTokenDB.ID = uint(id) //nolint:gosec // dismiss G115
return vppTokenDB, nil
}
func (ds *Datastore) UpdateVPPToken(ctx context.Context, tokenID uint, tok *fleet.VPPTokenData) (*fleet.VPPTokenDB, error) {
stmt := `
UPDATE vpp_tokens
SET
organization_name = ?,
location = ?,
renew_at = ?,
token = ?
WHERE
id = ?
`
vppTokenDB, err := vppTokenDataToVppTokenDB(ctx, tok)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "translating vpp token to db representation")
}
tokEnc, err := encrypt([]byte(vppTokenDB.Token), ds.serverPrivateKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "encrypt token with datastore.serverPrivateKey")
}
_, err = ds.writer(ctx).ExecContext(
ctx,
stmt,
vppTokenDB.OrgName,
vppTokenDB.Location,
vppTokenDB.RenewDate,
tokEnc,
tokenID,
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting vpp token")
}
return ds.GetVPPToken(ctx, tokenID)
}
func vppTokenDataToVppTokenDB(ctx context.Context, tok *fleet.VPPTokenData) (*fleet.VPPTokenDB, error) {
tokRawBytes, err := base64.StdEncoding.DecodeString(tok.Token)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "decoding raw vpp token data")
}
var tokRaw fleet.VPPTokenRaw
if err := json.Unmarshal(tokRawBytes, &tokRaw); err != nil {
return nil, ctxerr.Wrap(ctx, err, "unmarshalling raw vpp token")
}
exp, err := time.Parse(fleet.VPPTimeFormat, tokRaw.ExpDate)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "parsing vpp token expiration date")
}
exp = exp.UTC()
vppTokenDB := &fleet.VPPTokenDB{
OrgName: tokRaw.OrgName,
Location: tok.Location,
RenewDate: exp,
Token: tok.Token,
}
return vppTokenDB, nil
}
func (ds *Datastore) GetVPPToken(ctx context.Context, tokenID uint) (*fleet.VPPTokenDB, error) {
stmt := `
SELECT
id,
organization_name,
location,
renew_at,
token
FROM
vpp_tokens v
WHERE
id = ?
`
stmtTeams := `
SELECT
vt.team_id,
vt.null_team_type,
COALESCE(t.name, '') AS name
FROM
vpp_token_teams vt
LEFT OUTER JOIN
teams t
ON t.id = vt.team_id
WHERE
vpp_token_id = ?
`
var tokEnc fleet.VPPTokenDB
var tokTeams []struct {
TeamID *uint `db:"team_id"`
NullTeam fleet.NullTeamType `db:"null_team_type"`
Name string `db:"name"`
}
if err := sqlx.GetContext(ctx, ds.reader(ctx), &tokEnc, stmt, tokenID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ctxerr.Wrap(ctx, notFound("VPPToken"), "selecting vpp token from db")
}
return nil, ctxerr.Wrap(ctx, err, "selecting vpp token from db")
}
tokDec, err := decrypt([]byte(tokEnc.Token), ds.serverPrivateKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "decrypting vpp token with serverPrivateKey")
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &tokTeams, stmtTeams, tokenID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting vpp token teams from db")
}
tok := &fleet.VPPTokenDB{
ID: tokEnc.ID,
OrgName: tokEnc.OrgName,
Location: tokEnc.Location,
RenewDate: tokEnc.RenewDate,
Token: string(tokDec),
}
if tokTeams == nil {
// Not assigned, no need to loop over teams
return tok, nil
}
TEAMLOOP:
for _, team := range tokTeams {
switch team.NullTeam {
case fleet.NullTeamAllTeams:
// This should only be possible if there are no other teams
// Make sure something array is non-nil
if len(tokTeams) != 1 {
return nil, ctxerr.Errorf(ctx, "team \"%s\" belongs to All teams, and %d other team(s)", tok.OrgName, len(tokTeams)-1)
}
tok.Teams = []fleet.TeamTuple{}
break TEAMLOOP
case fleet.NullTeamNoTeam:
tok.Teams = append(tok.Teams, fleet.TeamTuple{
ID: 0,
Name: fleet.TeamNameNoTeam,
})
case fleet.NullTeamNone:
// Regular team
tok.Teams = append(tok.Teams, fleet.TeamTuple{
ID: *team.TeamID,
Name: team.Name,
})
}
}
return tok, nil
}
func (ds *Datastore) UpdateVPPTokenTeams(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
stmtTeamName := `SELECT name FROM teams WHERE id = ?`
stmtRemove := `DELETE FROM vpp_token_teams WHERE vpp_token_id = ?`
stmtInsert := `
INSERT INTO
vpp_token_teams (
vpp_token_id,
team_id,
null_team_type
) VALUES `
stmtValues := `(?, ?, ?)`
// Delete all apps, and associated policy automations, associated with a token if we change its team
stmtRemovePolicyAutomations := `UPDATE policies p
JOIN vpp_apps_teams vat ON vat.id = p.vpp_apps_teams_id AND vat.vpp_token_id = ?
SET vpp_apps_teams_id = NULL`
stmtDeleteApps := `DELETE FROM vpp_apps_teams WHERE vpp_token_id = ? %s`
var teamsFilter string
if len(teams) > 0 {
teamsFilter = "AND global_or_team_id NOT IN (?)"
}
stmtDeleteApps = fmt.Sprintf(stmtDeleteApps, teamsFilter)
var values string
var args []any
// No DB constraint for null_team_type, if no team or all teams
// comes up we have to check it in go
var nullTeamCheck fleet.NullTeamType
if len(teams) > 0 {
for _, team := range teams {
team := team
if values == "" {
values = stmtValues
} else {
values = strings.Join([]string{values, stmtValues}, ",")
}
var teamptr *uint
nullTeam := fleet.NullTeamNone
if team != 0 {
// Regular team
teamptr = &team
} else {
// NoTeam team
nullTeam = fleet.NullTeamNoTeam
nullTeamCheck = fleet.NullTeamNoTeam
}
args = append(args, id, teamptr, nullTeam)
}
} else if teams != nil {
// Empty but not nil, All Teams!
values = stmtValues
args = append(args, id, nil, fleet.NullTeamAllTeams)
nullTeamCheck = fleet.NullTeamAllTeams
}
stmtInsertFull := stmtInsert + values
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// NOTE This is not optimal, and has the potential to
// introduce race conditions. Ideally we would insert and
// check the constraints in a single query.
if err := checkVPPNullTeam(ctx, tx, &id, nullTeamCheck); err != nil {
return ctxerr.Wrap(ctx, err, "vpp token null team check")
}
if _, err := tx.ExecContext(ctx, stmtRemovePolicyAutomations, id); err != nil {
return ctxerr.Wrap(ctx, err, "deleting old vpp team apps policy automations")
}
delArgs := []any{id}
if len(teams) > 0 {
inStmt, inArgs, err := sqlx.In(stmtDeleteApps, id, teams)
if err != nil {
return ctxerr.Wrap(ctx, err, "building IN statement for deleting old vpp apps teams associations")
}
stmtDeleteApps = inStmt
delArgs = inArgs
}
if _, err := tx.ExecContext(ctx, stmtDeleteApps, delArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "deleting old vpp team apps associations")
}
if _, err := tx.ExecContext(ctx, stmtRemove, id); err != nil {
return ctxerr.Wrap(ctx, err, "removing old vpp team associations")
}
if len(args) > 0 {
if _, err := tx.ExecContext(ctx, stmtInsertFull, args...); err != nil {
if isChildForeignKeyError(err) {
return foreignKey("team", fmt.Sprintf("(team_id)=(%v)", values))
}
return ctxerr.Wrap(ctx, err, "updating vpp token team")
}
}
return nil
})
if err != nil {
var mysqlErr *mysql.MySQLError
// https://dev.mysql.com/doc/mysql-errors/8.4/en/server-error-reference.html#error_er_dup_entry
if errors.As(err, &mysqlErr) && IsDuplicate(err) {
var dupeTeamID uint
var dupeTeamName string
_, _ = fmt.Sscanf(mysqlErr.Message, "Duplicate entry '%d' for", &dupeTeamID)
if err := sqlx.GetContext(ctx, ds.reader(ctx), &dupeTeamName, stmtTeamName, dupeTeamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting team name for vpp token conflict error")
}
return nil, ctxerr.Wrap(ctx, fleet.ErrVPPTokenTeamConstraint{Name: dupeTeamName, ID: &dupeTeamID})
}
return nil, ctxerr.Wrap(ctx, err, "modifying vpp token team associations")
}
return ds.GetVPPToken(ctx, id)
}
func (ds *Datastore) DeleteVPPToken(ctx context.Context, tokenID uint) error {
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, `UPDATE policies p
JOIN vpp_apps_teams vat ON vat.id = p.vpp_apps_teams_id AND vat.vpp_token_id = ?
SET vpp_apps_teams_id = NULL`, tokenID)
if err != nil {
return ctxerr.Wrap(ctx, err, "removing policy automations associated with vpp token")
}
_, err = tx.ExecContext(ctx, `DELETE FROM vpp_apps_teams WHERE vpp_token_id = ?`, tokenID)
if err != nil {
return ctxerr.Wrap(ctx, err, "removing vpp apps associated with vpp token")
}
_, err = tx.ExecContext(ctx, `DELETE FROM vpp_tokens WHERE id = ?`, tokenID)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting vpp token")
}
return nil
})
}
func (ds *Datastore) ListVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
// linter false positive on the word "token" (gosec G101)
//nolint:gosec
stmtTokens := `
SELECT
id,
organization_name,
location,
renew_at,
token
FROM
vpp_tokens v
`
stmtTeams := `
SELECT
vt.id,
vt.vpp_token_id,
vt.team_id,
vt.null_team_type,
COALESCE(t.name, '') AS name
FROM
vpp_token_teams vt
LEFT OUTER JOIN
teams t
ON vt.team_id = t.id
`
var tokEncs []fleet.VPPTokenDB
var teams []struct {
ID string `db:"id"`
VPPTokenID uint `db:"vpp_token_id"`
TeamID *uint `db:"team_id"`
TeamName string `db:"name"`
NullTeam fleet.NullTeamType `db:"null_team_type"`
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &tokEncs, stmtTokens); err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting vpp tokens from db")
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &teams, stmtTeams); err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting vpp token teams from db")
}
tokens := map[uint]*fleet.VPPTokenDB{}
for _, tokEnc := range tokEncs {
tokDec, err := decrypt([]byte(tokEnc.Token), ds.serverPrivateKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "decrypting vpp token with serverPrivateKey")
}
tokens[tokEnc.ID] = &fleet.VPPTokenDB{
ID: tokEnc.ID,
OrgName: tokEnc.OrgName,
Location: tokEnc.Location,
RenewDate: tokEnc.RenewDate,
Token: string(tokDec),
}
}
for _, team := range teams {
token := tokens[team.VPPTokenID]
if token.Teams != nil && len(token.Teams) == 0 {
// Token was already assigned to All Teams, we should not
// see it again in a loop
return nil, fmt.Errorf("vpp token \"%s\" has been assigned to All teams, and another team", token.OrgName)
}
switch team.NullTeam {
case fleet.NullTeamAllTeams:
// All teams, there should be no other teams.
// Make sure array is non-nil
if token.Teams != nil {
// This team has already been assigned something, this
// should not happen
return nil, fmt.Errorf("vpp token \"%s\" has been asssigned to All teams, and another team", token.OrgName)
}
token.Teams = []fleet.TeamTuple{}
case fleet.NullTeamNoTeam:
token.Teams = append(token.Teams, fleet.TeamTuple{ID: 0, Name: fleet.TeamNameNoTeam})
case fleet.NullTeamNone:
// Regular team
token.Teams = append(token.Teams, fleet.TeamTuple{ID: *team.TeamID, Name: team.TeamName})
}
}
var outTokens []*fleet.VPPTokenDB
for _, token := range tokens {
outTokens = append(outTokens, token)
}
slices.SortFunc(outTokens, func(a, b *fleet.VPPTokenDB) int {
return cmp.Compare(a.OrgName, b.OrgName)
})
return outTokens, nil
}
func (ds *Datastore) GetVPPTokenByTeamID(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
stmtTeam := `
SELECT
v.id,
v.organization_name,
v.location,
v.renew_at,
v.token
FROM
vpp_token_teams vt
INNER JOIN
vpp_tokens v
ON vt.vpp_token_id = v.id
WHERE
vt.team_id = ?
`
stmtTeamNames := `
SELECT
vt.team_id,
vt.null_team_type,
COALESCE(t.name, '') AS name
FROM
vpp_token_teams vt
LEFT OUTER JOIN
teams t
ON t.id = vt.team_id
WHERE
vt.vpp_token_id = ?
`
stmtNullTeam := `
SELECT
v.id,
v.organization_name,
v.location,
v.renew_at,
v.token
FROM
vpp_tokens v
INNER JOIN
vpp_token_teams vt
ON v.id = vt.vpp_token_id
WHERE
vt.team_id IS NULL
AND
vt.null_team_type = ?
`
var tokEnc fleet.VPPTokenDB
var tokTeams []struct {
TeamID *uint `db:"team_id"`
NullTeam fleet.NullTeamType `db:"null_team_type"`
Name string `db:"name"`
}
var err error
if teamID != nil && *teamID != 0 {
err = sqlx.GetContext(ctx, ds.reader(ctx), &tokEnc, stmtTeam, teamID)
} else {
err = sqlx.GetContext(ctx, ds.reader(ctx), &tokEnc, stmtNullTeam, fleet.NullTeamNoTeam)
}
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
if err := sqlx.GetContext(ctx, ds.reader(ctx), &tokEnc, stmtNullTeam, fleet.NullTeamAllTeams); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ctxerr.Wrap(ctx, notFound("VPPToken"), "retrieving vpp token by team")
}
return nil, ctxerr.Wrap(ctx, err, "retrieving vpp token by team")
}
} else {
return nil, ctxerr.Wrap(ctx, err, "retrieving vpp token by team")
}
}
tokDec, err := decrypt([]byte(tokEnc.Token), ds.serverPrivateKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "decrypting vpp token with serverPrivateKey")
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &tokTeams, stmtTeamNames, tokEnc.ID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "retrieving vpp token team information")
}
tok := &fleet.VPPTokenDB{
ID: tokEnc.ID,
OrgName: tokEnc.OrgName,
Location: tokEnc.Location,
RenewDate: tokEnc.RenewDate,
Token: string(tokDec),
}
if tokTeams == nil {
// Not assigned, no need to loop over teams
return tok, nil
}
TEAMLOOP:
for _, team := range tokTeams {
switch team.NullTeam {
case fleet.NullTeamAllTeams:
// This should only be possible if there are no other teams
// Make sure something array is non-nil
if len(tokTeams) != 1 {
return nil, ctxerr.Errorf(ctx, "team \"%s\" belongs to All teams, and %d other team(s)", tok.OrgName, len(tokTeams)-1)
}
tok.Teams = []fleet.TeamTuple{}
break TEAMLOOP
case fleet.NullTeamNoTeam:
tok.Teams = append(tok.Teams, fleet.TeamTuple{
ID: 0,
Name: fleet.TeamNameNoTeam,
})
case fleet.NullTeamNone:
// Regular team
tok.Teams = append(tok.Teams, fleet.TeamTuple{
ID: *team.TeamID,
Name: team.Name,
})
}
}
return tok, nil
}
func checkVPPNullTeam(ctx context.Context, tx sqlx.ExtContext, currentID *uint, nullTeam fleet.NullTeamType) error {
nullTeamStmt := `SELECT vpp_token_id FROM vpp_token_teams WHERE null_team_type = ?`
anyTeamStmt := `SELECT vpp_token_id FROM vpp_token_teams WHERE (null_team_type = 'allteams' OR null_team_type = 'noteam' OR team_id IS NOT NULL) AND vpp_token_id != ?`
if nullTeam == fleet.NullTeamAllTeams {
var ids []uint
if err := sqlx.SelectContext(ctx, tx, &ids, anyTeamStmt, *currentID); err != nil {
return ctxerr.Wrap(ctx, err, "scanning row in check vpp token null team")
}
// Only blocks assignment if another token is already assigned to one or more teams.
// Allows the current token to switch from teams to "all teams" freely
if len(ids) > 0 {
return ctxerr.Wrap(ctx, errors.New("Cannot assign token to All teams, other teams have tokens"))
}
}
var id uint
allTeamsFound := true
if err := sqlx.GetContext(ctx, tx, &id, nullTeamStmt, fleet.NullTeamAllTeams); err != nil {
if errors.Is(err, sql.ErrNoRows) {
allTeamsFound = false
} else {
return ctxerr.Wrap(ctx, err, "scanning row in check vpp token null team")
}
}
if allTeamsFound && currentID != nil && *currentID != id {
return ctxerr.Wrap(ctx, fleet.ErrVPPTokenTeamConstraint{Name: fleet.ReservedNameAllTeams})
}
if nullTeam != fleet.NullTeamNone {
var id uint
if err := sqlx.GetContext(ctx, tx, &id, nullTeamStmt, nullTeam); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return ctxerr.Wrap(ctx, err, "scanning row in check vpp token null team")
}
if currentID == nil || *currentID != id {
return ctxerr.Wrap(ctx, fleet.ErrVPPTokenTeamConstraint{Name: nullTeam.PrettyName()})
}
}
return nil
}
func (ds *Datastore) GetIncludedHostIDMapForVPPApp(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error) {
return ds.getIncludedHostIDMapForSoftware(ctx, ds.writer(ctx), vppAppTeamID, softwareTypeVPP)
}
func (ds *Datastore) GetIncludedHostIDMapForVPPAppTx(ctx context.Context, tx sqlx.ExtContext, vppAppTeamID uint) (map[uint]struct{}, error) {
return ds.getIncludedHostIDMapForSoftware(ctx, tx, vppAppTeamID, softwareTypeVPP)
}
func (ds *Datastore) GetExcludedHostIDMapForVPPApp(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error) {
return ds.getExcludedHostIDMapForSoftware(ctx, vppAppTeamID, softwareTypeVPP)
}
func (ds *Datastore) GetAllVPPApps(ctx context.Context) ([]*fleet.VPPApp, error) {
query := `
SELECT
adam_id,
title_id,
bundle_identifier,
icon_url,
name,
latest_version,
platform
FROM vpp_apps WHERE platform IN (?)`
query, args, err := sqlx.In(query, fleet.ApplePlatforms)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get all vpp apps: building query")
}
var apps []*fleet.VPPApp
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &apps, query, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting all VPP apps")
}
return apps, nil
}
func (ds *Datastore) GetUnverifiedVPPInstallsForHost(ctx context.Context, hostUUID string) ([]*fleet.HostVPPSoftwareInstall, error) {
stmt := `
SELECT
hvsi.host_id AS host_id,
hvsi.command_uuid AS command_uuid,
hvsi.host_id AS host_id,
ncr.updated_at AS ack_at,
ncr.status AS install_command_status,
va.bundle_identifier AS bundle_identifier,
va.latest_version AS expected_version,
hvsi.retry_count AS retry_count
FROM nano_command_results ncr
JOIN host_vpp_software_installs hvsi ON hvsi.command_uuid = ncr.command_uuid
JOIN vpp_apps va ON va.adam_id = hvsi.adam_id AND va.platform = hvsi.platform
WHERE ncr.id = ?
AND ncr.status = 'Acknowledged'
AND hvsi.verification_at IS NULL
AND hvsi.verification_failed_at IS NULL
`
var result []*fleet.HostVPPSoftwareInstall
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &result, stmt, hostUUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get unverified VPP installs for host")
}
return result, nil
}
func (s softwareType) getInstallMappingTableName() string {
tableNames := map[softwareType]string{
softwareTypeInHouseApp: "host_in_house_software_installs",
softwareTypeVPP: "host_vpp_software_installs",
}
return tableNames[s]
}
func (ds *Datastore) AssociateMDMInstallToVerificationUUID(ctx context.Context, installUUID, verifyCommandUUID, hostUUID string) error {
stmt := `
UPDATE %s
SET verification_command_uuid = ?
WHERE command_uuid = ?
`
hostCmdStmt := `
INSERT INTO host_mdm_commands
(host_id, command_type)
VALUES ((SELECT id FROM hosts WHERE uuid = ?), ?)
`
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
var rowsAffected int64
r, err := tx.ExecContext(ctx, fmt.Sprintf(stmt, softwareTypeVPP.getInstallMappingTableName()), verifyCommandUUID, installUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "update vpp install verification command")
}
count, _ := r.RowsAffected()
rowsAffected += count
r, err = tx.ExecContext(ctx, fmt.Sprintf(stmt, softwareTypeInHouseApp.getInstallMappingTableName()), verifyCommandUUID, installUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "update in-house app install verification command")
}
count, _ = r.RowsAffected()
rowsAffected += count
if rowsAffected == 0 {
// There's a bug somewhere
return ctxerr.WrapWithData(ctx, err, "no MDM install attempts found with given uuid", map[string]any{"install_command_uuid": installUUID, "verify_command_uuid": verifyCommandUUID, "host_uuid": hostUUID})
}
if _, err := tx.ExecContext(ctx, hostCmdStmt, hostUUID, fleet.VerifySoftwareInstallVPPPrefix); err != nil {
return ctxerr.Wrap(ctx, err, "insert verify host mdm command")
}
return nil
})
}
func (ds *Datastore) ReplaceVPPInstallVerificationUUID(ctx context.Context, oldVerifyUUID, verifyCommandUUID string) error {
stmt := `
UPDATE host_vpp_software_installs
SET verification_command_uuid = ?
WHERE verification_command_uuid = ?
`
if _, err := ds.writer(ctx).ExecContext(ctx, stmt, verifyCommandUUID, oldVerifyUUID); err != nil {
return ctxerr.Wrap(ctx, err, "update vpp install verification command")
}
return nil
}
func (ds *Datastore) SetVPPInstallAsVerified(ctx context.Context, hostID uint, installUUID, verificationUUID string) error {
stmt := `
UPDATE host_vpp_software_installs
SET verification_at = CURRENT_TIMESTAMP(6),
verification_command_uuid = ?
WHERE command_uuid = ?
`
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
if _, err := tx.ExecContext(ctx, stmt, verificationUUID, installUUID); err != nil {
return ctxerr.Wrap(ctx, err, "set vpp install as verified")
}
if _, err := ds.activateNextUpcomingActivity(ctx, tx, hostID, installUUID); err != nil {
return ctxerr.Wrap(ctx, err, "activate next activity from VPP app install verify")
}
return nil
})
}
func (ds *Datastore) SetVPPInstallAsFailed(ctx context.Context, hostID uint, installUUID, verificationUUID string) error {
stmt := `
UPDATE host_vpp_software_installs
SET verification_failed_at = CURRENT_TIMESTAMP(6),
verification_command_uuid = ?
WHERE command_uuid = ?
`
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
if _, err := tx.ExecContext(ctx, stmt, verificationUUID, installUUID); err != nil {
return ctxerr.Wrap(ctx, err, "set vpp install as failed")
}
if _, err := ds.activateNextUpcomingActivity(ctx, tx, hostID, installUUID); err != nil {
return ctxerr.Wrap(ctx, err, "activate next activity from VPP app install failed")
}
return nil
})
}
func (ds *Datastore) MarkAllPendingAppleVPPAndInHouseInstallsAsFailed(ctx context.Context, jobName string) error {
// this is called on an Apple-only event, when APNS cert is deleted, so it should only
// affect Apple platforms. Currently, VPP installs in the upcoming queue are Apple only,
// but those in host_vpp_software_installs could be Android as well.
clearVPPUpcomingActivitiesStmt := `
DELETE ua FROM
upcoming_activities ua
JOIN
host_vpp_software_installs hvsi ON hvsi.command_uuid = ua.execution_id
WHERE ua.activity_type = ? AND hvsi.verification_failed_at IS NULL
AND hvsi.verification_at IS NULL AND hvsi.platform != 'android'
`
clearInHouseUpcomingActivitiesStmt := `
DELETE ua FROM
upcoming_activities ua
JOIN
host_in_house_software_installs hihs ON hihs.command_uuid = ua.execution_id
WHERE ua.activity_type = ? AND hihs.verification_failed_at IS NULL AND hihs.verification_at IS NULL
`
installVPPFailStmt := `
UPDATE host_vpp_software_installs
SET verification_failed_at = CURRENT_TIMESTAMP(6)
WHERE verification_failed_at IS NULL AND verification_at IS NULL AND platform != 'android'
`
installInHouseFailStmt := `
UPDATE host_in_house_software_installs
SET verification_failed_at = CURRENT_TIMESTAMP(6)
WHERE verification_failed_at IS NULL AND verification_at IS NULL
`
deletePendingJobsStmt := `
DELETE FROM jobs
WHERE name = ?
AND state = ?
`
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
if _, err := tx.ExecContext(ctx, clearVPPUpcomingActivitiesStmt, "vpp_app_install"); err != nil {
return ctxerr.Wrap(ctx, err, "clear apple vpp install upcoming activities")
}
if _, err := tx.ExecContext(ctx, installVPPFailStmt); err != nil {
return ctxerr.Wrap(ctx, err, "set all apple vpp install as failed")
}
if _, err := tx.ExecContext(ctx, clearInHouseUpcomingActivitiesStmt, "in_house_app_install"); err != nil {
return ctxerr.Wrap(ctx, err, "clear apple in-house install upcoming activities")
}
if _, err := tx.ExecContext(ctx, installInHouseFailStmt); err != nil {
return ctxerr.Wrap(ctx, err, "set all apple in-house install as failed")
}
if _, err := tx.ExecContext(ctx, deletePendingJobsStmt, jobName, fleet.JobStateQueued); err != nil {
return ctxerr.Wrap(ctx, err, "delete apple pending jobs")
}
return nil
})
}
func (ds *Datastore) MarkAllPendingAndroidVPPInstallsAsFailed(ctx context.Context) error {
// this is called on an Android-only event, when the enterprise is deleted
// (Android MDM turned off globally), so it should only affect Android
// platforms. Currently, only VPP installs in host_vpp_software_installs
// can be Android.
installVPPFailStmt := `
UPDATE host_vpp_software_installs
SET verification_failed_at = CURRENT_TIMESTAMP(6)
WHERE verification_failed_at IS NULL AND verification_at IS NULL AND platform = 'android'
`
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
if _, err := tx.ExecContext(ctx, installVPPFailStmt); err != nil {
return ctxerr.Wrap(ctx, err, "set all android vpp install as failed")
}
return nil
})
}
func (ds *Datastore) MarkAllPendingVPPInstallsAsFailedForAndroidHost(ctx context.Context, hostID uint) (users []*fleet.User, activities []fleet.ActivityDetails, err error) {
return ds.markAllPendingVPPInstallsAsFailedForHost(ctx, ds.writer(ctx), hostID, "android", softwareTypeVPP)
}
func (ds *Datastore) markAllPendingVPPInstallsAsFailedForHost(ctx context.Context, tx sqlx.ExtContext,
hostID uint, hostPlatform string, softwareType softwareType,
) (users []*fleet.User, activities []fleet.ActivityDetails, err error) {
var tableName string
switch softwareType {
case softwareTypeInHouseApp:
tableName = "host_in_house_software_installs"
case softwareTypeVPP:
tableName = "host_vpp_software_installs"
default:
return nil, nil, ctxerr.New(ctx, fmt.Sprintf("softwareType %s not supported", softwareType))
}
const loadFailedCmdsStmt = `
SELECT
command_uuid
FROM
%s
WHERE
verification_failed_at IS NULL
AND verification_at IS NULL
AND host_id = ?
AND canceled = 0
`
var failedCmds []string
if err := sqlx.SelectContext(ctx, tx, &failedCmds, fmt.Sprintf(loadFailedCmdsStmt, tableName), hostID); err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "load pending vpp install commands for host")
}
const installFailStmt = `
UPDATE %s
SET verification_failed_at = CURRENT_TIMESTAMP(6)
WHERE
verification_failed_at IS NULL
AND verification_at IS NULL
AND host_id = ?
`
if _, err := tx.ExecContext(ctx, fmt.Sprintf(installFailStmt, tableName), hostID); err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "set all vpp install as failed")
}
// We want to clear this table out, because otherwise we'll stop future installs from verifying.
// This is currently ios/ipados-only, but harmless on other platforms and less error-prone than
// adding a platform check that could become outdated.
const deleteHostMDMCommandStmt = `DELETE FROM host_mdm_commands WHERE host_id = ? AND command_type = ?`
if _, err := tx.ExecContext(ctx, deleteHostMDMCommandStmt, hostID, fleet.VerifySoftwareInstallVPPPrefix); err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "delete pending host mdm command records")
}
for _, cmd := range failedCmds {
// NOTE: I don't think we need to check/set the from setup experience
// field for Apple, as it should not be possible to turn MDM off during setup
// experience (host is not released).
var (
user *fleet.User
actAppStore *fleet.ActivityInstalledAppStoreApp
actInHouse *fleet.ActivityTypeInstalledSoftware
err error
)
switch {
case hostPlatform == "android":
user, actAppStore, err = ds.getPastActivityDataForAndroidVPPAppInstallDB(ctx, tx, cmd, fleet.SoftwareInstallFailed)
case softwareType == softwareTypeVPP:
user, actAppStore, err = ds.getPastActivityDataForVPPAppInstallDB(ctx, tx, &mdm.CommandResults{CommandUUID: cmd, Status: fleet.MDMAppleStatusError})
case softwareType == softwareTypeInHouseApp:
user, actInHouse, err = ds.getPastActivityDataForInHouseAppInstallDB(ctx, tx, &mdm.CommandResults{CommandUUID: cmd, Status: fleet.MDMAppleStatusError})
}
if err != nil {
if fleet.IsNotFound(err) {
// shouldn't happen, but no need to fail
continue
}
return nil, nil, ctxerr.Wrap(ctx, err, "get past activity data for vpp app install")
}
// user may be nil if fleet-initiated activity, but the activity itself indicates
// if a new entry must be made, since users and activities must match in length
// only one activity should be created per command
if actAppStore != nil {
if hostPlatform == "android" {
// currently, android installs are always during setup experience
actAppStore.FromSetupExperience = true
}
users = append(users, user)
activities = append(activities, actAppStore)
} else if actInHouse != nil {
users = append(users, user)
activities = append(activities, actInHouse)
}
}
return users, activities, nil
}
func (ds *Datastore) GetAndroidAppsInScopeForHost(ctx context.Context, hostID uint) (applicationIDs []string, err error) {
stmt := `
SELECT
installable_id
FROM (
-- no labels
SELECT
0 AS count_installer_labels,
0 AS count_host_labels,
0 AS count_host_updated_after_labels,
vpp_apps_teams.adam_id AS installable_id
FROM vpp_apps_teams
JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id
LEFT JOIN vpp_app_team_labels ON vpp_app_team_labels.vpp_app_team_id = vpp_apps_teams.id
WHERE vpp_app_team_labels.id IS NULL AND vpp_apps_teams.platform = 'android'
UNION
-- include any
SELECT
COUNT(*) AS count_installer_labels,
COUNT(lm.label_id) AS count_host_labels,
0 AS count_host_updated_after_labels,
vpp_apps_teams.adam_id AS installable_id
FROM
vpp_app_team_labels vatl
LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id
JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id
LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id AND lm.host_id = ?
WHERE vatl.exclude = 0 AND vatl.require_all = 0 AND vpp_apps_teams.platform = 'android'
GROUP BY installable_id
HAVING
count_installer_labels > 0
AND count_host_labels > 0
UNION
-- exclude any, ignore software that depends on labels created
-- _after_ the label_updated_at timestamp of the host (because
-- we don't have results for that label yet, the host may or may
-- not be a member).
SELECT
COUNT(*) AS count_installer_labels,
COUNT(lm.label_id) AS count_host_labels,
SUM(
CASE WHEN lbl.created_at IS NOT NULL
AND lbl.label_membership_type = 0
AND(
SELECT
label_updated_at FROM hosts
WHERE
id = ?) >= lbl.created_at THEN
1
WHEN lbl.created_at IS NOT NULL
AND lbl.label_membership_type = 1 THEN
1
ELSE
0
END) AS count_host_updated_after_labels,
vpp_apps_teams.adam_id AS installable_id
FROM
vpp_app_team_labels vatl
LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id
JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id
LEFT OUTER JOIN labels lbl ON lbl.id = vatl.label_id
LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id AND lm.host_id = ?
WHERE vatl.exclude = 1 AND vatl.require_all = 0 AND vpp_apps_teams.platform = 'android'
GROUP BY installable_id
HAVING
count_installer_labels > 0
AND count_installer_labels = count_host_updated_after_labels
AND count_host_labels = 0
UNION
-- include all
SELECT
COUNT(*) AS count_installer_labels,
COUNT(lm.label_id) AS count_host_labels,
0 AS count_host_updated_after_labels,
vpp_apps_teams.adam_id AS installable_id
FROM
vpp_app_team_labels vatl
LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id
JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id
LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id AND lm.host_id = ?
WHERE vatl.exclude = 0 AND vatl.require_all = 1 AND vpp_apps_teams.platform = 'android'
GROUP BY installable_id
HAVING
count_installer_labels > 0
AND count_host_labels = count_installer_labels
) t
`
err = sqlx.SelectContext(ctx, ds.reader(ctx), &applicationIDs, stmt, hostID, hostID, hostID, hostID, hostID, hostID, hostID, hostID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get in android apps in scope for host")
}
return applicationIDs, err
}
func (ds *Datastore) GetVPPAppsToInstallDuringSetupExperience(ctx context.Context, teamID *uint, platform string) ([]string, error) {
stmt := `
SELECT
adam_id
FROM
vpp_apps_teams vat
WHERE
vat.global_or_team_id = ? AND
vat.platform = ? AND
vat.install_during_setup = 1
`
var tmID uint
if teamID != nil {
tmID = *teamID
}
var ids []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &ids, stmt, tmID, platform); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get VPP apps to install during setup experience")
}
return ids, nil
}
type appStoreAppChanges struct {
Any bool
Categories bool
Labels bool
InstallDuringSetup bool
DisplayName bool
Configuration bool
}
func (ds *Datastore) hasAppStoreAppChanged(ctx context.Context, teamID *uint, incomingApp fleet.VPPAppTeam, existingApp fleet.VPPAppTeam, isExistingApp bool) (appStoreAppChanges, error) {
var categoriesChanged, labelsChanged, installDuringSetupChanged, displayNameChanged, configurationChanged bool
if isExistingApp {
existingLabels, err := ds.getExistingLabels(ctx, incomingApp.AppTeamID)
if err != nil {
return appStoreAppChanges{}, ctxerr.Wrap(ctx, err, "getting existing labels for vpp app")
}
labelsChanged = !existingLabels.Equal(incomingApp.ValidatedLabels)
existingCatIDs, err := ds.getVPPAppTeamCategoryIDs(ctx, incomingApp.AppTeamID)
if err != nil {
return appStoreAppChanges{}, ctxerr.Wrap(ctx, err, "getting existing categories for vpp app")
}
categoriesChanged = !slices.Equal(existingCatIDs, incomingApp.CategoryIDs)
existingDisplayName := ptr.ValOrZero(existingApp.DisplayName)
incomingDisplayName := ptr.ValOrZero(incomingApp.DisplayName)
displayNameChanged = existingDisplayName != incomingDisplayName
if incomingApp.Platform == fleet.AndroidPlatform {
configurationChanged, err = ds.HasAndroidAppConfigurationChanged(ctx, existingApp.AdamID, ptr.ValOrZero(teamID), incomingApp.Configuration)
if err != nil {
return appStoreAppChanges{}, ctxerr.Wrap(ctx, err, "getting existing configuration for android app")
}
// Set configuration to empty if it exists and is provided as null
if configurationChanged && len(incomingApp.Configuration) == 0 {
incomingApp.Configuration = json.RawMessage("{}")
}
}
installDuringSetupChanged = incomingApp.InstallDuringSetup != nil &&
existingApp.InstallDuringSetup != nil &&
*incomingApp.InstallDuringSetup != *existingApp.InstallDuringSetup
}
// New app added with setup experience enabled
if !isExistingApp && incomingApp.InstallDuringSetup != nil && *incomingApp.InstallDuringSetup {
installDuringSetupChanged = true
}
if !isExistingApp || existingApp.SelfService != incomingApp.SelfService || labelsChanged ||
categoriesChanged || displayNameChanged || configurationChanged || installDuringSetupChanged {
return appStoreAppChanges{true, categoriesChanged, labelsChanged, installDuringSetupChanged, displayNameChanged, configurationChanged}, nil
}
return appStoreAppChanges{}, nil
}
func (ds *Datastore) IsAutoUpdateVPPInstall(ctx context.Context, commandUUID string) (bool, error) {
stmt := `
SELECT COUNT(*) > 0
FROM upcoming_activities
WHERE execution_id = ?
AND activity_type = 'vpp_app_install'
AND JSON_EXTRACT(payload, '$.from_auto_update') = 1
`
var isAutoUpdate bool
if err := sqlx.GetContext(ctx, ds.reader(ctx), &isAutoUpdate, stmt, commandUUID); err != nil {
return false, ctxerr.Wrap(ctx, err, "checking if vpp install is from auto update")
}
return isAutoUpdate, nil
}
func (ds *Datastore) checkSoftwareConflictsForVPPApp(ctx context.Context, tx sqlx.QueryerContext, teamID *uint, teamName string, appID fleet.VPPAppID) error {
if appID.Platform == fleet.MacOSPlatform {
exists, conflictingTitle, err := ds.checkConflictingSoftwareInstallerForVPPApp(ctx, tx, teamID, appID)
if err != nil {
return ctxerr.Wrap(ctx, err, "checking if software installer exists")
}
if exists {
return ctxerr.Wrap(ctx, fleet.ConflictError{
Message: fmt.Sprintf(fleet.CantAddSoftwareConflictMessage,
conflictingTitle, teamName)}, "vpp app conflicts with existing software installer")
}
}
// check if the vpp app conflicts with an existing in-house app
if appID.Platform == fleet.IOSPlatform || appID.Platform == fleet.IPadOSPlatform {
exists, conflictingTitle, err := ds.checkInHouseAppExistsForAdamID(ctx, tx, teamID, appID)
if err != nil {
return ctxerr.Wrap(ctx, err, "check if in-house app exists")
}
if exists {
return ctxerr.Wrap(ctx, fleet.ConflictError{
Message: fmt.Sprintf(fleet.CantAddSoftwareConflictMessage, conflictingTitle, teamName),
}, "vpp app conflicts with existing in-house app")
}
}
return nil
}
func (ds *Datastore) GetHostVPPInstallByCommandUUID(ctx context.Context, commandUUID string) (*fleet.HostVPPSoftwareInstallLite, error) {
const stmt = `
SELECT
command_uuid,
host_id,
retry_count
FROM host_vpp_software_installs
WHERE command_uuid = ?`
var vppSoftwareInstalls []fleet.HostVPPSoftwareInstallLite
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &vppSoftwareInstalls, stmt, commandUUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get VPP host install")
}
// This should not happen, since the command_uuid is unique, but we put it here as a safe guard.
if len(vppSoftwareInstalls) > 1 {
return nil, ctxerr.Wrap(ctx, errors.New("multiple VPP software installs found for command UUID"), "get VPP install by command UUID")
}
if len(vppSoftwareInstalls) == 0 {
return nil, nil // Not an error
}
return &vppSoftwareInstalls[0], nil
}
func (ds *Datastore) RetryVPPInstall(ctx context.Context, vppInstall *fleet.HostVPPSoftwareInstallLite) error {
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
newCommandUUID := uuid.New().String()
if _, err := tx.ExecContext(ctx, `UPDATE host_vpp_software_installs
SET command_uuid = ?, retry_count = retry_count + 1, verification_at = NULL, verification_failed_at = NULL
WHERE command_uuid = ? AND host_id = ?`, newCommandUUID, vppInstall.InstallCommandUUID, vppInstall.HostID); err != nil {
return ctxerr.Wrap(ctx, err, "updating vpp install with new command uuid")
}
if _, err := tx.ExecContext(ctx, `UPDATE upcoming_activities SET execution_id = ? WHERE execution_id = ? AND host_id = ?`, newCommandUUID, vppInstall.InstallCommandUUID, vppInstall.HostID); err != nil {
return ctxerr.Wrap(ctx, err, "updating upcoming activities with new execution id")
}
return ds.nanoEnqueueVPPInstall(ctx, tx, vppInstall.HostID, []string{newCommandUUID})
})
}
func (ds *Datastore) nanoEnqueueVPPInstall(ctx context.Context, tx sqlx.ExtContext, hostID uint, execIDs []string) error {
// sanity-check that there's something to activate
if len(execIDs) == 0 {
return nil
}
const getHostUUIDStmt = `
SELECT
uuid, platform
FROM
hosts
WHERE
id = ?
`
// get the host uuid, requires for the nano tables
var hostData struct {
UUID string `db:"uuid"`
Platform string `db:"platform"`
}
if err := sqlx.GetContext(ctx, tx, &hostData, getHostUUIDStmt, hostID); err != nil {
return ctxerr.Wrap(ctx, err, "get host uuid")
}
const insCmdStmt = `
INSERT INTO
nano_commands
(command_uuid, request_type, command, subtype)
SELECT
ua.execution_id,
'InstallApplication',
CONCAT(:raw_cmd_part1, vaua.adam_id, :raw_cmd_part2, ua.execution_id, :raw_cmd_part3),
:subtype
FROM
upcoming_activities ua
INNER JOIN vpp_app_upcoming_activities vaua
ON vaua.upcoming_activity_id = ua.id
WHERE
ua.host_id = :host_id AND
ua.execution_id IN (:execution_ids)
`
rawCmdPart1 := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>InstallAsManaged</key>
<true/>
<key>ManagementFlags</key>
<integer>%d</integer>
<key>ChangeManagementState</key>
<string>Managed</string>
<key>InstallAsManaged</key>
<true />
<key>Options</key>
<dict>
<key>PurchaseMethod</key>
<integer>1</integer>
</dict>
<key>RequestType</key>
<string>InstallApplication</string>
<key>iTunesStoreID</key>
<integer>`
const rawCmdPart2 = `</integer>
</dict>
<key>CommandUUID</key>
<string>`
const rawCmdPart3 = `</string>
</dict>
</plist>`
// Set management flags based on platform
if fleet.IsAppleMobilePlatform(hostData.Platform) {
// Remove app upon MDM removal
rawCmdPart1 = fmt.Sprintf(rawCmdPart1, 1) // Mobile devices use management flag 1
} else {
// Keep app upon MDM removal
rawCmdPart1 = fmt.Sprintf(rawCmdPart1, 0) // macOS devices use management flag 0
}
// insert the nano command
namedArgs := map[string]any{
"raw_cmd_part1": rawCmdPart1,
"raw_cmd_part2": rawCmdPart2,
"raw_cmd_part3": rawCmdPart3,
"subtype": mdm.CommandSubtypeNone,
"host_id": hostID,
"execution_ids": execIDs,
}
stmt, args, err := sqlx.Named(insCmdStmt, namedArgs)
if err != nil {
return ctxerr.Wrap(ctx, err, "prepare insert nano commands")
}
stmt, args, err = sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "expand IN arguments to insert nano commands")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "insert nano commands")
}
const insNanoQueueStmt = `
INSERT INTO
nano_enrollment_queue
(id, command_uuid, created_at)
SELECT
?,
execution_id,
created_at -- force same timestamp to keep ordering
FROM
upcoming_activities
WHERE
host_id = ? AND
execution_id IN (?)
ORDER BY
priority DESC, created_at ASC
`
// enqueue the nano command in the nano queue
stmt, args, err = sqlx.In(insNanoQueueStmt, hostData.UUID, hostID, execIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "prepare insert nano queue")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "insert nano queue")
}
// best-effort APNs push notification to the host, not critical because we
// have a cron job that will retry for hosts with pending MDM commands.
if ds.pusher != nil {
if _, err := ds.pusher.Push(ctx, []string{hostData.UUID}); err != nil {
ds.logger.ErrorContext(ctx, "failed to send push notification", "err", err, "hostID", hostID, "hostUUID", hostData.UUID)
}
}
return nil
}
func (ds *Datastore) CheckAndroidWebAppNameExistsOnTeam(ctx context.Context, teamID *uint, name string, excludeAdamID string) (bool, error) {
globalOrTeamID := ptr.ValOrZero(teamID)
var exists bool
err := sqlx.GetContext(ctx, ds.reader(ctx), &exists, `
SELECT EXISTS(
SELECT 1 FROM vpp_apps va
JOIN vpp_apps_teams vat ON va.adam_id = vat.adam_id AND va.platform = vat.platform
WHERE va.name = ?
AND va.adam_id LIKE ?
AND va.adam_id != ?
AND va.platform = 'android'
AND vat.global_or_team_id = ?
)`, name, fleet.AndroidWebAppPrefix+"%", excludeAdamID, globalOrTeamID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "checking android web app name exists on team")
}
return exists, nil
}