RAA: Support removing an android app from managed google play (self-service) on deletion (#39343)

This commit is contained in:
Martin Angers
2026-02-11 11:35:00 -05:00
committed by GitHub
parent 54036801f3
commit 2f20a0f2b9
13 changed files with 413 additions and 14 deletions
@@ -0,0 +1 @@
- Implemented uninstall of Android apps on the device (and removal from self-service in the managed Google Play store) when an app is removed from Fleet.
@@ -22,6 +22,7 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/dev_mode"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
@@ -151,6 +152,12 @@ func RunServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*http
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
}
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
return []fleet.VPPAppResponse{}, nil
}
ds.GetEnterpriseFunc = func(ctx context.Context) (*android.Enterprise, error) {
return nil, nil
}
var cachedDS fleet.Datastore
if len(opts) > 0 && opts[0].NoCacheDatastore {
cachedDS = ds
+26
View File
@@ -30,6 +30,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/worker"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/google/uuid"
@@ -802,10 +803,35 @@ func (svc *Service) deleteVPPApp(ctx context.Context, teamID *uint, meta *fleet.
return fleet.ErrNoContext
}
var androidHostsUUIDToPolicyID map[string]string
if meta.Platform == fleet.AndroidPlatform {
// if this is an Android app we're deleting, collect the host uuids that should have it removed
// (as we uninstall Android apps on delete). We can't do this in the worker as it will be too late,
// the vpp_apps_teams entry will have been deleted.
hosts, err := svc.ds.GetIncludedHostUUIDMapForAppStoreApp(ctx, meta.VPPAppsTeamsID)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete app store app: getting android hosts in scope")
}
androidHostsUUIDToPolicyID = hosts
}
if err := svc.ds.DeleteVPPAppFromTeam(ctx, teamID, meta.VPPAppID); err != nil {
return ctxerr.Wrap(ctx, err, "deleting VPP app")
}
// if this is an android app, remove the self-service app from the managed Google Play store
// and uninstall it from the hosts.
if meta.Platform == fleet.AndroidPlatform && len(androidHostsUUIDToPolicyID) > 0 {
enterprise, err := svc.ds.GetEnterprise(ctx)
if err != nil {
return &fleet.BadRequestError{Message: "Android MDM is not enabled", InternalErr: err}
}
err = worker.QueueMakeAndroidAppUnavailableJob(ctx, svc.ds, svc.logger, meta.VPPAppID.AdamID, androidHostsUUIDToPolicyID, enterprise.Name())
if err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing job to make android app unavailable")
}
}
var teamName *string
if teamID != nil && *teamID != 0 {
t, err := svc.ds.TeamLite(ctx, *teamID)
+30 -13
View File
@@ -18,7 +18,6 @@ import (
"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/android"
"github.com/fleetdm/fleet/v4/server/mdm/apple/apple_apps"
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
"github.com/fleetdm/fleet/v4/server/ptr"
@@ -264,11 +263,31 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
appStoreApps = append(appStoreApps, apps...)
}
var enterprise *android.Enterprise
if len(incomingAndroidApps) > 0 {
var err error
enterprise, err = svc.ds.GetEnterprise(ctx)
enterprise, err := svc.ds.GetEnterprise(ctx)
if err != nil && !fleet.IsNotFound(err) {
return nil, &fleet.BadRequestError{Message: "Android MDM is not enabled", InternalErr: err}
}
androidHostPoliciesToUpdate := map[string]string{}
if len(incomingAndroidApps) == 0 {
// get the currently available VPP apps, and the hosts that have them in scope,
// to update their set of apps to the empty set (and remove/uninstall the apps).
removedApps, err := svc.ds.GetVPPApps(ctx, teamID)
if err != nil {
return nil, err
}
for _, app := range removedApps {
if app.Platform == fleet.AndroidPlatform {
hostsInScope, err := svc.ds.GetIncludedHostUUIDMapForAppStoreApp(ctx, app.AppTeamID)
if err != nil {
return nil, err
}
maps.Copy(androidHostPoliciesToUpdate, hostsInScope)
}
}
} else {
if enterprise == nil {
return nil, &fleet.BadRequestError{Message: "Android MDM is not enabled", InternalErr: err}
}
@@ -387,16 +406,11 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
return nil, err // returned error already includes context that we could include here
}
if len(allPlatformApps) == 0 {
return []fleet.VPPAppResponse{}, nil
}
addedApps, err := svc.ds.GetVPPApps(ctx, teamID)
if err != nil {
return nil, err
}
policiesToUpdate := map[string]string{}
var appIDs []string
for _, app := range addedApps {
if app.Platform == fleet.AndroidPlatform {
@@ -405,13 +419,13 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
return nil, err
}
maps.Copy(policiesToUpdate, hostsInScope)
maps.Copy(androidHostPoliciesToUpdate, hostsInScope)
appIDs = append(appIDs, app.AppStoreID)
}
}
if len(policiesToUpdate) > 0 && enterprise != nil {
for hostUUID, policyID := range policiesToUpdate {
if len(androidHostPoliciesToUpdate) > 0 && enterprise != nil {
for hostUUID, policyID := range androidHostPoliciesToUpdate {
err := worker.QueueBulkSetAndroidAppsAvailableForHost(ctx, svc.ds, svc.logger, hostUUID, policyID, appIDs, enterprise.Name())
if err != nil {
return nil, ctxerr.WrapWithData(
@@ -435,6 +449,9 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
}
}
if len(addedApps) == 0 {
return []fleet.VPPAppResponse{}, nil
}
return addedApps, nil
}
+12
View File
@@ -39,6 +39,8 @@ type EnterprisesApplicationsFunc func(ctx context.Context, enterpriseName string
type EnterprisesPoliciesModifyPolicyApplicationsFunc func(ctx context.Context, policyName string, appPolicies []*androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error)
type EnterprisesPoliciesRemovePolicyApplicationsFunc func(ctx context.Context, policyName string, packageNames []string) (*androidmanagement.Policy, error)
type Client struct {
SignupURLsCreateFunc SignupURLsCreateFunc
SignupURLsCreateFuncInvoked bool
@@ -79,6 +81,9 @@ type Client struct {
EnterprisesPoliciesModifyPolicyApplicationsFunc EnterprisesPoliciesModifyPolicyApplicationsFunc
EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked bool
EnterprisesPoliciesRemovePolicyApplicationsFunc EnterprisesPoliciesRemovePolicyApplicationsFunc
EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked bool
mu sync.Mutex
}
@@ -172,3 +177,10 @@ func (p *Client) EnterprisesPoliciesModifyPolicyApplications(ctx context.Context
p.mu.Unlock()
return p.EnterprisesPoliciesModifyPolicyApplicationsFunc(ctx, policyName, appPolicies)
}
func (p *Client) EnterprisesPoliciesRemovePolicyApplications(ctx context.Context, policyName string, packageNames []string) (*androidmanagement.Policy, error) {
p.mu.Lock()
p.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked = true
p.mu.Unlock()
return p.EnterprisesPoliciesRemovePolicyApplicationsFunc(ctx, policyName, packageNames)
}
+1
View File
@@ -23,6 +23,7 @@ type Service interface {
EnterprisesApplications(ctx context.Context, enterpriseName, applicationID string) (*androidmanagement.Application, error)
AddAppsToAndroidPolicy(ctx context.Context, enterpriseName string, appPolicies []*androidmanagement.ApplicationPolicy, hostUUIDs map[string]string) (map[string]*MDMAndroidPolicyRequest, error)
RemoveAppsFromAndroidPolicy(ctx context.Context, enterpriseName string, packageNames []string, hostUUIDs map[string]string) (map[string]*MDMAndroidPolicyRequest, error)
// SetAppsForAndroidPolicy sets the available apps for the given hosts' Android MDM policy to the given list of apps.
// Note that unlike AddAppsToAndroidPolicy, this method replaces the existing app list with the given one, it is
// not additive/PATCH semantics.
@@ -64,6 +64,8 @@ type Client interface {
EnterprisesApplications(ctx context.Context, enterpriseName, packageName string) (*androidmanagement.Application, error)
EnterprisesPoliciesModifyPolicyApplications(ctx context.Context, policyName string, appPolicies []*androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error)
EnterprisesPoliciesRemovePolicyApplications(ctx context.Context, policyName string, packageNames []string) (*androidmanagement.Policy, error)
}
type EnterprisesCreateRequest struct {
@@ -377,3 +377,18 @@ func (g *GoogleClient) EnterprisesPoliciesModifyPolicyApplications(ctx context.C
}
return ret.Policy, nil
}
func (g *GoogleClient) EnterprisesPoliciesRemovePolicyApplications(ctx context.Context, policyName string, packageNames []string) (*androidmanagement.Policy, error) {
req := androidmanagement.RemovePolicyApplicationsRequest{
PackageNames: packageNames,
}
ret, err := g.mgmt.Enterprises.Policies.RemovePolicyApplications(policyName, &req).Context(ctx).Do()
switch {
case googleapi.IsNotModified(err):
g.logger.Log("msg", "Android application policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, ctxerr.Wrapf(ctx, err, "removing packages from application policy %s", policyName)
}
return ret.Policy, nil
}
@@ -341,3 +341,21 @@ func (p *ProxyClient) EnterprisesPoliciesModifyPolicyApplications(ctx context.Co
}
return ret.Policy, nil
}
func (p *ProxyClient) EnterprisesPoliciesRemovePolicyApplications(ctx context.Context, policyName string, packageNames []string) (*androidmanagement.Policy, error) {
req := androidmanagement.RemovePolicyApplicationsRequest{
PackageNames: packageNames,
}
call := p.mgmt.Enterprises.Policies.RemovePolicyApplications(policyName, &req).Context(ctx)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
ret, err := call.Do()
switch {
case googleapi.IsNotModified(err):
p.logger.Log("msg", "Android application policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, ctxerr.Wrapf(ctx, err, "removing packages from application policy %s", policyName)
}
return ret.Policy, nil
}
+16
View File
@@ -49,6 +49,22 @@ func newAndroidPolicyApplicationsRequest(policyID, policyName string, apps []*an
}, nil
}
func newAndroidPolicyRemoveApplicationsRequest(policyID, policyName string, packageNames []string) (*android.MDMAndroidPolicyRequest, error) {
req := androidmanagement.RemovePolicyApplicationsRequest{
PackageNames: packageNames,
}
b, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal remove policy applications to json: %w", err)
}
return &android.MDMAndroidPolicyRequest{
RequestName: policyName,
PolicyID: policyID,
Payload: b,
}, nil
}
func newAndroidPolicyRequest(policyID, policyName string, policy *androidmanagement.Policy, metadata map[string]string) (*android.MDMAndroidPolicyRequest, error) {
// save the payload with metadata about what setting comes from what profile
m := fleet.AndroidPolicyRequestPayload{
+24
View File
@@ -903,6 +903,30 @@ func (svc *Service) AddAppsToAndroidPolicy(ctx context.Context, enterpriseName s
return hostToPolicyRequest, errors.Join(errs...)
}
func (svc *Service) RemoveAppsFromAndroidPolicy(ctx context.Context, enterpriseName string, packageNames []string, hostUUIDs map[string]string) (map[string]*android.MDMAndroidPolicyRequest, error) {
var errs []error
hostToPolicyRequest := make(map[string]*android.MDMAndroidPolicyRequest, len(hostUUIDs))
for uuid, policyID := range hostUUIDs {
policyName := fmt.Sprintf("%s/policies/%s", enterpriseName, policyID)
policyRequest, err := newAndroidPolicyRemoveApplicationsRequest(policyID, policyName, packageNames)
if err != nil {
return nil, ctxerr.Wrapf(ctx, err, "prepare policy request %s", policyName)
}
policy, apiErr := svc.androidAPIClient.EnterprisesPoliciesRemovePolicyApplications(ctx, policyName, packageNames)
if _, err := recordAndroidRequestResult(ctx, svc.fleetDS, policyRequest, policy, nil, apiErr); err != nil {
return nil, ctxerr.Wrapf(ctx, err, "save android policy request for host %s", uuid)
}
if apiErr != nil {
errs = append(errs, ctxerr.Wrapf(ctx, apiErr, "google api: remove policy applications for host %s", uuid))
}
hostToPolicyRequest[uuid] = policyRequest
}
return hostToPolicyRequest, errors.Join(errs...)
}
// getFleetAgentPackageInfo returns the Fleet agent package name and SHA256 fingerprint.
// Returns empty strings if the package is not configured.
func (svc *Service) getFleetAgentPackageInfo() (packageName, sha256Fingerprint string) {
@@ -603,6 +603,10 @@ func (s *integrationMDMTestSuite) enableAndroidMDM(t *testing.T) string {
return &androidmanagement.Policy{}, nil
}
s.androidAPIClient.EnterprisesPoliciesRemovePolicyApplicationsFunc = func(ctx context.Context, policyName string, packageNames []string) (*androidmanagement.Policy, error) {
return &androidmanagement.Policy{}, nil
}
s.androidAPIClient.EnterprisesDevicesPatchFunc = func(ctx context.Context, deviceName string, device *androidmanagement.Device) (*androidmanagement.Device, error) {
return &androidmanagement.Device{}, nil
}
@@ -867,3 +871,219 @@ func (s *integrationMDMTestSuite) TestBatchAndroidApps() {
require.Equal(t, 3, count)
})
}
func (s *integrationMDMTestSuite) TestAndroidAppsUninstallOnDelete() {
ctx := context.Background()
t := s.T()
s.setSkipWorkerJobs(t)
s.setVPPTokenForTeam(0)
appConf, err := s.ds.AppConfig(ctx)
require.NoError(t, err)
appConf.MDM.AndroidEnabledAndConfigured = false
err = s.ds.SaveAppConfig(ctx, appConf)
require.NoError(t, err)
// create a team for the test host that will not be affected
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{Name: "test"}, http.StatusOK, &createTeamResp)
teamID := createTeamResp.Team.ID
s.enableAndroidMDM(t)
amapiConfig := struct {
AppIDsToNames map[string]string
EnterprisesPoliciesPatchValidator func(policyName string, policy *androidmanagement.Policy, opts androidmgmt.PoliciesPatchOpts)
}{
AppIDsToNames: map[string]string{},
EnterprisesPoliciesPatchValidator: func(policyName string, policy *androidmanagement.Policy, opts androidmgmt.PoliciesPatchOpts) {},
}
s.androidAPIClient.EnterprisesApplicationsFunc = func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
title := amapiConfig.AppIDsToNames[packageName]
return &androidmanagement.Application{IconUrl: "https://example.com/1.jpg", Title: title}, nil
}
s.androidAPIClient.EnterprisesPoliciesPatchFunc = func(ctx context.Context, policyName string, policy *androidmanagement.Policy, opts androidmgmt.PoliciesPatchOpts) (*androidmanagement.Policy, error) {
amapiConfig.EnterprisesPoliciesPatchValidator(policyName, policy, opts)
return &androidmanagement.Policy{}, nil
}
// add some Android apps
androidApps := make([]*fleet.VPPApp, 5)
titleIDs := make([]uint, len(androidApps))
for i := range androidApps {
androidApps[i] = &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{AdamID: "com.app" + fmt.Sprint(i), Platform: fleet.AndroidPlatform},
},
Name: "App" + fmt.Sprint(i),
BundleIdentifier: "com.app" + fmt.Sprint(i),
IconURL: "https://example.com/images/" + fmt.Sprint(i),
}
amapiConfig.AppIDsToNames[androidApps[i].AdamID] = androidApps[i].Name
var addAppResp addAppStoreAppResponse
// last app goes on the team, will not affect the test
var teamIDPtr *uint
if i == len(androidApps)-1 {
teamIDPtr = &teamID
}
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
AppStoreID: androidApps[i].AdamID,
Platform: fleet.AndroidPlatform,
TeamID: teamIDPtr,
}, http.StatusOK, &addAppResp)
titleIDs[i] = addAppResp.TitleID
}
// delete app [0], does not affect any host so no removeApplicationsPolicy call
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", titleIDs[0]), nil, http.StatusNoContent, "team_id", "0")
s.runWorkerUntilDone()
require.False(t, s.androidAPIClient.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked)
require.False(t, s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
// enroll a few Android devices
secrets, err := s.ds.GetEnrollSecrets(ctx, nil)
require.NoError(t, err)
require.Len(t, secrets, 1)
assets, err := s.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAndroidPubSubToken}, nil)
require.NoError(t, err)
pubsubToken := assets[fleet.MDMAssetAndroidPubSubToken]
require.NotEmpty(t, pubsubToken.Value)
deviceID1 := createAndroidDeviceID("test-android")
deviceID2 := createAndroidDeviceID("test-android-2")
deviceID3 := createAndroidDeviceID("test-android-3")
enterpriseSpecificID1 := strings.ToUpper(uuid.New().String())
enterpriseSpecificID2 := strings.ToUpper(uuid.New().String())
enterpriseSpecificID3 := strings.ToUpper(uuid.New().String())
var req android_service.PubSubPushRequest
for _, d := range []struct {
id string
esi string
}{{deviceID1, enterpriseSpecificID1}, {deviceID2, enterpriseSpecificID2}, {deviceID3, enterpriseSpecificID3}} {
enrollmentMessage := enrollmentMessageWithEnterpriseSpecificID(t, androidmanagement.Device{
Name: d.id,
EnrollmentTokenData: fmt.Sprintf(`{"EnrollSecret": "%s"}`, secrets[0].Secret),
}, d.esi)
req = android_service.PubSubPushRequest{PubSubMessage: *enrollmentMessage}
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
}
var hosts listHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &hosts)
require.Len(t, hosts.Hosts, 3)
host1 := hosts.Hosts[0]
host2 := hosts.Hosts[1]
host3 := hosts.Hosts[2] // isolated host, not affected by test
// transfer host3 to the team
s.DoJSON("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{
TeamID: &teamID,
HostIDs: []uint{host3.ID},
}, http.StatusOK, &addHostsToTeamResponse{})
// run the worker, the hosts get the 3 remaining android apps as self-service-available
s.runWorkerUntilDone()
require.False(t, s.androidAPIClient.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked)
require.True(t, s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked = false
s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked = false // from now on (after device enrollment), this gets called only when we expect it to
for _, host := range []fleet.HostResponse{host1, host2} {
var getHostSw getHostSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw,
"available_for_install", "true", "order_key", "name")
require.Len(t, getHostSw.Software, 3)
require.NotNil(t, getHostSw.Software[0].AppStoreApp)
require.Equal(t, androidApps[1].AdamID, getHostSw.Software[0].AppStoreApp.AppStoreID)
require.NotNil(t, getHostSw.Software[1].AppStoreApp)
require.Equal(t, androidApps[2].AdamID, getHostSw.Software[1].AppStoreApp.AppStoreID)
require.NotNil(t, getHostSw.Software[2].AppStoreApp)
require.Equal(t, androidApps[3].AdamID, getHostSw.Software[2].AppStoreApp.AppStoreID)
}
// delete app [1], should trigger remove from both hosts
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", titleIDs[1]), nil, http.StatusNoContent, "team_id", "0")
s.runWorkerUntilDone()
require.True(t, s.androidAPIClient.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked)
require.False(t, s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
require.False(t, s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked)
s.androidAPIClient.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked = false
for _, host := range []fleet.HostResponse{host1, host2} {
var getHostSw getHostSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw,
"available_for_install", "true", "order_key", "name")
require.Len(t, getHostSw.Software, 2)
require.NotNil(t, getHostSw.Software[0].AppStoreApp)
require.Equal(t, androidApps[2].AdamID, getHostSw.Software[0].AppStoreApp.AppStoreID)
require.NotNil(t, getHostSw.Software[1].AppStoreApp)
require.Equal(t, androidApps[3].AdamID, getHostSw.Software[1].AppStoreApp.AppStoreID)
}
// batch-set to keep only app [3] (effectively deletes app[2]), as per a gitops run
var batchResp batchAssociateAppStoreAppsResponse
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps/batch", batchAssociateAppStoreAppsRequest{
Apps: []fleet.VPPBatchPayload{
{AppStoreID: androidApps[3].AdamID, SelfService: true, Platform: fleet.AndroidPlatform, Configuration: json.RawMessage("{}")},
},
}, http.StatusOK, &batchResp)
s.runWorkerUntilDone() // calls policies patch, which sets (replaces) the list of apps
require.False(t, s.androidAPIClient.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked)
require.False(t, s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
require.True(t, s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked)
s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked = false
for _, host := range []fleet.HostResponse{host1, host2} {
var getHostSw getHostSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw,
"available_for_install", "true", "order_key", "name")
require.Len(t, getHostSw.Software, 1)
require.NotNil(t, getHostSw.Software[0].AppStoreApp)
require.Equal(t, androidApps[3].AdamID, getHostSw.Software[0].AppStoreApp.AppStoreID)
}
// batch-set to remove all apps
batchResp = batchAssociateAppStoreAppsResponse{}
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps/batch", batchAssociateAppStoreAppsRequest{
Apps: []fleet.VPPBatchPayload{},
}, http.StatusOK, &batchResp)
s.runWorkerUntilDone() // calls policies patch, which sets (replaces) the list of apps
require.False(t, s.androidAPIClient.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked)
require.False(t, s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
require.True(t, s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked)
s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked = false
for _, host := range []fleet.HostResponse{host1, host2} {
var getHostSw getHostSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw,
"available_for_install", "true", "order_key", "name")
require.Len(t, getHostSw.Software, 0)
}
// batch-set again without any app is a no-op
batchResp = batchAssociateAppStoreAppsResponse{}
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps/batch", batchAssociateAppStoreAppsRequest{
Apps: []fleet.VPPBatchPayload{},
}, http.StatusOK, &batchResp)
s.runWorkerUntilDone()
require.False(t, s.androidAPIClient.EnterprisesPoliciesRemovePolicyApplicationsFuncInvoked)
require.False(t, s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
require.False(t, s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked)
// isolated host was unaffected, still has the same team app
var getHostSw getHostSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host3.ID), nil, http.StatusOK, &getHostSw,
"available_for_install", "true", "order_key", "name")
require.Len(t, getHostSw.Software, 1)
require.NotNil(t, getHostSw.Software[0].AppStoreApp)
require.Equal(t, androidApps[4].AdamID, getHostSw.Software[0].AppStoreApp.AppStoreID)
}
+41 -1
View File
@@ -31,8 +31,9 @@ func (v *SoftwareWorker) Name() string {
}
const (
makeAndroidAppsAvailableForHostTask SoftwareWorkerTask = "make_android_apps_available_for_host"
makeAndroidAppsAvailableForHostTask SoftwareWorkerTask = "make_android_apps_available_for_host" // deprecated
makeAndroidAppAvailableTask SoftwareWorkerTask = "make_android_app_available"
makeAndroidAppUnavailableTask SoftwareWorkerTask = "make_android_app_unavailable"
runAndroidSetupExperienceTask SoftwareWorkerTask = "run_android_setup_experience"
bulkSetAndroidAppsAvailableForHostTask SoftwareWorkerTask = "bulk_set_android_apps_available_for_host"
bulkSetAndroidAppsAvailableForHostsTask SoftwareWorkerTask = "bulk_set_android_apps_available_for_hosts"
@@ -62,6 +63,10 @@ type softwareWorkerArgs struct {
// of the action that triggered this task.
AppConfigChanged bool `json:"app_config_changed,omitempty"`
UUIDsToIDs map[string]uint `json:"uuids_to_ids,omitempty"`
// HostUUIDToPolicyID is a map of host UUID as key to policy ID as value
// for which the app to make unavailable applies.
HostUUIDToPolicyID map[string]string `json:"host_uuid_to_policy_id,omitempty"`
}
func (v *SoftwareWorker) Run(ctx context.Context, argsJSON json.RawMessage) error {
@@ -89,6 +94,14 @@ func (v *SoftwareWorker) Run(ctx context.Context, argsJSON json.RawMessage) erro
makeAndroidAppAvailableTask,
)
case makeAndroidAppUnavailableTask:
return ctxerr.Wrapf(
ctx,
v.makeAndroidAppUnavailable(ctx, args.ApplicationID, args.HostUUIDToPolicyID, args.EnterpriseName),
"running %s task",
makeAndroidAppUnavailableTask,
)
case runAndroidSetupExperienceTask:
return ctxerr.Wrapf(
ctx,
@@ -167,6 +180,16 @@ func (v *SoftwareWorker) makeAndroidAppAvailable(ctx context.Context, applicatio
return nil
}
// this is called when an app is removed from Fleet.
func (v *SoftwareWorker) makeAndroidAppUnavailable(ctx context.Context, applicationID string, hostUUIDToPolicyID map[string]string, enterpriseName string) error {
// Update Android MDM policy to remove the app from the hosts
_, err := v.AndroidModule.RemoveAppsFromAndroidPolicy(ctx, enterpriseName, []string{applicationID}, hostUUIDToPolicyID)
if err != nil {
return ctxerr.Wrap(ctx, err, "add app store app: add app to android policy")
}
return nil
}
func (v *SoftwareWorker) ensureHostSpecificPolicyIsApplied(ctx context.Context, hostUUID string, enterpriseName, policyID string) error {
if policyID == fmt.Sprint(android.DefaultAndroidPolicyID) {
var policy androidmanagement.Policy
@@ -448,6 +471,23 @@ func QueueMakeAndroidAppAvailableJob(ctx context.Context, ds fleet.Datastore, lo
return nil
}
func QueueMakeAndroidAppUnavailableJob(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, applicationID string, hostsUUIDToPolicyID map[string]string, enterpriseName string) error {
args := &softwareWorkerArgs{
Task: makeAndroidAppUnavailableTask,
ApplicationID: applicationID,
HostUUIDToPolicyID: hostsUUIDToPolicyID,
EnterpriseName: enterpriseName,
}
job, err := QueueJob(ctx, ds, softwareWorkerJobName, args)
if err != nil {
return ctxerr.Wrap(ctx, err, "queueing job")
}
level.Debug(logger).Log("job_id", job.ID, "job_name", softwareWorkerJobName, "task", args.Task)
return nil
}
func QueueBulkSetAndroidAppsAvailableForHost(
ctx context.Context,
ds fleet.Datastore,