Feature branch: Android Setup Experience support (#35951)

Feature branch for
https://github.com/fleetdm/fleet/issues/33761#issuecomment-3548996114


---------

Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
This commit is contained in:
Martin Angers
2025-12-02 12:27:20 -05:00
committed by GitHub
co-authored by RachelElysia
parent f5a4d38564
commit 5a8e2774bf
52 changed files with 2545 additions and 400 deletions
+17
View File
@@ -1,9 +1,12 @@
package android
import (
"database/sql"
"time"
)
const DefaultAndroidPolicyID = 1
type SignupDetails struct {
Url string
Name string
@@ -59,3 +62,17 @@ type AgentManagedConfiguration struct {
type AgentCertificateTemplate struct {
ID uint `json:"id"`
}
// MDMAndroidPolicyRequest represents a request made to the Android Management
// API (AMAPI) to patch the policy or the device (as made by
// androidsvc.ReconcileProfiles).
type MDMAndroidPolicyRequest struct {
RequestUUID string `db:"request_uuid"`
RequestName string `db:"request_name"`
PolicyID string `db:"policy_id"`
Payload []byte `db:"payload"`
StatusCode int `db:"status_code"`
ErrorDetails sql.Null[string] `db:"error_details"`
AppliedPolicyVersion sql.Null[int64] `db:"applied_policy_version"`
PolicyVersion sql.Null[int64] `db:"policy_version"`
}
+1 -1
View File
@@ -22,7 +22,7 @@ type Service interface {
UnenrollAndroidHost(ctx context.Context, hostID uint) error
EnterprisesApplications(ctx context.Context, enterpriseName, applicationID string) (*androidmanagement.Application, error)
AddAppToAndroidPolicy(ctx context.Context, enterpriseName string, applicationIDs []string, hostUUIDs map[string]string) error
AddAppsToAndroidPolicy(ctx context.Context, enterpriseName string, applicationIDs []string, hostUUIDs map[string]string, installType string) (map[string]*MDMAndroidPolicyRequest, error)
AddFleetAgentToAndroidPolicy(ctx context.Context, enterpriseName string, hostConfigs map[string]AgentManagedConfiguration) error
EnableAppReportsOnDefaultPolicy(ctx context.Context) error
MigrateToPerDevicePolicy(ctx context.Context) error
@@ -217,6 +217,9 @@ func InitCommonDSMocks() *AndroidMockDS {
ds.Store.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return &fleet.Job{}, nil
}
ds.Store.MarkAllPendingAndroidVPPInstallsAsFailedFunc = func(ctx context.Context) error {
return nil
}
return &ds
}
+6 -96
View File
@@ -4,7 +4,6 @@ import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"net/http"
@@ -17,7 +16,6 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
kitlog "github.com/go-kit/log"
"google.golang.org/api/androidmanagement/v1"
"google.golang.org/api/googleapi"
)
func ReconcileProfiles(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, licenseKey string) error {
@@ -386,122 +384,34 @@ func buildPolicyFieldsOverriddenErrorMessage(overriddenFields []string) string {
func (r *profileReconciler) patchPolicy(ctx context.Context, policyID, policyName string,
policy *androidmanagement.Policy, metadata map[string]string,
) (req *fleet.MDMAndroidPolicyRequest, skip bool, err error) {
) (req *android.MDMAndroidPolicyRequest, skip bool, err error) {
policyRequest, err := newAndroidPolicyRequest(policyID, policyName, policy, metadata)
if err != nil {
return nil, false, ctxerr.Wrapf(ctx, err, "prepare policy request %s", policyName)
}
applied, apiErr := r.Client.EnterprisesPoliciesPatch(ctx, policyName, policy)
if apiErr != nil {
var gerr *googleapi.Error
if errors.As(apiErr, &gerr) {
policyRequest.StatusCode = gerr.Code
}
policyRequest.ErrorDetails.V = apiErr.Error()
policyRequest.ErrorDetails.Valid = true
// Note that from my tests, the "not modified" error is not reliable, the
// AMAPI happily returned 200 even if the policy was the same (as
// confirmed by the same version number being returned), so we do check
// for this error, but do not build critical logic on top of it.
//
// Tests do show that the version number is properly incremented when the
// policy changes, though.
if skip = androidmgmt.IsNotModifiedError(apiErr); skip {
apiErr = nil
}
} else {
policyRequest.StatusCode = http.StatusOK
policyRequest.PolicyVersion.V = applied.Version
policyRequest.PolicyVersion.Valid = true
}
if err := r.DS.NewAndroidPolicyRequest(ctx, policyRequest); err != nil {
return nil, false, ctxerr.Wrap(ctx, err, "save android policy request")
if skip, err = recordAndroidRequestResult(ctx, r.DS, policyRequest, applied, nil, apiErr); err != nil {
return nil, false, ctxerr.Wrap(ctx, err, "record android request")
}
return policyRequest, skip, nil
}
func newAndroidPolicyRequest(policyID, policyName string, policy *androidmanagement.Policy, metadata map[string]string) (*fleet.MDMAndroidPolicyRequest, error) {
// save the payload with metadata about what setting comes from what profile
m := fleet.AndroidPolicyRequestPayload{
Policy: policy,
Metadata: fleet.AndroidPolicyRequestPayloadMetadata{
SettingsOrigin: metadata,
},
}
b, err := json.Marshal(m)
if err != nil {
return nil, fmt.Errorf("failed to marshal policy to json: %w", err)
}
return &fleet.MDMAndroidPolicyRequest{
RequestName: policyName,
PolicyID: policyID,
Payload: b,
}, nil
}
func (r *profileReconciler) patchDevice(ctx context.Context, policyID, deviceName string,
device *androidmanagement.Device,
) (req *fleet.MDMAndroidPolicyRequest, skip bool, apiErr error) {
) (req *android.MDMAndroidPolicyRequest, skip bool, apiErr error) {
deviceRequest, err := newAndroidDeviceRequest(policyID, deviceName, device)
if err != nil {
return nil, false, ctxerr.Wrapf(ctx, err, "prepare device request %s", deviceName)
}
applied, apiErr := r.Client.EnterprisesDevicesPatch(ctx, deviceName, device)
if apiErr != nil {
var gerr *googleapi.Error
if errors.As(apiErr, &gerr) {
deviceRequest.StatusCode = gerr.Code
}
deviceRequest.ErrorDetails.V = apiErr.Error()
deviceRequest.ErrorDetails.Valid = true
if skip = androidmgmt.IsNotModifiedError(apiErr); skip {
apiErr = nil
}
} else {
deviceRequest.StatusCode = http.StatusOK
deviceRequest.AppliedPolicyVersion.V = applied.AppliedPolicyVersion
deviceRequest.AppliedPolicyVersion.Valid = true
}
if err := r.DS.NewAndroidPolicyRequest(ctx, deviceRequest); err != nil {
return nil, false, ctxerr.Wrap(ctx, err, "save android device request")
if skip, err = recordAndroidRequestResult(ctx, r.DS, deviceRequest, nil, applied, apiErr); err != nil {
return nil, false, ctxerr.Wrap(ctx, err, "record android request")
}
return deviceRequest, skip, nil
}
func newAndroidDeviceRequest(policyID, deviceName string, device *androidmanagement.Device) (*fleet.MDMAndroidPolicyRequest, error) {
b, err := json.Marshal(device)
if err != nil {
return nil, fmt.Errorf("failed to marshal device to json: %w", err)
}
return &fleet.MDMAndroidPolicyRequest{
RequestName: deviceName,
PolicyID: policyID,
Payload: b,
}, nil
}
func applyFleetEnforcedSettings(policy *androidmanagement.Policy) {
policy.StatusReportingSettings = &androidmanagement.StatusReportingSettings{
DeviceSettingsEnabled: true,
MemoryInfoEnabled: true,
NetworkInfoEnabled: true,
DisplayInfoEnabled: true,
PowerManagementEventsEnabled: true,
HardwareStatusEnabled: true,
SystemPropertiesEnabled: true,
SoftwareInfoEnabled: true,
CommonCriteriaModeEnabled: true,
ApplicationReportsEnabled: true,
ApplicationReportingSettings: nil, // only option is "includeRemovedApps", which I opted not to enable (we can diff apps to see removals)
}
}
// reconcileCertificateTemplates processes certificate templates for Android in host batches.
func (r *profileReconciler) reconcileCertificateTemplates(ctx context.Context) error {
const batchSize = 1000 // Process 1000 hosts at a time
+210 -5
View File
@@ -117,6 +117,14 @@ func (svc *Service) handlePubSubStatusReport(ctx context.Context, token string,
return ctxerr.Wrap(ctx, err, "unmarshal Android status report message")
}
// NOTE: uncomment as needed, can be useful for debugging as the pubsub report
// can be very large - it is not practical to print so it saves it to a file,
// different names for all instances of the pubsub, and under an extension that
// is git-ignored.
// dump := spew.Sdump(device)
// ts := time.Now().UnixNano()
// _ = os.WriteFile(fmt.Sprintf("host_%s_version_%d_timestamps_%d.log", device.HardwareInfo.EnterpriseSpecificId, device.AppliedPolicyVersion, ts), []byte(dump), 0644)
// Consider both appliedState and state fields for deletion, to handle variations in payloads.
isDeleted := strings.ToUpper(device.AppliedState) == string(android.DeviceStateDeleted)
if !isDeleted {
@@ -146,6 +154,22 @@ func (svc *Service) handlePubSubStatusReport(ctx context.Context, token string,
if err != nil {
return ctxerr.Wrap(ctx, err, "set android host unenrolled on DELETED state")
}
// cancel any apps pending install for this host
users, acts, err := svc.ds.MarkAllPendingVPPInstallsAsFailedForAndroidHost(ctx, host.Host.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "mark pending vpp installs as failed for deleted android host")
}
if len(users) != len(acts) {
return ctxerr.New(ctx, "number of users and activities must match, this is a Fleet development bug")
}
for i, act := range acts {
user := users[i]
if err := svc.activityModule.NewActivity(ctx, user, act); err != nil {
return ctxerr.Wrap(ctx, err, "create failed app install activity")
}
}
if !didUnenroll {
return nil // Skip activity, if we didn't update the enrollment state.
}
@@ -268,6 +292,22 @@ func (svc *Service) handlePubSubEnrollment(ctx context.Context, token string, ra
if _, err := svc.ds.SetAndroidHostUnenrolled(ctx, host.Host.ID); err != nil {
return ctxerr.Wrap(ctx, err, "set android host unenrolled on DELETED state (ENROLLMENT)")
}
// cancel any apps pending install for this host
users, acts, err := svc.ds.MarkAllPendingVPPInstallsAsFailedForAndroidHost(ctx, host.Host.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "mark pending vpp installs as failed for deleted android host")
}
if len(users) != len(acts) {
return ctxerr.New(ctx, "number of users and activities must match, this is a Fleet development bug")
}
for i, act := range acts {
user := users[i]
if err := svc.activityModule.NewActivity(ctx, user, act); err != nil {
return ctxerr.Wrap(ctx, err, "create failed app install activity")
}
}
displayName := svc.getComputerName(&device)
_ = svc.activityModule.NewActivity(ctx, nil, fleet.ActivityTypeMDMUnenrolled{
HostSerial: "",
@@ -370,6 +410,7 @@ func (svc *Service) updateHost(ctx context.Context, device *androidmanagement.De
}
host.Device.LastPolicySyncTime = ptr.Time(policySyncTime)
svc.verifyDevicePolicy(ctx, host.UUID, device)
svc.verifyDeviceSoftware(ctx, host.Host, device)
}
deviceID, err := svc.getDeviceID(ctx, device)
@@ -410,6 +451,20 @@ func (svc *Service) updateHost(ctx context.Context, device *androidmanagement.De
if err != nil {
return ctxerr.Wrap(ctx, err, "enrolling Android host")
}
if fromEnroll {
enterprise, err := svc.ds.GetEnterprise(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "get android enterprise")
}
err = worker.QueueRunAndroidSetupExperience(ctx, svc.fleetDS, svc.logger,
host.Host.UUID, host.Host.TeamID, enterprise.Name())
if err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing run android setup experience for host job")
}
}
// Enrollment activities are intentionally not emitted for Android at this time.
return nil
}
@@ -457,9 +512,8 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De
DeviceID: deviceID,
},
}
policy := ptr.String(fmt.Sprint(defaultAndroidPolicyID))
if device.AppliedPolicyName != "" {
policy, err = svc.getPolicyID(ctx, device)
policy, err := svc.getPolicyID(ctx, device)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting Android policy ID")
}
@@ -492,9 +546,10 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De
return ctxerr.Wrap(ctx, err, "get android enterprise")
}
err = worker.QueueMakeAndroidAppsAvailableForHostJob(ctx, svc.fleetDS, svc.logger, device.HardwareInfo.EnterpriseSpecificId, fleetHost.Host.ID, enterprise.Name(), *policy)
err = worker.QueueRunAndroidSetupExperience(ctx, svc.fleetDS, svc.logger,
fleetHost.Host.UUID, fleetHost.Host.TeamID, enterprise.Name())
if err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing make android apps available for host job")
return ctxerr.Wrap(ctx, err, "enqueuing run android setup experience for host job")
}
return nil
@@ -544,7 +599,7 @@ func (svc *Service) verifyDevicePolicy(ctx context.Context, hostUUID string, dev
level.Debug(svc.logger).Log("msg", "Verifying Android device policy", "host_uuid", hostUUID, "applied_policy_version", appliedPolicyVersion)
// Get all host_mdm_android_profiles that is pending, and included_in_policy_version = device.AppliedPolicyVersion.
// Get all host_mdm_android_profiles that is pending, and included_in_policy_version <= device.AppliedPolicyVersion.
// That way we can either fully verify the profile, or mark as failed if the field it tries to set is not compliant.
// Get all profiles that are pending install
@@ -660,6 +715,156 @@ func (svc *Service) verifyDevicePolicy(ctx context.Context, hostUUID string, dev
}
}
func (svc *Service) verifyDeviceSoftware(ctx context.Context, host *fleet.Host, device *androidmanagement.Device) {
appliedPolicyVersion := device.AppliedPolicyVersion
hostUUID := host.UUID
level.Debug(svc.logger).Log("msg", "Verifying Android device software", "host_uuid", hostUUID, "applied_policy_version", appliedPolicyVersion)
// Get all host_vpp_software_installs that are pending, and set in a policy version <= device.AppliedPolicyVersion.
// That way we can either fully verify the app install, or mark as failed if the app is not compliant.
pendingInstallApps, err := svc.ds.ListHostMDMAndroidVPPAppsPendingInstallWithVersion(ctx, hostUUID, appliedPolicyVersion)
if err != nil {
level.Error(svc.logger).Log("msg", "error getting pending vpp installs", "err", err)
return
}
if len(pendingInstallApps) == 0 {
return
}
// index pending installs by package name
pendingByPackageName := make(map[string]*fleet.HostAndroidVPPSoftwareInstall, len(pendingInstallApps))
for _, app := range pendingInstallApps {
pendingByPackageName[app.AdamID] = app
}
// index non-compliance reports by package name, currently we don't care about why it wasn't
// compliant, as soon as it is non-compliant we will mark the install as failed
nonCompliantByPackageName := make(map[string]*androidmanagement.NonComplianceDetail)
for _, report := range device.NonComplianceDetails {
if _, ok := pendingByPackageName[report.PackageName]; ok {
// this is a package we're tracking, keep its non-compliance report
nonCompliantByPackageName[report.PackageName] = report
}
}
// track for each app if it should be marked verified (true) or failed (false)
markVerified := make(map[string]bool, len(pendingInstallApps))
for _, appReport := range device.ApplicationReports {
if _, ok := pendingByPackageName[appReport.PackageName]; ok {
// TODO(mna): what if appReport.State is "REMOVED", and the user removed it before
// the "INSTALLED" state was reported? Should we say it was installed successfully,
// and how do we even know it was installed at all? Does it matter? Not handling for
// now, could be something to improve when we implement standard app install support
// for Android.
// NOTE: I've seen appReport.State == INSTALLED while a non-compliant report says
// "IN_PROGRESS", but on the device the app was indeed installed and no further
// pub-sub report came in, so I think the best approach is to mark it as successfully
// installed (regardless of any non-compliance report) if its state is INSTALLED.
if appReport.State == "INSTALLED" {
// definitely installed successfully
markVerified[appReport.PackageName] = true
level.Debug(svc.logger).Log("msg", "Software marked as verified", "host_uuid", hostUUID, "package_name", appReport.PackageName)
continue
}
}
level.Debug(svc.logger).Log("msg", "Software not marked as verified, checking if failed", "host_uuid", hostUUID, "package_name", appReport.PackageName)
}
// for the remaining apps, mark as failed if non-conformant
for packageName := range pendingByPackageName {
if _, ok := markVerified[packageName]; ok {
// already marked as verified
continue
}
if report := nonCompliantByPackageName[packageName]; report != nil {
if report.NonComplianceReason == "PENDING" || report.InstallationFailureReason == "IN_PROGRESS" {
// keep as pending, the understanding is that another pub-sub will follow when the app's state
// chances to installed or failed.
level.Debug(svc.logger).Log("msg", "Software not reported as installed yet, will remain pending", "host_uuid", hostUUID, "package_name", packageName,
"non_compliance_reason", report.NonComplianceReason,
"installation_failure_reason", report.InstallationFailureReason)
continue
}
// otherwise it has failed to install, mark as failed
markVerified[packageName] = false
level.Error(svc.logger).Log("msg", "Software failed to install", "host_uuid", hostUUID, "package_name", packageName,
"non_compliance_reason", report.NonComplianceReason,
"installation_failure_reason", report.InstallationFailureReason,
"specific_non_compliance_reason", report.SpecificNonComplianceReason)
continue
}
// no non-compliance report, but also not reported as installed, give it another
// chance later if the applied version == requested version? For now, marking as
// failed, we don't know how long it might take for the device to receive another
// policy, it may never happen.
markVerified[packageName] = false
level.Error(svc.logger).Log("msg", "Software failed to install without non-compliance report", "host_uuid", hostUUID, "package_name", packageName,
"installation_failure_reason", "unknown - no non-compliance report received")
}
var toVerifyUUIDs, toFailUUIDs []string
for packageName, install := range pendingByPackageName {
// ignore those not in markVerified, as they will enter a final state in a future
// pub-sub message.
if verified, ok := markVerified[packageName]; ok {
if verified {
toVerifyUUIDs = append(toVerifyUUIDs, install.CommandUUID)
} else {
toFailUUIDs = append(toFailUUIDs, install.CommandUUID)
}
}
}
if err := svc.ds.BulkSetVPPInstallsAsVerified(ctx, host.ID, toVerifyUUIDs); err != nil {
level.Error(svc.logger).Log("msg", "error marking vpp installs as verified", "err", err, "host_uuid", hostUUID)
return
}
if err := svc.ds.BulkSetVPPInstallsAsFailed(ctx, host.ID, toFailUUIDs); err != nil {
level.Error(svc.logger).Log("msg", "error marking vpp installs as failed", "err", err, "host_uuid", hostUUID)
return
}
createPastActivity := func(cmdUUID string, status fleet.SoftwareInstallerStatus) (stop bool) {
user, act, err := svc.ds.GetPastActivityDataForAndroidVPPAppInstall(ctx, cmdUUID, status)
if err != nil {
if fleet.IsNotFound(err) {
// shouldn't happen, but no need to fail
return false
}
// otherwise it's a DB error and we should fail
level.Error(svc.logger).Log("msg", "error getting past activity for installed software", "err", err, "host_uuid", hostUUID)
return true
}
if act == nil {
// could happen if command is not found, but shouldn't
level.Debug(svc.logger).Log("msg", "getting past activity for installed software did not find the command", "host_uuid", hostUUID)
return false
}
act.FromSetupExperience = true // currently, all Android app installs are from setup experience
if err := svc.activityModule.NewActivity(ctx, user, act); err != nil {
level.Error(svc.logger).Log("msg", "error creating past activity for installed software", "err", err, "host_uuid", hostUUID)
return true
}
return false
}
// create the matching past activities
for _, cmd := range toVerifyUUIDs {
if stop := createPastActivity(cmd, fleet.SoftwareInstalled); stop {
return
}
}
for _, cmd := range toFailUUIDs {
if stop := createPastActivity(cmd, fleet.SoftwareInstallFailed); stop {
return
}
}
}
func buildNonComplianceErrorMessage(nonCompliance []*androidmanagement.NonComplianceDetail) string {
failedSettings := []string{}
failedReasons := []string{}
+372 -2
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"os"
"strconv"
"strings"
"testing"
"time"
@@ -238,6 +239,9 @@ func TestStatusReportPolicyValidation(t *testing.T) {
installPendingProfile,
}, nil
}
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
return nil, nil
}
mockDS.BulkUpsertMDMAndroidHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAndroidProfilePayload) error {
require.Len(t, payload, 1)
require.Equal(t, installPendingProfile.ProfileUUID, payload[0].ProfileUUID)
@@ -286,7 +290,7 @@ func TestStatusReportPolicyValidation(t *testing.T) {
PolicyRequestUUID: &policyRequestUUID,
}
mockDS.GetAndroidPolicyRequestByUUIDFunc = func(ctx context.Context, id string) (*fleet.MDMAndroidPolicyRequest, error) {
mockDS.GetAndroidPolicyRequestByUUIDFunc = func(ctx context.Context, id string) (*android.MDMAndroidPolicyRequest, error) {
if id == policyRequestUUID {
payload, err := json.Marshal(map[string]any{
"policy": map[string]any{
@@ -301,7 +305,7 @@ func TestStatusReportPolicyValidation(t *testing.T) {
},
})
require.NoError(t, err)
return &fleet.MDMAndroidPolicyRequest{
return &android.MDMAndroidPolicyRequest{
Payload: payload,
}, nil
}
@@ -315,6 +319,9 @@ func TestStatusReportPolicyValidation(t *testing.T) {
installPendingProfile2,
}, nil
}
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
return nil, nil
}
mockDS.BulkUpsertMDMAndroidHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAndroidProfilePayload) error {
require.Len(t, payload, 2)
for _, profile := range payload {
@@ -1002,8 +1009,13 @@ func createEnrollmentMessageWithMultipleExternalDetectedEvents(t *testing.T, dev
}
func createStatusReportMessage(t *testing.T, deviceId, name, policyName string, policyVersion *int, nonComplianceDetails []*androidmanagement.NonComplianceDetail) android.PubSubMessage {
return createStatusAppReportMessage(t, deviceId, name, policyName, policyVersion, nil, nonComplianceDetails)
}
func createStatusAppReportMessage(t *testing.T, deviceId, name, policyName string, policyVersion *int, appReports []*androidmanagement.ApplicationReport, nonComplianceDetails []*androidmanagement.NonComplianceDetail) android.PubSubMessage {
device := androidmanagement.Device{
Name: createAndroidDeviceId(name),
ApplicationReports: appReports,
NonComplianceDetails: nonComplianceDetails,
HardwareInfo: &androidmanagement.HardwareInfo{
EnterpriseSpecificId: deviceId,
@@ -1109,3 +1121,361 @@ func TestBuildNonComplianceErrorMessage(t *testing.T) {
})
}
}
func TestStatusReportAppInstallVerification(t *testing.T) {
svc, mockDS := createAndroidService(t)
androidDevice := &fleet.AndroidHost{
Host: &fleet.Host{
UUID: uuid.NewString(),
},
Device: &android.Device{
DeviceID: createAndroidDeviceId("test"),
},
}
mockDS.AndroidHostLiteFunc = func(ctx context.Context, enterpriseSpecificID string) (*fleet.AndroidHost, error) {
return androidDevice, nil
}
mockDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
AndroidEnabledAndConfigured: true,
},
}, nil
}
mockDS.UpdateAndroidHostFunc = func(ctx context.Context, host *fleet.AndroidHost, fromEnroll bool) error {
return nil
}
mockDS.ListHostMDMAndroidProfilesPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.MDMAndroidProfilePayload, error) {
return nil, nil
}
mockDS.BulkUpsertMDMAndroidHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAndroidProfilePayload) error {
return nil
}
mockDS.BulkDeleteMDMAndroidHostProfilesFunc = func(ctx context.Context, hostUUID string, policyVersionID int64) error {
return nil
}
mockDS.UpdateHostSoftwareFunc = func(ctx context.Context, hostID uint, software []fleet.Software) (*fleet.UpdateHostSoftwareDBResult, error) {
return &fleet.UpdateHostSoftwareDBResult{}, nil
}
mockDS.GetAndroidPolicyRequestByUUIDFunc = func(ctx context.Context, id string) (*android.MDMAndroidPolicyRequest, error) {
return nil, &notFoundError{}
}
mockDS.GetPastActivityDataForAndroidVPPAppInstallFunc = func(ctx context.Context, cmdUUID string, status fleet.SoftwareInstallerStatus) (*fleet.User, *fleet.ActivityInstalledAppStoreApp, error) {
return nil, nil, nil
}
t.Run("no pending app install", func(t *testing.T) {
t.Cleanup(func() {
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked = false
mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked = false
mockDS.BulkSetVPPInstallsAsFailedFuncInvoked = false
})
policyVersion := ptr.Int(1)
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
return nil, nil
}
mockDS.BulkSetVPPInstallsAsVerifiedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
return nil
}
mockDS.BulkSetVPPInstallsAsFailedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
return nil
}
enrollmentMessage := createStatusReportMessage(t, androidDevice.UUID, "test", createAndroidDeviceId("test"), policyVersion, nil)
err := svc.ProcessPubSubPush(context.Background(), "value", &enrollmentMessage)
require.NoError(t, err)
require.True(t, mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked)
require.False(t, mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked)
require.False(t, mockDS.BulkSetVPPInstallsAsFailedFuncInvoked)
})
t.Run("pending app but in a future version", func(t *testing.T) {
t.Cleanup(func() {
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked = false
mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked = false
mockDS.BulkSetVPPInstallsAsFailedFuncInvoked = false
})
pendingApp := &fleet.HostAndroidVPPSoftwareInstall{
AdamID: "com.example.app",
CommandUUID: "a",
AssociatedEventID: "2", // future policy version
}
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
appVersion, _ := strconv.Atoi(pendingApp.AssociatedEventID)
if int64(appVersion) <= version {
return []*fleet.HostAndroidVPPSoftwareInstall{pendingApp}, nil
}
return nil, nil
}
mockDS.BulkSetVPPInstallsAsVerifiedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
return nil
}
mockDS.BulkSetVPPInstallsAsFailedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
return nil
}
policyVersion := ptr.Int(1)
enrollmentMessage := createStatusReportMessage(t, androidDevice.UUID, "test", createAndroidDeviceId("test"), policyVersion, nil)
err := svc.ProcessPubSubPush(context.Background(), "value", &enrollmentMessage)
require.NoError(t, err)
require.True(t, mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked)
require.False(t, mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked)
require.False(t, mockDS.BulkSetVPPInstallsAsFailedFuncInvoked)
})
t.Run("pending app verified", func(t *testing.T) {
t.Cleanup(func() {
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked = false
mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked = false
mockDS.BulkSetVPPInstallsAsFailedFuncInvoked = false
})
pendingApp := &fleet.HostAndroidVPPSoftwareInstall{
AdamID: "com.example.app",
CommandUUID: "a",
AssociatedEventID: "2",
}
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
appVersion, _ := strconv.Atoi(pendingApp.AssociatedEventID)
if int64(appVersion) <= version {
return []*fleet.HostAndroidVPPSoftwareInstall{pendingApp}, nil
}
return nil, nil
}
mockDS.BulkSetVPPInstallsAsVerifiedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.Equal(t, []string{pendingApp.CommandUUID}, cmdUUIDs)
return nil
}
mockDS.BulkSetVPPInstallsAsFailedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.Empty(t, cmdUUIDs)
return nil
}
policyVersion := ptr.Int(2)
enrollmentMessage := createStatusAppReportMessage(t, androidDevice.UUID, "test", createAndroidDeviceId("test"), policyVersion, []*androidmanagement.ApplicationReport{
{PackageName: pendingApp.AdamID, State: "INSTALLED"},
}, nil)
err := svc.ProcessPubSubPush(context.Background(), "value", &enrollmentMessage)
require.NoError(t, err)
require.True(t, mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsFailedFuncInvoked)
})
t.Run("pending app verified with unrelated non-compliance event", func(t *testing.T) {
t.Cleanup(func() {
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked = false
mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked = false
mockDS.BulkSetVPPInstallsAsFailedFuncInvoked = false
})
pendingApp := &fleet.HostAndroidVPPSoftwareInstall{
AdamID: "com.example.app",
CommandUUID: "a",
AssociatedEventID: "2",
}
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
appVersion, _ := strconv.Atoi(pendingApp.AssociatedEventID)
if int64(appVersion) <= version {
return []*fleet.HostAndroidVPPSoftwareInstall{pendingApp}, nil
}
return nil, nil
}
mockDS.BulkSetVPPInstallsAsVerifiedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.Equal(t, []string{pendingApp.CommandUUID}, cmdUUIDs)
return nil
}
mockDS.BulkSetVPPInstallsAsFailedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.Empty(t, cmdUUIDs)
return nil
}
policyVersion := ptr.Int(2)
enrollmentMessage := createStatusAppReportMessage(t, androidDevice.UUID, "test", createAndroidDeviceId("test"), policyVersion, []*androidmanagement.ApplicationReport{
{PackageName: pendingApp.AdamID, State: "INSTALLED"},
}, []*androidmanagement.NonComplianceDetail{
{SettingName: "DefaultPermissionPolicy", NonComplianceReason: "INVALID_VALUE"},
})
err := svc.ProcessPubSubPush(context.Background(), "value", &enrollmentMessage)
require.NoError(t, err)
require.True(t, mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsFailedFuncInvoked)
})
t.Run("pending app failed with non-compliance", func(t *testing.T) {
t.Cleanup(func() {
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked = false
mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked = false
mockDS.BulkSetVPPInstallsAsFailedFuncInvoked = false
})
pendingApp := &fleet.HostAndroidVPPSoftwareInstall{
AdamID: "com.example.app",
CommandUUID: "a",
AssociatedEventID: "2",
}
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
appVersion, _ := strconv.Atoi(pendingApp.AssociatedEventID)
if int64(appVersion) <= version {
return []*fleet.HostAndroidVPPSoftwareInstall{pendingApp}, nil
}
return nil, nil
}
mockDS.BulkSetVPPInstallsAsVerifiedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.Empty(t, cmdUUIDs)
return nil
}
mockDS.BulkSetVPPInstallsAsFailedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.Equal(t, []string{pendingApp.CommandUUID}, cmdUUIDs)
return nil
}
policyVersion := ptr.Int(2)
enrollmentMessage := createStatusAppReportMessage(t, androidDevice.UUID, "test", createAndroidDeviceId("test"), policyVersion, []*androidmanagement.ApplicationReport{
{PackageName: pendingApp.AdamID, State: "APPLICATION_STATE_UNSPECIFIED"},
}, []*androidmanagement.NonComplianceDetail{
{PackageName: pendingApp.AdamID, NonComplianceReason: "APP_NOT_INSTALLED", InstallationFailureReason: "NOT_FOUND"},
})
err := svc.ProcessPubSubPush(context.Background(), "value", &enrollmentMessage)
require.NoError(t, err)
require.True(t, mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsFailedFuncInvoked)
})
t.Run("pending app in progress", func(t *testing.T) {
t.Cleanup(func() {
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked = false
mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked = false
mockDS.BulkSetVPPInstallsAsFailedFuncInvoked = false
})
pendingApp := &fleet.HostAndroidVPPSoftwareInstall{
AdamID: "com.example.app",
CommandUUID: "a",
AssociatedEventID: "2",
}
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
appVersion, _ := strconv.Atoi(pendingApp.AssociatedEventID)
if int64(appVersion) <= version {
return []*fleet.HostAndroidVPPSoftwareInstall{pendingApp}, nil
}
return nil, nil
}
mockDS.BulkSetVPPInstallsAsVerifiedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.Empty(t, cmdUUIDs)
return nil
}
mockDS.BulkSetVPPInstallsAsFailedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.Empty(t, cmdUUIDs)
return nil
}
policyVersion := ptr.Int(2)
enrollmentMessage := createStatusAppReportMessage(t, androidDevice.UUID, "test", createAndroidDeviceId("test"), policyVersion, nil, []*androidmanagement.NonComplianceDetail{
{PackageName: pendingApp.AdamID, NonComplianceReason: "PENDING", InstallationFailureReason: "IN_PROGRESS"},
})
err := svc.ProcessPubSubPush(context.Background(), "value", &enrollmentMessage)
require.NoError(t, err)
require.True(t, mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsFailedFuncInvoked)
})
t.Run("multiple apps in various states", func(t *testing.T) {
t.Cleanup(func() {
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked = false
mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked = false
mockDS.BulkSetVPPInstallsAsFailedFuncInvoked = false
mockDS.GetPastActivityDataForAndroidVPPAppInstallFuncInvoked = false
})
// 4 apps installed in the same policy, so same version,
// and 1 more in a future policy (not possible for setup experience, but
// tests the logic)
pendingApps := []*fleet.HostAndroidVPPSoftwareInstall{
{
AdamID: "com.example.app1",
CommandUUID: "a",
AssociatedEventID: "2",
},
{
AdamID: "com.example.app2",
CommandUUID: "b",
AssociatedEventID: "2",
},
{
AdamID: "com.example.app3",
CommandUUID: "c",
AssociatedEventID: "2",
},
{
AdamID: "com.example.app4",
CommandUUID: "d",
AssociatedEventID: "2",
},
{
AdamID: "com.example.app5",
CommandUUID: "e",
AssociatedEventID: "3",
},
}
mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFunc = func(ctx context.Context, hostUUID string, version int64) ([]*fleet.HostAndroidVPPSoftwareInstall, error) {
switch version {
case 0, 1:
return nil, nil
case 2:
return pendingApps[:4], nil
default:
return pendingApps, nil
}
}
commandsToStatus := map[string]fleet.SoftwareInstallerStatus{
"a": fleet.SoftwareInstalled,
"b": fleet.SoftwareInstalled,
"c": fleet.SoftwareInstallFailed,
"d": fleet.SoftwareInstallFailed,
}
mockDS.BulkSetVPPInstallsAsVerifiedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.ElementsMatch(t, []string{"a", "b"}, cmdUUIDs)
return nil
}
mockDS.BulkSetVPPInstallsAsFailedFunc = func(ctx context.Context, hostID uint, cmdUUIDs []string) error {
require.ElementsMatch(t, []string{"c", "d"}, cmdUUIDs)
return nil
}
mockDS.GetPastActivityDataForAndroidVPPAppInstallFunc = func(ctx context.Context, cmdUUID string, status fleet.SoftwareInstallerStatus) (*fleet.User, *fleet.ActivityInstalledAppStoreApp, error) {
want, ok := commandsToStatus[cmdUUID]
require.True(t, ok, "unexpected command UUID: %s", cmdUUID)
require.Equal(t, want, status)
return &fleet.User{}, &fleet.ActivityInstalledAppStoreApp{CommandUUID: cmdUUID, Status: string(status)}, nil
}
policyVersion := ptr.Int(2)
// app1 and app2 verified, app3 not reported at all so failed, app4 failed with compliance report
enrollmentMessage := createStatusAppReportMessage(t, androidDevice.UUID, "test", createAndroidDeviceId("test"), policyVersion, []*androidmanagement.ApplicationReport{
{PackageName: pendingApps[0].AdamID, State: "INSTALLED"},
{PackageName: pendingApps[1].AdamID, State: "INSTALLED"},
}, []*androidmanagement.NonComplianceDetail{
{PackageName: pendingApps[3].AdamID, NonComplianceReason: "APP_NOT_INSTALLED", InstallationFailureReason: "NOT_APPROVED"},
})
err := svc.ProcessPubSubPush(context.Background(), "value", &enrollmentMessage)
require.NoError(t, err)
require.True(t, mockDS.ListHostMDMAndroidVPPAppsPendingInstallWithVersionFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsVerifiedFuncInvoked)
require.True(t, mockDS.BulkSetVPPInstallsAsFailedFuncInvoked)
require.True(t, mockDS.GetPastActivityDataForAndroidVPPAppInstallFuncInvoked)
})
}
+126
View File
@@ -0,0 +1,126 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"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/android/service/androidmgmt"
"google.golang.org/api/androidmanagement/v1"
"google.golang.org/api/googleapi"
)
func newAndroidDeviceRequest(policyID, deviceName string, device *androidmanagement.Device) (*android.MDMAndroidPolicyRequest, error) {
b, err := json.Marshal(device)
if err != nil {
return nil, fmt.Errorf("failed to marshal device to json: %w", err)
}
return &android.MDMAndroidPolicyRequest{
RequestName: deviceName,
PolicyID: policyID,
Payload: b,
}, nil
}
func newAndroidPolicyApplicationsRequest(policyID, policyName string, apps []*androidmanagement.ApplicationPolicy) (*android.MDMAndroidPolicyRequest, error) {
var changes []*androidmanagement.ApplicationPolicyChange
for _, app := range apps {
changes = append(changes, &androidmanagement.ApplicationPolicyChange{
Application: app,
})
}
req := androidmanagement.ModifyPolicyApplicationsRequest{
Changes: changes,
}
b, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal modify 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{
Policy: policy,
Metadata: fleet.AndroidPolicyRequestPayloadMetadata{
SettingsOrigin: metadata,
},
}
b, err := json.Marshal(m)
if err != nil {
return nil, fmt.Errorf("failed to marshal policy to json: %w", err)
}
return &android.MDMAndroidPolicyRequest{
RequestName: policyName,
PolicyID: policyID,
Payload: b,
}, nil
}
// record an Android API request result in the database, filling the pre-initialized
// (via newAndroidXxxRequest) requestObject with data from the result (success or
// error). Only one of policyResult or deviceResult should be non-nil, depending
// on the type of request made.
func recordAndroidRequestResult(ctx context.Context, ds fleet.Datastore, requestObject *android.MDMAndroidPolicyRequest,
policyResult *androidmanagement.Policy, deviceResult *androidmanagement.Device, apiErr error) (skip bool, err error) {
if apiErr != nil {
var gerr *googleapi.Error
if errors.As(apiErr, &gerr) {
requestObject.StatusCode = gerr.Code
}
requestObject.ErrorDetails.V = apiErr.Error()
requestObject.ErrorDetails.Valid = true
// Note that from my tests, the "not modified" error is not reliable, the
// AMAPI happily returned 200 even if the policy was the same (as
// confirmed by the same version number being returned), so we do check
// for this error, but do not build critical logic on top of it.
//
// Tests do show that the version number is properly incremented when the
// policy changes, though.
if skip = androidmgmt.IsNotModifiedError(apiErr); skip {
apiErr = nil
}
} else {
requestObject.StatusCode = http.StatusOK
if policyResult != nil {
requestObject.PolicyVersion.V = policyResult.Version
requestObject.PolicyVersion.Valid = true
} else if deviceResult != nil {
requestObject.AppliedPolicyVersion.V = deviceResult.AppliedPolicyVersion
requestObject.AppliedPolicyVersion.Valid = true
}
}
if err := ds.NewAndroidPolicyRequest(ctx, requestObject); err != nil {
return false, ctxerr.Wrap(ctx, err, "save android policy request")
}
return skip, nil
}
func applyFleetEnforcedSettings(policy *androidmanagement.Policy) {
policy.StatusReportingSettings = &androidmanagement.StatusReportingSettings{
DeviceSettingsEnabled: true,
MemoryInfoEnabled: true,
NetworkInfoEnabled: true,
DisplayInfoEnabled: true,
PowerManagementEventsEnabled: true,
HardwareStatusEnabled: true,
SystemPropertiesEnabled: true,
SoftwareInfoEnabled: true,
CommonCriteriaModeEnabled: true,
ApplicationReportsEnabled: true,
ApplicationReportingSettings: nil, // only option is "includeRemovedApps", which I opted not to enable (we can diff apps to see removals)
}
}
+32 -13
View File
@@ -31,9 +31,7 @@ import (
// Used for overriding the private key validation in testing
var testSetEmptyPrivateKey bool
// We use numbers for policy names for easier mapping/indexing with Fleet DB.
const (
defaultAndroidPolicyID = 1
DefaultSignupSSEInterval = 3 * time.Second
SignupSSESuccess = "Android Enterprise successfully connected"
)
@@ -331,7 +329,7 @@ func (svc *Service) EnterpriseSignupCallback(ctx context.Context, signupToken st
return ctxerr.Wrap(ctx, err, "updating enterprise")
}
policyName := fmt.Sprintf("%s/policies/%s", enterprise.Name(), fmt.Sprintf("%d", defaultAndroidPolicyID))
policyName := fmt.Sprintf("%s/policies/%s", enterprise.Name(), fmt.Sprintf("%d", android.DefaultAndroidPolicyID))
_, err = svc.androidAPIClient.EnterprisesPoliciesPatch(ctx, policyName, &androidmanagement.Policy{
StatusReportingSettings: &androidmanagement.StatusReportingSettings{
DeviceSettingsEnabled: true,
@@ -349,7 +347,7 @@ func (svc *Service) EnterpriseSignupCallback(ctx context.Context, signupToken st
},
})
if err != nil && !androidmgmt.IsNotModifiedError(err) {
return ctxerr.Wrapf(ctx, err, "patching %d policy", defaultAndroidPolicyID)
return ctxerr.Wrapf(ctx, err, "patching %d policy", android.DefaultAndroidPolicyID)
}
err = svc.ds.DeleteOtherEnterprises(ctx, enterprise.ID)
@@ -453,6 +451,11 @@ func (svc *Service) DeleteEnterprise(ctx context.Context) error {
return ctxerr.Wrap(ctx, err, "bulk set android hosts as unenrolled")
}
err = svc.ds.MarkAllPendingAndroidVPPInstallsAsFailed(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "marking pending android vpp installs as failed")
}
if err = svc.activityModule.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDisabledAndroidMDM{}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for disabled Android MDM")
}
@@ -582,7 +585,7 @@ func (svc *Service) CreateEnrollmentToken(ctx context.Context, enrollSecret, idp
AdditionalData: string(enrollmentTokenRequest),
AllowPersonalUsage: "PERSONAL_USAGE_ALLOWED",
PolicyName: fmt.Sprintf("%s/policies/%d", enterprise.Name(), +defaultAndroidPolicyID),
PolicyName: fmt.Sprintf("%s/policies/%d", enterprise.Name(), android.DefaultAndroidPolicyID),
OneTimeOnly: true,
}
token, err = svc.androidAPIClient.EnterprisesEnrollmentTokensCreate(ctx, enterprise.Name(), token)
@@ -791,6 +794,10 @@ func (svc *Service) cleanupDeletedEnterprise(ctx context.Context, enterprise *an
if unenrollErr := svc.ds.BulkSetAndroidHostsUnenrolled(ctx); unenrollErr != nil {
level.Error(svc.logger).Log("msg", "failed to unenroll Android hosts after enterprise deletion", "err", unenrollErr)
}
if err := svc.ds.MarkAllPendingAndroidVPPInstallsAsFailed(ctx); err != nil {
level.Error(svc.logger).Log("msg", "failed to mark pending Android VPP installs as failed after enterprise deletion", "err", err)
}
}
// Admin-initiated Android unenroll
@@ -861,26 +868,38 @@ func (svc *Service) EnterprisesApplications(ctx context.Context, enterpriseName,
return svc.androidAPIClient.EnterprisesApplications(ctx, enterpriseName, applicationID)
}
func (svc *Service) AddAppToAndroidPolicy(ctx context.Context, enterpriseName string, applicationIDs []string, hostUUIDs map[string]string) error {
// Adds the specified apps to the host-specific Android policy of the provided hosts, and
// returns a map of host UUID to the policy request object of their updated policy on success.
func (svc *Service) AddAppsToAndroidPolicy(ctx context.Context, enterpriseName string, applicationIDs []string, hostUUIDs map[string]string, installType string) (map[string]*android.MDMAndroidPolicyRequest, error) {
var appPolicies []*androidmanagement.ApplicationPolicy
for _, a := range applicationIDs {
appPolicies = append(appPolicies, &androidmanagement.ApplicationPolicy{
PackageName: a,
InstallType: "AVAILABLE",
InstallType: installType,
})
}
var errs []error
hostToPolicyRequest := make(map[string]*android.MDMAndroidPolicyRequest, len(hostUUIDs))
for uuid, policyID := range hostUUIDs {
policyName := fmt.Sprintf("%s/policies/%s", enterpriseName, policyID)
_, err := svc.androidAPIClient.EnterprisesPoliciesModifyPolicyApplications(ctx, policyName, appPolicies)
policyRequest, err := newAndroidPolicyApplicationsRequest(policyID, policyName, appPolicies)
if err != nil {
errs = append(errs, ctxerr.Wrapf(ctx, err, "google api: modify policy applications for host %s", uuid))
return nil, ctxerr.Wrapf(ctx, err, "prepare policy request %s", policyName)
}
policy, apiErr := svc.androidAPIClient.EnterprisesPoliciesModifyPolicyApplications(ctx, policyName, appPolicies)
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: modify policy applications for host %s", uuid))
}
hostToPolicyRequest[uuid] = policyRequest
}
return errors.Join(errs...)
return hostToPolicyRequest, errors.Join(errs...)
}
// AddFleetAgentToAndroidPolicy adds the Fleet Agent to the Android policy for the given enterprise.
@@ -949,7 +968,7 @@ func (svc *Service) EnableAppReportsOnDefaultPolicy(ctx context.Context) error {
}
_ = svc.androidAPIClient.SetAuthenticationSecret(secret)
policyName := fmt.Sprintf("%s/policies/%d", enterprise.Name(), defaultAndroidPolicyID)
policyName := fmt.Sprintf("%s/policies/%d", enterprise.Name(), android.DefaultAndroidPolicyID)
_, err = svc.androidAPIClient.EnterprisesPoliciesPatch(ctx, policyName, &androidmanagement.Policy{
StatusReportingSettings: &androidmanagement.StatusReportingSettings{
DeviceSettingsEnabled: true,
@@ -966,7 +985,7 @@ func (svc *Service) EnableAppReportsOnDefaultPolicy(ctx context.Context) error {
},
})
if err != nil && !androidmgmt.IsNotModifiedError(err) {
return ctxerr.Wrapf(ctx, err, "enabling app reports on %d default policy", defaultAndroidPolicyID)
return ctxerr.Wrapf(ctx, err, "enabling app reports on %d default policy", android.DefaultAndroidPolicyID)
}
return nil
}