fleet/server/datastore/mysql/vpp.go
Ian Littman 0d29f2bfc0
Add custom software icons (#32652)
For #29478, sans GitOps.

---------

Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Co-authored-by: Konstantin Sykulev <konst@sykulev.com>
2025-09-05 17:31:03 -05:00

1904 lines
57 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-kit/log/level"
"github.com/go-sql-driver/mysql"
"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 []fleet.SoftwareScopeLabel
for _, l := range labels {
if l.Exclude {
exclAny = append(exclAny, l)
} else {
inclAny = append(inclAny, l)
}
}
if len(inclAny) > 0 && len(exclAny) > 0 {
// there's a bug somewhere
level.Warn(ds.logger).Log("msg", "vpp app has both include and exclude labels", "vpp_apps_teams_id", app.VPPAppsTeamsID, "include", fmt.Sprintf("%v", inclAny), "exclude", fmt.Sprintf("%v", exclAny))
}
app.LabelsExcludeAny = exclAny
app.LabelsIncludeAny = inclAny
categories, err := ds.getCategoriesForVPPApp(ctx, app.VPPAppsTeamsID)
if err != nil {
return nil, err
}
app.Categories = categories
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
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
// TODO(sarah): do we need to handle host_deleted_at similar to GetSummaryHostSoftwareInstalls?
// Currently there is no host_deleted_at in host_vpp_software_installs, so
// not handling it as part of the unified queue work.
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
past AS (
SELECT
hvsi.host_id,
CASE
WHEN ncr.status = :mdm_status_acknowledged THEN
:software_status_installed
WHEN ncr.status = :mdm_status_error OR ncr.status = :mdm_status_format_error THEN
:software_status_failed
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
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 (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
}
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 []fleet.SoftwareScopeLabel
for _, l := range existingLabels {
if l.Exclude {
exclAny = append(exclAny, l)
} else {
inclAny = append(inclAny, l)
}
}
if len(inclAny) > 0 && len(exclAny) > 0 {
// there's a bug somewhere
return nil, ctxerr.New(ctx, "found both include and exclude labels 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
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, appFleets []fleet.VPPAppTeam) error {
existingApps, err := ds.GetAssignedVPPApps(ctx, teamID)
if err != nil {
return 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(appFleets) == 0 || appFleets[0].InstallDuringSetup != nil {
replacingInstallDuringSetup = true
}
var toAddApps []fleet.VPPAppTeam
var toRemoveApps []fleet.VPPAppID
for existingApp, appTeamInfo := range existingApps {
var found bool
for _, appFleet := range appFleets {
// 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 errDeleteInstallerInstalledDuringSetup
}
toRemoveApps = append(toRemoveApps, existingApp)
}
}
appsWithChangedLabels := make(map[uint]map[uint]struct{})
for _, appFleet := range appFleets {
// upsert it if it does not exist or labels or SelfService or InstallDuringSetup flags are changed
existingApp, isExistingApp := existingApps[appFleet.VPPAppID]
appFleet.AppTeamID = existingApp.AppTeamID
var labelsChanged, categoriesChanged bool
if isExistingApp {
existingLabels, err := ds.getExistingLabels(ctx, appFleet.AppTeamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting existing labels for vpp app")
}
labelsChanged = !existingLabels.Equal(appFleet.ValidatedLabels)
existingCatIDs, err := ds.getVPPAppTeamCategoryIDs(ctx, appFleet.AppTeamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting existing categories for vpp app")
}
categoriesChanged = !slices.Equal(existingCatIDs, appFleet.CategoryIDs)
}
// Get the hosts that are NOT in label scope currently (before the update happens)
if labelsChanged {
hostsNotInScope, err := ds.GetExcludedHostIDMapForVPPApp(ctx, appFleet.AppTeamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting hosts not in scope for vpp app")
}
appsWithChangedLabels[appFleet.AppTeamID] = hostsNotInScope
}
if !isExistingApp ||
existingApp.SelfService != appFleet.SelfService ||
labelsChanged ||
categoriesChanged ||
appFleet.InstallDuringSetup != nil &&
existingApp.InstallDuringSetup != nil &&
*appFleet.InstallDuringSetup != *existingApp.InstallDuringSetup {
toAddApps = append(toAddApps, appFleet)
}
}
var vppToken *fleet.VPPTokenDB
if len(appFleets) > 0 {
vppToken, err = ds.GetVPPTokenByTeamID(ctx, teamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "SetTeamVPPApps retrieve VPP token ID")
}
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
for _, toAdd := range toAddApps {
vppAppTeamID, err := insertVPPAppTeams(ctx, tx, toAdd, teamID, vppToken.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "SetTeamVPPApps inserting vpp app into team")
}
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 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
})
}
func (ds *Datastore) InsertVPPAppWithTeam(ctx context.Context, app *fleet.VPPApp, teamID *uint) (*fleet.VPPApp, error) {
vppToken, err := ds.GetVPPTokenByTeamID(ctx, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam unable to get VPP Token ID")
}
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, vppToken.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam insertVPPAppTeams transaction")
}
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
}
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) {
var tmID uint
if teamID != nil {
tmID = *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.team_id, va.title_id, vat.adam_id app_store_id, vat.platform
FROM vpp_apps_teams vat
JOIN vpp_apps va ON va.adam_id = vat.adam_id AND va.platform = vat.platform
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 IsDuplicate(err) {
err = &existsError{
Identifier: fmt.Sprintf("%s %s self_service: %v", appID.AdamID, appID.Platform, appID.SelfService),
TeamID: teamID,
ResourceType: "VPPAppID",
}
}
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")
}
_, err = tx.ExecContext(ctx, `DELETE FROM vpp_apps_teams WHERE adam_id = ? AND team_id = ? AND platform = ?`, appID.AdamID, teamID, appID.Platform)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting vpp app from team")
}
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"
default:
source = "apps"
}
selectStmt := `SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''`
selectArgs := []any{app.Name, source}
insertStmt := `INSERT INTO software_titles (name, source, browser) 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
switch source {
case "ios_apps", "ipados_apps":
selectStmt = `
SELECT id
FROM software_titles
WHERE (bundle_identifier = ? AND source = ?) OR (name = ? AND source = ? AND browser = '')
ORDER BY bundle_identifier = ? DESC
LIMIT 1`
selectArgs = []any{app.BundleIdentifier, source, app.Name, source, app.BundleIdentifier}
default:
selectStmt = `
SELECT id
FROM software_titles
WHERE bundle_identifier = ? AND additional_identifier = 0`
selectArgs = []any{app.BundleIdentifier}
}
insertStmt = `INSERT INTO software_titles (name, source, bundle_identifier, browser) VALUES (?, ?, ?, '')`
insertArgs = append(insertArgs, 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 VPP 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 errDeleteInstallerWithAssociatedPolicy
}
}
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))
}
return nil
}
func (ds *Datastore) GetTitleInfoFromVPPAppsTeamsID(ctx context.Context, vppAppsTeamsID uint) (*fleet.PolicySoftwareTitle, error) {
var info fleet.PolicySoftwareTitle
err := sqlx.GetContext(ctx, ds.reader(ctx), &info, `SELECT name, title_id FROM vpp_apps va
JOIN vpp_apps_teams vat ON vat.adam_id = va.adam_id AND vat.platform = va.platform AND vat.id = ?`, 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.AppleDevicePlatform, 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', ?,
'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,
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
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) GetPastActivityDataForVPPAppInstall(ctx context.Context, 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
FROM
host_vpp_software_installs hvsi
LEFT OUTER JOIN users u ON hvsi.user_id = u.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"`
}
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, ds.reader(ctx), &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:
case 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,
}
return user, act, 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_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`
var apps []*fleet.VPPApp
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &apps, query); 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
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 (ds *Datastore) AssociateVPPInstallToVerificationUUID(ctx context.Context, installUUID, verifyCommandUUID string) error {
stmt := `
UPDATE host_vpp_software_installs
SET verification_command_uuid = ?
WHERE command_uuid = ?
`
hostCmdStmt := `
INSERT INTO host_mdm_commands
(host_id, command_type)
VALUES ((SELECT host_id FROM host_vpp_software_installs WHERE command_uuid = ?), ?)
`
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
if _, err := tx.ExecContext(ctx, stmt, verifyCommandUUID, installUUID); err != nil {
return ctxerr.Wrap(ctx, err, "update vpp install verification command")
}
if _, err := tx.ExecContext(ctx, hostCmdStmt, installUUID, 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) MarkAllPendingVPPInstallsAsFailed(ctx context.Context, jobName string) error {
clearUpcomingActivitiesStmt := `
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
`
installFailStmt := `
UPDATE host_vpp_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, clearUpcomingActivitiesStmt, "vpp_app_install"); err != nil {
return ctxerr.Wrap(ctx, err, "clear vpp install upcoming activities")
}
if _, err := tx.ExecContext(ctx, installFailStmt); err != nil {
return ctxerr.Wrap(ctx, err, "set all vpp install as failed")
}
if _, err := tx.ExecContext(ctx, deletePendingJobsStmt, jobName, fleet.JobStateQueued); err != nil {
return ctxerr.Wrap(ctx, err, "delete pending jobs")
}
return nil
})
}
func (ds *Datastore) markAllPendingVPPInstallsAsFailedForHost(ctx context.Context, tx sqlx.ExtContext, hostID uint) error {
installFailStmt := `
UPDATE host_vpp_software_installs
SET verification_failed_at = CURRENT_TIMESTAMP(6)
WHERE
verification_failed_at IS NULL
AND verification_at IS NULL
AND host_id = ?
`
// We want to clear this table out, because otherwise we'll stop future installs from verifying.
deleteHostMDMCommandStmt := `DELETE FROM host_mdm_commands WHERE host_id = ? AND command_type = ?`
if _, err := tx.ExecContext(ctx, installFailStmt, hostID); err != nil {
return ctxerr.Wrap(ctx, err, "set all vpp install as failed")
}
if _, err := tx.ExecContext(ctx, deleteHostMDMCommandStmt, hostID, fleet.VerifySoftwareInstallVPPPrefix); err != nil {
return ctxerr.Wrap(ctx, err, "delete pending host mdm command records")
}
return nil
}