RAA: Support removing an android app from managed google play (self-service) on deletion (#39343)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user