Skip to content
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

Add RemoveApplication method #25078

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
af02192
Add RemoveApplication method
jspenc72 Jan 1, 2025
e3aae6b
Execute RemoveApplication for vpp applications on iOS and iPadOS
jspenc72 Jan 2, 2025
66cac37
Update HostSoftwareTableConfig.tsx
jspenc72 Jan 2, 2025
a2126a8
Add RemoveApplication method
jspenc72 Jan 1, 2025
b5a29e2
Execute RemoveApplication for vpp applications on iOS and iPadOS
jspenc72 Jan 2, 2025
a82a062
Update HostSoftwareTableConfig.tsx
jspenc72 Jan 2, 2025
dffcaf3
Merge branch 'jspenc72/25077-apple-mdm-remove-application' of https:/…
jspenc72 Jan 2, 2025
48ffe26
Merge branch 'main' into jspenc72/25077-apple-mdm-remove-application
jspenc72 Jan 2, 2025
0cd19f1
Merge branch 'main' into jspenc72/25077-apple-mdm-remove-application
jspenc72 Jan 6, 2025
dd83fde
Create custom activity for uninstalling vpp app
jspenc72 Jan 8, 2025
46006ac
Merge branch 'jspenc72/25077-apple-mdm-remove-application' of https:/…
jspenc72 Jan 8, 2025
afb69f8
Merge branch 'main' into jspenc72/25077-apple-mdm-remove-application
jspenc72 Jan 9, 2025
a8cd2fa
Update software_installers.go
jspenc72 Jan 9, 2025
2e0247b
Merge branch 'jspenc72/25077-apple-mdm-remove-application' of https:/…
jspenc72 Jan 9, 2025
579275e
create migration to AddUninstallToHostVppInstalls
jspenc72 Jan 9, 2025
4be5db0
Add uninstall column to host_vpp_software_installs
jspenc72 Jan 10, 2025
5511b94
Merge branch 'main' into jspenc72/25077-apple-mdm-remove-application
jspenc72 Jan 10, 2025
fc49615
gofmt
jspenc72 Jan 10, 2025
cda7462
Fix timestamp for new db migrations
jspenc72 Jan 10, 2025
ebe7fb4
Update schema.sql
jspenc72 Jan 10, 2025
5ba8df2
gofmt
jspenc72 Jan 10, 2025
0297fa0
run make generate-doc
jspenc72 Jan 10, 2025
5482ae1
add changefile
jspenc72 Jan 10, 2025
41f153f
Merge branch 'main' into jspenc72/25077-apple-mdm-remove-application
jspenc72 Jan 13, 2025
65242eb
Merge branch 'main' into jspenc72/25077-apple-mdm-remove-application
jspenc72 Feb 11, 2025
11b7b70
Update migration timestamp.
jspenc72 Feb 11, 2025
1939f52
Update schema.sql
jspenc72 Feb 11, 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
1 change: 1 addition & 0 deletions changes/25078-add-mdm-remove-application
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* add ability to uninstall VPP applications via mdm RemoveApplication command.
28 changes: 22 additions & 6 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,7 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
return updatedInstaller, nil
}

