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:
co-authored by
RachelElysia
parent
f5a4d38564
commit
5a8e2774bf
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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, ¬FoundError{}
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user