Skip to content

Add VPP policy automation support to backend #25154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
33e6aea
Add changes file, migration for VPP policy automation (#23529)
iansltx Jan 3, 2025
3e52848
Start building policy VPP association load/save
iansltx Jan 3, 2025
ce558a2
Implement VPP automation visibility on policy object, implement orbit…
iansltx Jan 4, 2025
621f6f4
Allow multi-platform and "macos" filtering on software titles, includ…
iansltx Jan 5, 2025
fa1c4a8
Fix linting issues
iansltx Jan 5, 2025
68a54e1
Fix core test issues
iansltx Jan 5, 2025
c4a5f6a
Fix integration tests
iansltx Jan 5, 2025
97400f4
Switch to vpp_apps_teams_id fkey on policies table rather than join t…
iansltx Jan 6, 2025
a0ca81b
Drop VPPAppsTeamsID from comparison for equality check in VPP metadat…
iansltx Jan 9, 2025
9c311f7
Merge branch 'main' into 23115-vpp-policy-be, renumber migration
iansltx Jan 9, 2025
08b566f
Error when deleting a single policy-associated VPP app, clear policy …
iansltx Jan 9, 2025
c7dae46
Add tests other than integration test, fix bugs found in automated te…
iansltx Jan 10, 2025
5ffa085
Lint fix
iansltx Jan 10, 2025
92bdd78
Merge branch 'main' into 23115-vpp-policy-be
iansltx Jan 10, 2025
3306f85
Update audit logs docs
iansltx Jan 10, 2025
a384c97
Add more tests, fix bugs found by tests
iansltx Jan 10, 2025
ac7f860
Add remaining data store tests for VPP automation
iansltx Jan 10, 2025
82b06cf
Merge branch 'main' into 23115-vpp-policy-be
iansltx Jan 10, 2025
35258f3
Revise error message on get VPP title info from VPP apps team ID
iansltx Jan 10, 2025
6e3e01f
Note new API capability in changes file
iansltx Jan 10, 2025
28763a5
Tweak changes file
iansltx Jan 10, 2025
d7775b3
Add integration test, fix bugs found by integration test, don't queue…
iansltx Jan 13, 2025
87b3219
Fix lint issues
iansltx Jan 13, 2025
06eedd0
Add more test coverage, fix variable naming
iansltx Jan 13, 2025
6028d05
Lint fix
iansltx Jan 13, 2025
1ce8ab5
Merge branch 'main' into 23115-vpp-policy-be
iansltx Jan 13, 2025
ec21ec3
Add missing automatic install policies on single software title endpo…
iansltx Jan 13, 2025
4900d95
Add test for individual software title response to ensure automatic i…
iansltx Jan 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes/23115-vpp-policy
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Added ability to install VPP apps on policy failure
* Allowed filtering titles by "any of these platforms" in `GET /api/v1/fleet/software/titles`
7 changes: 6 additions & 1 deletion docs/Contributing/Audit-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,9 @@ This activity contains the following fields:
- software_title: Name of the App Store app.
- app_store_id: ID of the app on the Apple App Store.
- command_uuid: UUID of the MDM command used to install the app.
- policy_id: ID of the policy whose failure triggered the install. Null if no associated policy.
- policy_name: Name of the policy whose failure triggered the install. Null if no associated policy.


#### Example

Expand All @@ -1441,7 +1444,9 @@ This activity contains the following fields:
"host_display_name": "Anna's MacBook Pro",
"software_title": "Logic Pro",
"app_store_id": "1234567",
"command_uuid": "98765432-1234-1234-1234-1234567890ab"
"command_uuid": "98765432-1234-1234-1234-1234567890ab",
"policy_id": 123,
"policy_name": "[Install Software] Logic Pro"
}
```

Expand Down
2 changes: 2 additions & 0 deletions ee/server/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ func NewService(
MDMWindowsDisableOSUpdates: eeservice.mdmWindowsDisableOSUpdates,
MDMAppleEditedAppleOSUpdates: eeservice.mdmAppleEditedAppleOSUpdates,
SetupExperienceNextStep: eeservice.SetupExperienceNextStep,
GetVPPTokenIfCanInstallVPPApps: eeservice.GetVPPTokenIfCanInstallVPPApps,
InstallVPPAppPostValidation: eeservice.InstallVPPAppPostValidation,
})

return eeservice, nil
Expand Down
22 changes: 17 additions & 5 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1006,12 +1006,21 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
}

func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, appleDevice bool, selfService bool) (string, error) {
token, err := svc.GetVPPTokenIfCanInstallVPPApps(ctx, appleDevice, host)
if err != nil {
return "", err
}

return svc.InstallVPPAppPostValidation(ctx, host, vppApp, token, selfService, nil)
}

func (svc *Service) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDevice bool, host *fleet.Host) (string, error) {
if !appleDevice {
return "", &fleet.BadRequestError{
Message: "VPP apps can only be installed only on Apple hosts.",
InternalErr: ctxerr.NewWithData(
ctx, "invalid host platform for requested installer",
map[string]any{"host_id": host.ID, "team_id": host.TeamID, "title_id": vppApp.TitleID},
map[string]any{"host_id": host.ID, "team_id": host.TeamID},
),
}
}
Expand All @@ -1035,7 +1044,7 @@ func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host
Message: "Error: Couldn't install. To install App Store app, turn on MDM for this host.",
InternalErr: ctxerr.NewWithData(
ctx, "VPP install attempted on non-MDM host",
map[string]any{"host_id": host.ID, "team_id": host.TeamID, "title_id": vppApp.TitleID},
map[string]any{"host_id": host.ID, "team_id": host.TeamID},
),
}
}
Expand All @@ -1045,7 +1054,11 @@ func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host
return "", ctxerr.Wrap(ctx, err, "getting VPP token")
}

// at this moment, neither the UI or the back-end are prepared to
return token, nil
}

func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, token string, selfService bool, policyID *uint) (string, error) {
// at this moment, neither the UI nor the back-end are prepared to
// handle [asyncronous errors][1] on assignment, so before assigning a
// device to a license, we need to:
//
Expand Down Expand Up @@ -1106,7 +1119,6 @@ func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host
if err != nil {
return "", ctxerr.Wrapf(ctx, err, "associating asset with adamID %s to host %s", vppApp.AdamID, host.HardwareSerial)
}

}

// add command to install
Expand All @@ -1116,7 +1128,7 @@ func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host
return "", ctxerr.Wrapf(ctx, err, "sending command to install VPP %s application to host with serial %s", vppApp.AdamID, host.HardwareSerial)
}

err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, selfService)
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, selfService, policyID)
if err != nil {
return "", ctxerr.Wrapf(ctx, err, "inserting host vpp software install for host with serial %s and app with adamID %s", host.HardwareSerial, vppApp.AdamID)
}
Expand Down
5 changes: 4 additions & 1 deletion server/datastore/mysql/activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,13 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
hsi.host_id = :host_id AND
hsi.status = :software_status_uninstall_pending
`,
// list pending VPP installs
`
SELECT
hvsi.command_uuid AS uuid,
u.name AS name,
-- policies with automatic installers generate a host_vpp_software_installs with (user_id=NULL,self_service=0),
-- so we mark those as "Fleet"
IF(hvsi.user_id IS NULL AND NOT hvsi.self_service, 'Fleet', u.name) AS name,
u.id AS user_id,
u.gravatar_url as gravatar_url,
u.email as user_email,
Expand Down
4 changes: 2 additions & 2 deletions server/datastore/mysql/activities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {

// install the VPP app on h1
commander, _ := createMDMAppleCommanderAndStorage(t, ds)
err = ds.InsertHostVPPSoftwareInstall(ctx, h1.ID, vppApp.VPPAppID, vppCommand1, "event-id-1", false)
err = ds.InsertHostVPPSoftwareInstall(ctx, h1.ID, vppApp.VPPAppID, vppCommand1, "event-id-1", false, nil)
require.NoError(t, err)
err = commander.EnqueueCommand(
ctx,
Expand All @@ -463,7 +463,7 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
)
require.NoError(t, err)
// install the VPP app on h2, self-service
err = ds.InsertHostVPPSoftwareInstall(noUserCtx, h2.ID, vppApp.VPPAppID, vppCommand2, "event-id-2", true)
err = ds.InsertHostVPPSoftwareInstall(noUserCtx, h2.ID, vppApp.VPPAppID, vppCommand2, "event-id-2", true, nil)
require.NoError(t, err)
err = commander.EnqueueCommand(
ctx,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20250110205257, Down_20250110205257)
}

func Up_20250110205257(tx *sql.Tx) error {
if _, err := tx.Exec(`
ALTER TABLE policies
ADD COLUMN vpp_apps_teams_id INT UNSIGNED DEFAULT NULL,
ADD FOREIGN KEY fk_policies_vpp_apps_team_id (vpp_apps_teams_id) REFERENCES vpp_apps_teams (id);
`); err != nil {
return fmt.Errorf("failed to add vpp_apps_teams_id to policies: %w", err)
}

if _, err := tx.Exec(`
ALTER TABLE host_vpp_software_installs
ADD COLUMN policy_id INT UNSIGNED DEFAULT NULL,
ADD FOREIGN KEY fk_host_vpp_software_installs_policy_id (policy_id) REFERENCES policies (id) ON DELETE SET NULL
`); err != nil {
return fmt.Errorf("failed to add policy_id to host VPP software installs: %w", err)
}

return nil
}

func Down_20250110205257(tx *sql.Tx) error {
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package tables

import (
"testing"

"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/require"
)

func TestUp_20250110205257(t *testing.T) {
db := applyUpToPrev(t)

// Create user
u1 := execNoErrLastID(t, db, `INSERT INTO users (name, email, password, salt) VALUES (?, ?, ?, ?)`, "u1", "u1@b.c", "1234", "salt")

// insert a team
teamID := execNoErrLastID(t, db, `INSERT INTO teams (name) VALUES ("Foo")`)

// Create host
insertHostStmt := `
INSERT INTO hosts (
hostname, uuid, platform, osquery_version, os_version, build, platform_like, code_name,
cpu_type, cpu_subtype, cpu_brand, hardware_vendor, hardware_model, hardware_version,
hardware_serial, computer_name, team_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
hostName := "Dummy Hostname"
hostUUID := "12345678-1234-1234-1234-123456789012"
hostPlatform := "ios"
osqueryVer := "5.9.1"
osVersion := "Windows 10"
buildVersion := "10.0.19042.1234"
platformLike := "apple"
codeName := "20H2"
cpuType := "x86_64"
cpuSubtype := "x86_64"
cpuBrand := "Intel"
hwVendor := "Dell Inc."
hwModel := "OptiPlex 7090"
hwVersion := "1.0"
hwSerial := "ABCDEFGHIJ"
computerName := "DESKTOP-TEST"

hostID := execNoErrLastID(t, db, insertHostStmt, hostName, hostUUID, hostPlatform, osqueryVer,
osVersion, buildVersion, platformLike, codeName, cpuType, cpuSubtype, cpuBrand, hwVendor, hwModel, hwVersion, hwSerial, computerName, teamID)

// Create VPP app, token, and associated team
adamID := "a"
execNoErr(
t, db, `INSERT INTO vpp_apps (adam_id, platform) VALUES (?,?)`, adamID, hostPlatform,
)
vppTokenID := execNoErrLastID(t, db, `
INSERT INTO vpp_tokens (
organization_name,
location,
renew_at,
token
) VALUES
(?, ?, ?, ?)
`,
"org1", "loc1", "2030-01-01 10:10:10", "blob1",
)

vppAppsTeamsID := execNoErrLastID(
t,
db,
`INSERT INTO vpp_apps_teams (adam_id, platform, global_or_team_id, team_id, vpp_token_id) VALUES (?,?,?,?,?)`,
adamID,
fleet.IOSPlatform,
teamID,
teamID,
vppTokenID,
)

// Apply current migration.
applyNext(t, db)

// create a policy associated with a VPP apps teams record
policyID := execNoErrLastID(t, db, `INSERT INTO policies (name, query, description, team_id, vpp_apps_teams_id, checksum)
VALUES ('test_policy', "SELECT 1", "", ?, ?, "a123b123")`, teamID, vppAppsTeamsID)

// create a VPP install with the policy ID
hvsi1 := execNoErrLastID(t, db, `INSERT INTO host_vpp_software_installs (host_id, adam_id, platform, command_uuid, user_id, policy_id) VALUES (?,?,?,?,?, ?)`, hostID, adamID, hostPlatform, "command_uuid", u1, policyID)

// attempt to delete the VPP app; should error
_, err := db.Exec(`DELETE FROM vpp_apps_teams WHERE id = ?`, vppAppsTeamsID)
require.Error(t, err)

// delete the policy
execNoErr(t, db, `DELETE FROM policies WHERE id = ?`, policyID)

// confirm that the policy ID on the existing install is null
var retrievedPolicyID *uint
require.NoError(t, db.Get(&retrievedPolicyID, `SELECT policy_id FROM host_vpp_software_installs WHERE id = ?`, hvsi1))
require.Nil(t, retrievedPolicyID)

// attempt to delete the VPP app; should succeed
execNoErr(t, db, `DELETE FROM vpp_apps_teams WHERE id = ?`, vppAppsTeamsID)
}
Loading
Loading