fleet/server/worker/software_worker.go
Victor Lyuboslavsky cc5b4f3947
Install Fleet android agent on device enrollment. (#36050)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #35434

Feature is largely behind feature flag `FLEET_DEV_ANDROID_AGENT_PACKAGE`
Set it like: `export
FLEET_DEV_ANDROID_AGENT_PACKAGE=com.fleetdm.agent.private.victor`

Rough set up:
1. Change the applicationId of your Android app in `build.gradle.kts`:
```kt
    defaultConfig {
        applicationId = "com.fleetdm.agent.private.you"
```
2. Build a release version of your app (use dummy signing key). Build ->
Generate Signed App Bundle or APK ...
3. Get the super secret Google Play URL like: `go run
tools/android/android.go --command enterprises.webTokens.create
--enterprise_id 'XXXX'`
4. Upload your signed app.
5. Wait ~10 minutes
6. Enroll your Android device.
7. The agent should start installing pretty soon. Check your Google Play
in Work profile. Mine was pending for a while the last time I tried it
and I restarted the device before it actually started installing.

@ksykulev you can use this Android service method for "notification":
`AddFleetAgentToAndroidPolicy(ctx context.Context, enterpriseName
string, hostConfigs map[string]AgentManagedConfiguration) error`
You'll need to update `AgentManagedConfiguration` struct to define what
to send down to the device. It includes the enroll secret, so I think we
need to send it down every time just to be safe.

# Checklist for submitter

- Changes file will be updated when full feature is done.

## Testing

- [x] QA'd all new/changed functionality manually
2025-11-21 14:42:24 -06:00

215 lines
7.1 KiB
Go

package worker
import (
"context"
"encoding/json"
"fmt"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"google.golang.org/api/androidmanagement/v1"
)
const softwareWorkerJobName = "software_worker"
type SoftwareWorkerTask string
type SoftwareWorker struct {
Datastore fleet.Datastore
AndroidModule android.Service
Log kitlog.Logger
}
func (v *SoftwareWorker) Name() string {
return softwareWorkerJobName
}
const makeAndroidAppsAvailableForHostTask SoftwareWorkerTask = "make_android_apps_available_for_host"
const makeAndroidAppAvailableTask SoftwareWorkerTask = "make_android_app_available"
type softwareWorkerArgs struct {
Task SoftwareWorkerTask `json:"task"`
HostUUID string `json:"host_uuid"`
ApplicationID string `json:"application_id"`
EnterpriseName string `json:"enterprise_name"`
AppTeamID uint `json:"app_team_id"`
HostID uint `json:"host_id"`
PolicyID string `json:"policy_id"`
}
func (v *SoftwareWorker) Run(ctx context.Context, argsJSON json.RawMessage) error {
var args softwareWorkerArgs
if err := json.Unmarshal(argsJSON, &args); err != nil {
return ctxerr.Wrap(ctx, err, "unmarshal args")
}
switch args.Task {
case makeAndroidAppsAvailableForHostTask:
return ctxerr.Wrapf(
ctx,
v.makeAndroidAppsAvailableForHost(ctx, args.HostUUID, args.HostID, args.EnterpriseName, args.PolicyID),
"running %s task",
makeAndroidAppsAvailableForHostTask,
)
case makeAndroidAppAvailableTask:
return ctxerr.Wrapf(
ctx,
v.makeAndroidAppAvailable(ctx, args.ApplicationID, args.AppTeamID, args.EnterpriseName),
"running %s task",
makeAndroidAppAvailableTask,
)
default:
return ctxerr.Errorf(ctx, "unknown task: %v", args.Task)
}
}
func (v *SoftwareWorker) makeAndroidAppAvailable(ctx context.Context, applicationID string, appTeamID uint, enterpriseName string) error {
hosts, err := v.Datastore.GetIncludedHostUUIDMapForAppStoreApp(ctx, appTeamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "add app store app: getting android hosts in scope")
}
// Update Android MDM policy to include the app in self service
err = v.AndroidModule.AddAppToAndroidPolicy(ctx, enterpriseName, []string{applicationID}, hosts)
if err != nil {
return ctxerr.Wrap(ctx, err, "add app store app: add app to android policy")
}
return nil
}
func QueueMakeAndroidAppAvailableJob(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, applicationID string, appTeamID uint, enterpriseName string) error {
args := &softwareWorkerArgs{
Task: makeAndroidAppAvailableTask,
ApplicationID: applicationID,
AppTeamID: appTeamID,
EnterpriseName: enterpriseName,
}
job, err := QueueJob(ctx, ds, softwareWorkerJobName, args)
if err != nil {
return ctxerr.Wrap(ctx, err, "queueing job")
}
level.Debug(logger).Log("job_id", job.ID, "job_name", softwareWorkerJobName, "task", makeAndroidAppAvailableTask)
return nil
}
func (v *SoftwareWorker) makeAndroidAppsAvailableForHost(ctx context.Context, hostUUID string, hostID uint, enterpriseName, policyID string) error {
if policyID == "1" {
// Get the host once for both enroll secret and device patching
androidHost, err := v.Datastore.AndroidHostLiteByHostUUID(ctx, hostUUID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "get android host by host UUID %s", hostUUID)
}
var 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)
}
policyName := fmt.Sprintf("%s/policies/%s", enterpriseName, hostUUID)
_, err = v.AndroidModule.PatchPolicy(ctx, hostUUID, policyName, &policy, nil)
if err != nil {
return err
}
// Get enroll secrets for the host's team (nil means global/no team)
enrollSecrets, err := v.Datastore.GetEnrollSecrets(ctx, androidHost.Host.TeamID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "get enroll secrets for team %v", androidHost.Host.TeamID)
}
if len(enrollSecrets) == 0 {
return ctxerr.Errorf(ctx, "no enroll secrets found for team %v", androidHost.Host.TeamID)
}
// Use the first enroll secret
enrollSecret := enrollSecrets[0].Secret
appConfig, err := v.Datastore.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "get app config")
}
err = v.AndroidModule.AddFleetAgentToAndroidPolicy(ctx, enterpriseName, map[string]android.AgentManagedConfiguration{
hostUUID: {
ServerURL: appConfig.ServerSettings.ServerURL,
HostUUID: hostUUID,
EnrollSecret: enrollSecret,
},
})
if err != nil {
return ctxerr.Wrapf(ctx, err, "add fleet agent to android policy for host %s", hostUUID)
}
device := &androidmanagement.Device{
PolicyName: policyName,
// State must be specified when updating a device, otherwise it fails with
// "Illegal state transition from ACTIVE to DEVICE_STATE_UNSPECIFIED"
//
// > Note that when calling enterprises.devices.patch, ACTIVE and
// > DISABLED are the only allowable values.
// TODO(ap): should we send whatever the previous state was? If it was DISABLED,
// we probably don't want to re-enable it by accident. Those are the only
// 2 valid states when patching a device.
State: "ACTIVE",
}
deviceName := fmt.Sprintf("%s/devices/%s", enterpriseName, androidHost.DeviceID)
_, err = v.AndroidModule.PatchDevice(ctx, hostUUID, deviceName, device)
if err != nil {
return err
}
}
appIDs, err := v.Datastore.GetAndroidAppsInScopeForHost(ctx, hostID)
if err != nil {
return ctxerr.Wrap(ctx, err, "get android apps in scope for host")
}
if len(appIDs) == 0 {
return nil
}
err = v.AndroidModule.AddAppToAndroidPolicy(ctx, enterpriseName, appIDs, map[string]string{hostUUID: hostUUID})
if err != nil {
return ctxerr.Wrap(ctx, err, "add app store app: add app to android policy")
}
return nil
}
func QueueMakeAndroidAppsAvailableForHostJob(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, hostUUID string, hostID uint, enterpriseName, policyID string) error {
args := &softwareWorkerArgs{
Task: makeAndroidAppsAvailableForHostTask,
HostUUID: hostUUID,
HostID: hostID,
EnterpriseName: enterpriseName,
PolicyID: policyID,
}
job, err := QueueJob(ctx, ds, softwareWorkerJobName, args)
if err != nil {
return ctxerr.Wrap(ctx, err, "queueing job")
}
level.Debug(logger).Log("job_id", job.ID, "job_name", softwareWorkerJobName, "task", makeAndroidAppsAvailableForHostTask)
return nil
}