func (svc *Service) validateEmbeddedSecretsOnScript(ctx context.Context, scriptName string, script *string,
argErr *fleet.InvalidArgumentError,
) *fleet.InvalidArgumentError {
func (svc *Service) validateEmbeddedSecretsOnScript(ctx context.Context, scriptName string, script *string, argErr *fleet.InvalidArgumentError) *fleet.InvalidArgumentError {
if script != nil {
if errScript := svc.ds.ValidateEmbeddedSecrets(ctx, []string{*script}); errScript != nil {
if argErr != nil {
Expand Down Expand Up @@ -1189,10 +1187,14 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof
return ctxerr.Wrap(ctx, err, "get host")
}

if host.OrbitNodeKey == nil || *host.OrbitNodeKey == "" {
// fleetd is required to install software so if the host is enrolled via plain osquery we return an error
platform := host.FleetPlatform()
mobileAppleDevice := fleet.AppleDevicePlatform(platform) == fleet.IOSPlatform || fleet.AppleDevicePlatform(platform) == fleet.IPadOSPlatform

if !mobileAppleDevice && (host.OrbitNodeKey == nil || *host.OrbitNodeKey == "") {
// fleetd is required to install software so if the host is
// enrolled via plain osquery we return an error
svc.authz.SkipAuthorization(ctx)
return fleet.NewUserMessageError(errors.New("host does not have fleetd installed"), http.StatusUnprocessableEntity)
return fleet.NewUserMessageError(errors.New("Host doesn't have fleetd installed"), http.StatusUnprocessableEntity)
}

// If scripts are disabled (according to the last detail query), we return an error.
Expand All @@ -1206,6 +1208,20 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{HostTeamID: host.TeamID}, fleet.ActionWrite); err != nil {
return err
}
// Try Apple MDM uninstallation for iOS and iPadOS devices
if mobileAppleDevice {
vppApp, err := svc.ds.GetVPPAppByTeamAndTitleID(ctx, host.TeamID, softwareTitleID)
if err != nil {
return ctxerr.Wrap(ctx, err, "finding VPP app for title")
}

cmdUUID := uuid.NewString()
err = svc.mdmAppleCommander.RemoveApplication(ctx, []string{host.UUID}, cmdUUID, vppApp.BundleIdentifier)
if err != nil {
return ctxerr.Wrapf(ctx, err, "sending command to uninstall VPP %s application to host with serial %s", vppApp.BundleIdentifier, host.HardwareSerial)
}
return nil
}

installer, err := svc.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, host.TeamID, softwareTitleID, false)
if err != nil {
Expand Down
26 changes: 26 additions & 0 deletions frontend/pages/hosts/details/cards/Software/HostSoftware.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,28 @@ const HostSoftware = ({
[id, renderFlash, refetchSoftware]
);

const uninstallHostVPPApp = useCallback(
async (softwareId: number) => {
setSoftwareIdActionPending(softwareId);
try {
await hostAPI.uninstallHostSoftwarePackage(id as number, softwareId);
renderFlash(
"success",
<>
The remove application command has been sent. Uninstallion will
occur when the device comes online. To see details, click
<b>Details &gt; Activity</b>.
</>
);
} catch (e) {
renderFlash("error", getUninstallErrorMessage(e));
}
setSoftwareIdActionPending(null);
refetchSoftware();
},
[id, renderFlash, refetchSoftware]
);

const onSelectAction = useCallback(
(software: IHostSoftware, action: string) => {
switch (action) {
Expand All @@ -235,6 +257,9 @@ const HostSoftware = ({
case "showDetails":
onShowSoftwareDetails?.(software);
break;
case "remove":
uninstallHostVPPApp(software.id);
break;
default:
break;
}
Expand All @@ -243,6 +268,7 @@ const HostSoftware = ({
installHostSoftwarePackage,
onShowSoftwareDetails,
uninstallHostSoftwarePackage,
uninstallHostVPPApp,
]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,11 @@ export const generateActions = ({
}

if (app_store_app) {
// remove uninstall for VPP apps
// remove `uninstall` for VPP apps and replace with `remove`
// iOS and iPadOS RemoveApplication MDM commands should be handled differently as the removal is not
// not executed via fleetd.
actions.splice(indexUninstallAction, 1);
actions.push({ value: "remove", label: "Uninstall", disabled: false });

if (!hostMDMEnrolled) {
actions[indexInstallAction].disabled = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20250109074626, Down_20250109074626)
}

func Up_20250109074626(tx *sql.Tx) error {

if !columnExists(tx, "host_vpp_software_installs", "uninstall") {
if _, err := tx.Exec("ALTER TABLE host_vpp_software_installs ADD COLUMN uninstall TINYINT NOT NULL DEFAULT 0"); err != nil {
return fmt.Errorf("failed to add removed to host_vpp_software_installs: %w", err)
}
}

return nil
}

func Down_20250109074626(tx *sql.Tx) error {
return nil
}
5 changes: 3 additions & 2 deletions server/datastore/mysql/schema.sql

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions server/datastore/mysql/vpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,97 @@ VALUES
return nil
}

func (ds *Datastore) GetActivityDataForVPPAppUnInstall(ctx context.Context, commandResults *mdm.CommandResults) (*fleet.User, *fleet.ActivityUnInstalledAppStoreApp, 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
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
WHERE
hvsi.command_uuid = :command_uuid
`

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"`
}

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.ActivityUnInstalledAppStoreApp{
HostID: res.HostID,
HostDisplayName: res.HostDisplayName,
SoftwareTitle: res.SoftwareTitle,
AppStoreID: res.AppStoreID,
CommandUUID: res.CommandUUID,
SelfService: res.SelfService,
Status: status,
}

return user, act, nil
}

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
Expand Down
36 changes: 36 additions & 0 deletions server/fleet/activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -1678,6 +1678,42 @@ type ActivityTypeUninstalledSoftware struct {
Status string `json:"status"`
}

// Using ActivityUnInstalledAppStoreApp as a reference to capture the AppStoreID, CommandUUID, Status & SelfService
type ActivityUnInstalledAppStoreApp struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
SoftwareTitle string `json:"software_title"`
AppStoreID string `json:"app_store_id"`
CommandUUID string `json:"command_uuid"`
Status string `json:"status,omitempty"`
SelfService bool `json:"self_service"`
}

func (a ActivityUnInstalledAppStoreApp) HostIDs() []uint {
return []uint{a.HostID}
}

func (a ActivityUnInstalledAppStoreApp) ActivityName() string {
return "uninstalled_app_store_app"
}

func (a ActivityUnInstalledAppStoreApp) Documentation() (string, string, string) {
return "Generated when an App Store app is uninstalled from a device.", `This activity contains the following fields:
- host_id: ID of the host on which the app was installed.
- self_service: App removal was initiated by device owner.
- host_display_name: Display name of the host.
- 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 uninstall the app.`, `{
"host_id": 42,
"self_service": false,
"host_display_name": "Anna's MacBook Pro",
"software_title": "Logic Pro",
"app_store_id": "1234567",
"command_uuid": "98765432-1234-1234-1234-1234567890ab"
}`
}

func (a ActivityTypeUninstalledSoftware) ActivityName() string {
return "uninstalled_software"
}
Expand Down
1 change: 1 addition & 0 deletions server/fleet/apple_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type MDMAppleCommandIssuer interface {
EraseDevice(ctx context.Context, host *Host, uuid string) error
InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error
InstallApplication(ctx context.Context, hostUUIDs []string, uuid string, adamID string) error
RemoveApplication(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error
DeviceConfigured(ctx context.Context, hostUUID, cmdUUID string) error
}

Expand Down
1 change: 1 addition & 0 deletions server/fleet/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -1834,6 +1834,7 @@ type Datastore interface {

InsertHostVPPSoftwareInstall(ctx context.Context, hostID uint, appID VPPAppID, commandUUID, associatedEventID string, selfService bool, policyID *uint) error
GetPastActivityDataForVPPAppInstall(ctx context.Context, commandResults *mdm.CommandResults) (*User, *ActivityInstalledAppStoreApp, error)
GetActivityDataForVPPAppUnInstall(ctx context.Context, commandResults *mdm.CommandResults) (*User, *ActivityUnInstalledAppStoreApp, error)

GetVPPTokenByLocation(ctx context.Context, loc string) (*VPPTokenDB, error)

Expand Down
37 changes: 35 additions & 2 deletions server/mdm/apple/commander.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,28 @@ func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, host *fleet.Host,
return nil
}

func (svc *MDMAppleCommander) RemoveApplication(ctx context.Context, hostUUIDs []string, uuid string, identifier string) error {
raw := fmt.Sprintf(`<?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>RequestType</key>
<string>RemoveApplication</string>
<key>Identifier</key>
<string>%s</string>
</dict>
<key>CommandUUID</key>
<string>%s</string>
</dict>
</plist>`, identifier, uuid)
fmt.Println("Executing RemoveApplication command")
fmt.Println(raw)

return svc.EnqueueCommand(ctx, hostUUIDs, raw)
}

func (svc *MDMAppleCommander) InstallApplication(ctx context.Context, hostUUIDs []string, uuid string, adamID string) error {
raw := fmt.Sprintf(`<?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">
Expand All @@ -187,6 +209,15 @@ func (svc *MDMAppleCommander) InstallApplication(ctx context.Context, hostUUIDs
</dict>
<key>RequestType</key>
<string>InstallApplication</string>
<key>Attributes</key>
<dict>
<key>Removable</key>
<true />
</dict>
<key>InstallAsManaged</key>
<true/>
<key>ChangeManagementState</key>
<string>Managed</string>
<key>iTunesStoreID</key>
<integer>%s</integer>
</dict>
Expand Down Expand Up @@ -378,7 +409,8 @@ func (svc *MDMAppleCommander) EnqueueCommand(ctx context.Context, hostUUIDs []st
}

func (svc *MDMAppleCommander) enqueueAndNotify(ctx context.Context, hostUUIDs []string, cmd *mdm.Command,
subtype mdm.CommandSubtype) error {
subtype mdm.CommandSubtype,
) error {
if _, err := svc.storage.EnqueueCommand(ctx, hostUUIDs,
&mdm.CommandWithSubtype{Command: *cmd, Subtype: subtype}); err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing command")
Expand All @@ -393,7 +425,8 @@ func (svc *MDMAppleCommander) enqueueAndNotify(ctx context.Context, hostUUIDs []
// EnqueueCommandInstallProfileWithSecrets is a special case of EnqueueCommand that does not expand secret variables.
// Secret variables are expanded when the command is sent to the device, and secrets are never stored in the database unencrypted.
func (svc *MDMAppleCommander) EnqueueCommandInstallProfileWithSecrets(ctx context.Context, hostUUIDs []string,
rawCommand mobileconfig.Mobileconfig, commandUUID string) error {
rawCommand mobileconfig.Mobileconfig, commandUUID string,
) error {
cmd := &mdm.Command{
CommandUUID: commandUUID,
Raw: []byte(rawCommand),
Expand Down
12 changes: 12 additions & 0 deletions server/mock/datastore_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,8 @@ type InsertHostVPPSoftwareInstallFunc func(ctx context.Context, hostID uint, app

type GetPastActivityDataForVPPAppInstallFunc func(ctx context.Context, commandResults *mdm.CommandResults) (*fleet.User, *fleet.ActivityInstalledAppStoreApp, error)

type GetActivityDataForVPPAppUnInstallFunc func(ctx context.Context, commandResults *mdm.CommandResults) (*fleet.User, *fleet.ActivityUnInstalledAppStoreApp, error)

type GetVPPTokenByLocationFunc func(ctx context.Context, loc string) (*fleet.VPPTokenDB, error)

type GetIncludedHostIDMapForVPPAppFunc func(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error)
Expand Down Expand Up @@ -2930,6 +2932,9 @@ type DataStore struct {
GetPastActivityDataForVPPAppInstallFunc GetPastActivityDataForVPPAppInstallFunc
GetPastActivityDataForVPPAppInstallFuncInvoked bool

GetActivityDataForVPPAppUnInstallFunc GetActivityDataForVPPAppUnInstallFunc
GetActivityDataForVPPAppUnInstallFuncInvoked bool

GetVPPTokenByLocationFunc GetVPPTokenByLocationFunc
GetVPPTokenByLocationFuncInvoked bool

Expand Down Expand Up @@ -7010,6 +7015,13 @@ func (s *DataStore) GetPastActivityDataForVPPAppInstall(ctx context.Context, com
return s.GetPastActivityDataForVPPAppInstallFunc(ctx, commandResults)
}

func (s *DataStore) GetActivityDataForVPPAppUnInstall(ctx context.Context, commandResults *mdm.CommandResults) (*fleet.User, *fleet.ActivityUnInstalledAppStoreApp, error) {
s.mu.Lock()
s.GetActivityDataForVPPAppUnInstallFuncInvoked = true
s.mu.Unlock()
return s.GetActivityDataForVPPAppUnInstallFunc(ctx, commandResults)
}

func (s *DataStore) GetVPPTokenByLocation(ctx context.Context, loc string) (*fleet.VPPTokenDB, error) {
s.mu.Lock()
s.GetVPPTokenByLocationFuncInvoked = true
Expand Down
Loading
Loading