fleet/tools/android/android.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

194 lines
5.5 KiB
Go

package main
import (
"context"
"flag"
"log"
"os"
"strings"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"google.golang.org/api/androidmanagement/v1"
"google.golang.org/api/option"
)
// Required env vars:
var (
androidServiceCredentials = os.Getenv("FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS")
androidProjectID string
)
func main() {
if androidServiceCredentials == "" {
log.Fatal("FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS must be set")
}
type credentials struct {
ProjectID string `json:"project_id"`
}
var creds credentials
err := json.Unmarshal([]byte(androidServiceCredentials), &creds)
if err != nil {
log.Fatalf("unmarshaling android service credentials: %s", err)
}
androidProjectID = creds.ProjectID
if androidProjectID == "" {
log.Fatal("project_id not found in android service credentials")
}
command := flag.String("command", "", "")
enterpriseID := flag.String("enterprise_id", "", "")
deviceID := flag.String("device_id", "", "")
flag.Parse()
// Normalize enterprise_id by stripping "enterprises/" prefix if present
if *enterpriseID != "" {
*enterpriseID = strings.TrimPrefix(*enterpriseID, "enterprises/")
}
ctx := context.Background()
mgmt, err := androidmanagement.NewService(ctx, option.WithCredentialsJSON([]byte(androidServiceCredentials)))
if err != nil {
log.Fatalf("Error creating android management service: %v", err)
}
switch *command {
case "enterprises.delete":
enterprisesDelete(mgmt, *enterpriseID)
case "enterprises.list":
enterprisesList(mgmt)
case "enterprises.webTokens.create":
enterprisesWebTokensCreate(mgmt, *enterpriseID)
case "policies.list":
policiesList(mgmt, *enterpriseID)
case "devices.list":
devicesList(mgmt, *enterpriseID)
case "devices.delete":
devicesDelete(mgmt, *enterpriseID, *deviceID)
case "devices.issueCommand.RELINQUISH_OWNERSHIP":
devicesRelinquishOwnership(mgmt, *enterpriseID, *deviceID)
default:
log.Fatalf("Unknown command: %s", *command)
}
}
func enterprisesDelete(mgmt *androidmanagement.Service, enterpriseID string) {
if enterpriseID == "" {
log.Fatalf("enterprise_id must be set")
}
_, err := mgmt.Enterprises.Delete("enterprises/" + enterpriseID).Do()
if err != nil {
log.Fatalf("Error deleting enterprise: %v", err)
}
}
func enterprisesList(mgmt *androidmanagement.Service) {
enterprises, err := mgmt.Enterprises.List().ProjectId(androidProjectID).Do()
if err != nil {
log.Fatalf("Error listing enterprises: %v", err)
}
if len(enterprises.Enterprises) == 0 {
log.Printf("No enterprises found")
return
}
for _, enterprise := range enterprises.Enterprises {
log.Printf("Enterprise: %+v", *enterprise)
}
}
func policiesList(mgmt *androidmanagement.Service, enterpriseID string) {
if enterpriseID == "" {
log.Fatalf("enterprise_id must be set")
}
result, err := mgmt.Enterprises.Policies.List("enterprises/" + enterpriseID).Do()
if err != nil {
log.Fatalf("Error listing policies: %v", err)
}
if len(result.Policies) == 0 {
log.Printf("No policies found")
return
}
for _, policy := range result.Policies {
log.Printf("Policy: %+v", *policy)
}
}
func devicesList(mgmt *androidmanagement.Service, enterpriseID string) {
if enterpriseID == "" {
log.Fatalf("enterprise_id must be set")
}
result, err := mgmt.Enterprises.Devices.List("enterprises/" + enterpriseID).Do()
if err != nil {
log.Fatalf("Error listing devices: %v", err)
}
if len(result.Devices) == 0 {
log.Printf("No policies found")
return
}
for _, device := range result.Devices {
data, err := json.Marshal(device, jsontext.WithIndent(" "))
if err != nil {
log.Fatalf("Error marshalling device: %v", err)
}
log.Println(string(data))
}
log.Printf("Total devices: %d", len(result.Devices))
}
func devicesDelete(mgmt *androidmanagement.Service, enterpriseID string, deviceID string) {
if enterpriseID == "" || deviceID == "" {
log.Fatalf("enterprise_id and device_id must be set")
}
_, err := mgmt.Enterprises.Devices.Delete("enterprises/" + enterpriseID + "/devices/" + deviceID).Do()
if err != nil {
log.Fatalf("Error listing devices: %v", err)
}
log.Printf("Device %s deleted", deviceID)
}
func devicesRelinquishOwnership(mgmt *androidmanagement.Service, enterpriseID, deviceID string) {
if enterpriseID == "" || deviceID == "" {
log.Fatalf("enterprise_id and device_id must be set")
}
operation, err := mgmt.Enterprises.Devices.IssueCommand("enterprises/"+enterpriseID+"/devices/"+deviceID, &androidmanagement.Command{
Type: "RELINQUISH_OWNERSHIP",
}).Do()
if err != nil {
log.Fatalf("Error issuing command: %v", err)
}
data, err := json.Marshal(operation, jsontext.WithIndent(" "))
if err != nil {
log.Fatalf("Error marshalling operation: %v", err)
}
log.Println(string(data))
}
func enterprisesWebTokensCreate(mgmt *androidmanagement.Service, enterpriseID string) {
if enterpriseID == "" {
log.Fatalf("enterprise_id must be set")
}
webToken := &androidmanagement.WebToken{
ParentFrameUrl: "https://example.com",
Permissions: []string{"APPROVE_APPS"},
}
result, err := mgmt.Enterprises.WebTokens.Create("enterprises/"+enterpriseID, webToken).Do()
if err != nil {
log.Fatalf("Error creating web token: %v", err)
}
data, err := json.Marshal(result, jsontext.WithIndent(" "))
if err != nil {
log.Fatalf("Error marshalling web token: %v", err)
}
log.Println(string(data))
// Construct and display the complete URL
if result.Value != "" {
playURL := "https://play.google.com/work/embedded/search?token=" + result.Value
log.Printf("\nComplete Play Store URL:\n%s\n", playURL)
}
}