Bootstrapping Android app (#36233)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #36202 

Updated how Android agent starts. See README updates.

# Checklist for submitter

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Periodic configuration check scheduled every 15 minutes in the Android
agent
* Improved Android management notification handling and app-role support

* **Documentation**
* Updated Android MDM deployment guide with SHA256 fingerprint
instructions and build configuration snippets

* **Chores**
* Added WorkManager and AMAPI SDK for Android; updated Android/Go
tooling and library versions

* **Tests**
  * Added unit test coverage for the periodic config worker

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky 2025-11-26 11:36:41 -06:00 committed by GitHub
parent 287710b1c5
commit 61c51672e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 259 additions and 81 deletions

View file

@ -123,6 +123,17 @@ jobs:
working-directory: android
run: chmod +x gradlew
- name: Free up disk space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
android: false
dotnet: true
haskell: true
swap-storage: true
large-packages: true
tool-cache: false
docker-images: false
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules

View file

@ -3,6 +3,7 @@
- [Requirements](#requirements)
- [Building the project](#building-the-project)
- [Deploying via Android MDM](#deploying-via-android-mdm-development)
- [How the app starts](#how-the-app-starts)
- [Running tests](#running-tests)
- [Code quality](#code-quality)
- [Troubleshooting](#troubleshooting)
@ -94,14 +95,28 @@ ls "$(grep sdk.dir local.properties | cut -d= -f2)/build-tools/"
jarsigner -verify app/build/outputs/bundle/release/app-release.aab
```
### Getting the SHA256 fingerprint
The SHA256 fingerprint is required for MDM deployment. You can get it from your keystore.
```bash
keytool -list -v -keystore keystore.jks -alias fleet-android
# Grab SHA256
echo <SHA256> | xxd -r -p | base64
```
Copy the fingerprint for use in `FLEET_DEV_ANDROID_AGENT_SHA256`
## Deploying via Android MDM (development)
This feature is behind the feature flag `FLEET_DEV_ANDROID_AGENT_PACKAGE`. Requires `FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS` to be set in your workarea.
This feature is behind the feature flag `FLEET_DEV_ANDROID_AGENT_PACKAGE`. Requires `FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS` to be set in your workarea to get the Google Play URL.
1. **Set the feature flag on your Fleet server:**
1. **Set these env vars on your Fleet server:**
```bash
export FLEET_DEV_ANDROID_AGENT_PACKAGE=com.fleetdm.agent.private.<yourname>
export FLEET_DEV_ANDROID_AGENT_SHA256=<SHA256 fingerprint>
```
2. **Change the `applicationId` in `app/build.gradle.kts`:**
@ -130,6 +145,33 @@ go run tools/android/android.go --command enterprises.webTokens.create --enterpr
The agent should start installing shortly. Check Google Play in your Work profile. If it shows as pending, try restarting the device.
## How the app starts
The Fleet Android agent is designed to run automatically without user interaction. The app starts in three scenarios:
### 1. On installation (COMPANION_APP role)
When the app is installed via MDM, Android Device Policy assigns it the `COMPANION_APP` role. This triggers `RoleNotificationReceiverService`, which starts the app process and runs `AgentApplication.onCreate()`.
### 2. On device boot
When the device boots, `BootReceiver` receives the `ACTION_BOOT_COMPLETED` broadcast and starts the app process, triggering `AgentApplication.onCreate()`.
### 3. Periodically every 15 minutes
`AgentApplication.onCreate()` schedules a `ConfigCheckWorker` to run every 15 minutes using WorkManager. This ensures the app wakes up periodically even if the process is killed.
**Note:** WorkManager ensures reliable background execution. The work persists across device reboots and process death.
### Why not ACTION_APPLICATION_RESTRICTIONS_CHANGED?
We don't use `ACTION_APPLICATION_RESTRICTIONS_CHANGED` to detect MDM config changes because:
1. This broadcast can only be registered dynamically (not in the manifest)
2. On Android 14+, [context-registered broadcasts are queued when the app is in cached state](https://developer.android.com/about/versions/14/behavior-changes-all#pending-broadcasts-queued)
This means the broadcast won't wake the app immediately when configs change if the app is in the background. WorkManager polling every 15 minutes is the reliable solution for detecting config changes.
### Full build with tests
```bash

View file

@ -101,7 +101,11 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.amapi.sdk)
testImplementation(libs.junit)
testImplementation(libs.androidx.work.testing)
testImplementation(libs.robolectric)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))

View file

@ -2,6 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Package visibility for Android Device Policy (required for targetSdk 30+) -->
<!-- Source: https://developers.google.com/android/management/sdk-integration#add_queries -->
<queries>
<package android:name="com.google.android.apps.work.clouddpc" />
</queries>
<!-- REQUIRED for downloading certificates from remote server -->
<uses-permission android:name="android.permission.INTERNET" />
@ -9,6 +15,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".AgentApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -38,14 +45,15 @@
</intent-filter>
</receiver>
<!-- ACTION_APPLICATION_RESTRICTIONS_CHANGED Receiver -->
<receiver
android:name=".RestrictionsReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_RESTRICTIONS_CHANGED" />
</intent-filter>
</receiver>
<!-- AMAPI NotificationReceiverService for COMPANION_APP role -->
<service
android:name=".RoleNotificationReceiverService"
android:exported="true"
tools:ignore="ExportedService">
<meta-data
android:name="com.google.android.managementapi.notification.NotificationReceiverService.SERVICE_APP_ROLES"
android:value="" />
</service>
<!-- Service Declaration -->
<service

View file

@ -0,0 +1,43 @@
package com.fleetdm.agent
import android.app.Application
import android.util.Log
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
/**
* Custom Application class for Fleet Agent.
* Runs when the app process starts (triggered by broadcasts, not by user).
*/
class AgentApplication : Application() {
companion object {
private const val TAG = "fleet-app"
private const val CONFIG_CHECK_WORK_NAME = "config_check_periodic"
}
override fun onCreate() {
super.onCreate()
Log.i(TAG, "Fleet agent process started")
schedulePeriodicConfigCheck()
}
private fun schedulePeriodicConfigCheck() {
val workRequest =
PeriodicWorkRequestBuilder<ConfigCheckWorker>(
15, // 15 is the minimum
TimeUnit.MINUTES,
).build()
WorkManager
.getInstance(this)
.enqueueUniquePeriodicWork(
CONFIG_CHECK_WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
workRequest,
)
Log.i(TAG, "Scheduled periodic config check every 15 minutes")
}
}

View file

@ -6,7 +6,9 @@ import android.content.Intent
import android.util.Log
class BootReceiver : BroadcastReceiver() {
private val TAG = "CertCompanionBoot"
companion object {
private const val TAG = "fleet-boot"
}
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {

View file

@ -0,0 +1,20 @@
package com.fleetdm.agent
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
/**
* WorkManager worker that periodically checks managed configurations.
*/
class ConfigCheckWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
companion object {
private const val TAG = "fleet-worker"
}
override fun doWork(): Result {
Log.i(TAG, "Periodic config check triggered")
return Result.success()
}
}

View file

@ -1,38 +0,0 @@
package com.fleetdm.agent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
class RestrictionsReceiver : BroadcastReceiver() {
private val TAG = "CertCompanionRestrict"
private val CERT_DATA_KEY = "certificate_data"
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) {
Log.i(TAG, "Application restrictions changed. Checking for new certificate data.")
// Ensure context is not null before proceeding
context?.let {
// 1. Fetch the Managed Configuration (Application Restrictions)
val restrictionsManager = context.getSystemService(Context.RESTRICTIONS_SERVICE) as android.content.RestrictionsManager
val appRestrictions = restrictionsManager.applicationRestrictions
val certData = appRestrictions.getString(CERT_DATA_KEY)
if (!certData.isNullOrEmpty()) {
Log.d(TAG, "New certificate data found in restrictions.")
// 2. Start the service to handle the installation asynchronously
val serviceIntent = Intent(it, CertificateService::class.java).apply {
putExtra("CERT_DATA", certData)
}
it.startService(serviceIntent)
} else {
Log.d(TAG, "No relevant certificate data found for key '$CERT_DATA_KEY'.")
}
}
}
}
}

View file

@ -0,0 +1,24 @@
package com.fleetdm.agent
import android.util.Log
import com.google.android.managementapi.approles.AppRolesListener
import com.google.android.managementapi.approles.model.AppRolesSetRequest
import com.google.android.managementapi.approles.model.AppRolesSetResponse
import com.google.android.managementapi.notification.NotificationReceiverService
/**
* Service to receive notifications from Android Device Policy (ADP) for COMPANION_APP role.
* We need the service to force the app to run right after it is installed via MDM.
*/
class RoleNotificationReceiverService : NotificationReceiverService() {
companion object {
private const val TAG = "fleet-notification"
}
override fun getAppRolesListener(): AppRolesListener = object : AppRolesListener {
override fun onAppRolesSet(request: AppRolesSetRequest): AppRolesSetResponse {
Log.i(TAG, "App roles set by Android Device Policy")
return AppRolesSetResponse.getDefaultInstance()
}
}
}

View file

@ -0,0 +1,32 @@
package com.fleetdm.agent
import android.content.Context
import androidx.work.ListenableWorker
import androidx.work.testing.TestListenableWorkerBuilder
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class ConfigCheckWorkerTest {
private lateinit var context: Context
@Before
fun setUp() {
context = RuntimeEnvironment.getApplication()
}
@Test
fun testDoWork() {
val worker =
TestListenableWorkerBuilder<ConfigCheckWorker>(context)
.build()
// Execute the worker
val result = worker.doWork()
assertEquals(ListenableWorker.Result.success(), result)
}
}

View file

@ -10,6 +10,9 @@ activityCompose = "1.12.0"
composeBom = "2025.11.01"
spotless = "8.1.0"
detekt = "1.23.8"
workManager = "2.11.0"
amapi = "1.7.0"
robolectric = "4.16"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -26,6 +29,10 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" }
androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "workManager" }
amapi-sdk = { group = "com.google.android.libraries.enterprise.amapi", name = "amapi", version.ref = "amapi" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

20
go.mod
View file

@ -153,14 +153,14 @@ require (
golang.org/x/image v0.18.0
golang.org/x/mod v0.29.0
golang.org/x/net v0.47.0
golang.org/x/oauth2 v0.30.0
golang.org/x/oauth2 v0.33.0
golang.org/x/sync v0.18.0
golang.org/x/sys v0.38.0
golang.org/x/term v0.37.0
golang.org/x/text v0.31.0
golang.org/x/tools v0.38.0
google.golang.org/api v0.249.0
google.golang.org/grpc v1.75.0
google.golang.org/api v0.256.0
google.golang.org/grpc v1.76.0
gopkg.in/guregu/null.v3 v3.5.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
@ -171,9 +171,9 @@ require (
require (
cloud.google.com/go v0.120.0 // indirect
cloud.google.com/go/auth v0.16.5 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cyphar.com/go-pathrs v0.2.1 // indirect
dario.cat/mergo v1.0.0 // indirect
@ -255,7 +255,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm-tools v0.4.5 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/goreleaser/chglog v0.4.2 // indirect
github.com/goreleaser/fileglob v1.3.0 // indirect
@ -337,11 +337,11 @@ require (
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/protobuf v1.36.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect

49
go.sum
View file

@ -1,12 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=
@ -206,6 +206,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
@ -303,7 +305,12 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c h1:KqlxcP2nuOcMjudCvK0qME2K/aFBDH+xcvYv7HYQaYc=
github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -468,8 +475,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
@ -705,6 +712,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
@ -985,8 +994,8 @@ golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1059,8 +1068,8 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -1081,8 +1090,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w=
google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
@ -1092,18 +1101,18 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1115,8 +1124,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -7,9 +7,9 @@ import (
)
func TestGeneratePolicyFieldMask(t *testing.T) {
const expectedMask = `accountTypesWithManagementDisabled,addUserDisabled,adjustVolumeDisabled,advancedSecurityOverrides,alwaysOnVpnPackage,androidDevicePolicyTracks,appAutoUpdatePolicy,appFunctions,assistContentPolicy,autoDateAndTimeZone,autoTimeRequired,blockApplicationsEnabled,bluetoothConfigDisabled,bluetoothContactSharingDisabled,bluetoothDisabled,cameraAccess,cameraDisabled,cellBroadcastsConfigDisabled,choosePrivateKeyRules,complianceRules,createWindowsDisabled,credentialProviderPolicyDefault,credentialsConfigDisabled,crossProfilePolicies,dataRoamingDisabled,debuggingFeaturesAllowed,defaultPermissionPolicy,deviceConnectivityManagement,deviceOwnerLockScreenInfo,deviceRadioState,displaySettings,encryptionPolicy,ensureVerifyAppsEnabled,enterpriseDisplayNameVisibility,factoryResetDisabled,frpAdminEmails,funDisabled,installAppsDisabled,installUnknownSourcesAllowed,keyguardDisabled,keyguardDisabledFeatures,kioskCustomLauncherEnabled,kioskCustomization,locationMode,longSupportMessage,maximumTimeToLock,microphoneAccess,minimumApiLevel,mobileNetworksConfigDisabled,modifyAccountsDisabled,mountPhysicalMediaDisabled,name,networkEscapeHatchEnabled,networkResetDisabled,oncCertificateProviders,openNetworkConfiguration,outgoingBeamDisabled,outgoingCallsDisabled,passwordPolicies,passwordRequirements,permissionGrants,permittedAccessibilityServices,permittedInputMethods,persistentPreferredActivities,personalUsagePolicies,playStoreMode,policyEnforcementRules,preferentialNetworkService,printingPolicy,privateKeySelectionEnabled,recommendedGlobalProxy,removeUserDisabled,safeBootDisabled,screenCaptureDisabled,setUserIconDisabled,setWallpaperDisabled,setupActions,shareLocationDisabled,shortSupportMessage,skipFirstUseHintsEnabled,smsDisabled,statusBarDisabled,statusReportingSettings,stayOnPluggedModes,systemUpdate,tetheringConfigDisabled,uninstallAppsDisabled,unmuteMicrophoneDisabled,usageLog,usbFileTransferDisabled,usbMassStorageEnabled,version,vpnConfigDisabled,wifiConfigDisabled,wifiConfigsLockdownEnabled,wipeDataFlags,workAccountSetupConfig`
const expectedMask = `accountTypesWithManagementDisabled,addUserDisabled,adjustVolumeDisabled,advancedSecurityOverrides,alwaysOnVpnPackage,androidDevicePolicyTracks,appAutoUpdatePolicy,appFunctions,assistContentPolicy,autoDateAndTimeZone,autoTimeRequired,blockApplicationsEnabled,bluetoothConfigDisabled,bluetoothContactSharingDisabled,bluetoothDisabled,cameraAccess,cameraDisabled,cellBroadcastsConfigDisabled,choosePrivateKeyRules,complianceRules,createWindowsDisabled,credentialProviderPolicyDefault,credentialsConfigDisabled,crossProfilePolicies,dataRoamingDisabled,debuggingFeaturesAllowed,defaultApplicationSettings,defaultPermissionPolicy,deviceConnectivityManagement,deviceOwnerLockScreenInfo,deviceRadioState,displaySettings,encryptionPolicy,ensureVerifyAppsEnabled,enterpriseDisplayNameVisibility,factoryResetDisabled,frpAdminEmails,funDisabled,installAppsDisabled,installUnknownSourcesAllowed,keyguardDisabled,keyguardDisabledFeatures,kioskCustomLauncherEnabled,kioskCustomization,locationMode,longSupportMessage,maximumTimeToLock,microphoneAccess,minimumApiLevel,mobileNetworksConfigDisabled,modifyAccountsDisabled,mountPhysicalMediaDisabled,name,networkEscapeHatchEnabled,networkResetDisabled,oncCertificateProviders,openNetworkConfiguration,outgoingBeamDisabled,outgoingCallsDisabled,passwordPolicies,passwordRequirements,permissionGrants,permittedAccessibilityServices,permittedInputMethods,persistentPreferredActivities,personalUsagePolicies,playStoreMode,policyEnforcementRules,preferentialNetworkService,printingPolicy,privateKeySelectionEnabled,recommendedGlobalProxy,removeUserDisabled,safeBootDisabled,screenCaptureDisabled,setUserIconDisabled,setWallpaperDisabled,setupActions,shareLocationDisabled,shortSupportMessage,skipFirstUseHintsEnabled,smsDisabled,statusBarDisabled,statusReportingSettings,stayOnPluggedModes,systemUpdate,tetheringConfigDisabled,uninstallAppsDisabled,unmuteMicrophoneDisabled,usageLog,usbFileTransferDisabled,usbMassStorageEnabled,version,vpnConfigDisabled,wifiConfigDisabled,wifiConfigsLockdownEnabled,wipeDataFlags,workAccountSetupConfig`
mask := generatePolicyFieldMask()
assert.NotContains(t, mask, "omitempty")
assert.NotContains(t, mask, "applications")
assert.Equal(t, mask, expectedMask)
assert.Equal(t, expectedMask, mask)
}

View file

@ -891,6 +891,10 @@ func (svc *Service) AddFleetAgentToAndroidPolicy(ctx context.Context, enterprise
) error {
var errs []error
if packageName := os.Getenv("FLEET_DEV_ANDROID_AGENT_PACKAGE"); packageName != "" {
sha256Fingerprint := os.Getenv("FLEET_DEV_ANDROID_AGENT_SHA256")
if sha256Fingerprint == "" {
return ctxerr.New(ctx, "FLEET_DEV_ANDROID_AGENT_SHA256 must be set when FLEET_DEV_ANDROID_AGENT_PACKAGE is set")
}
for uuid, managedConfig := range hostConfigs {
policyName := fmt.Sprintf("%s/policies/%s", enterpriseName, uuid)
@ -907,6 +911,16 @@ func (svc *Service) AddFleetAgentToAndroidPolicy(ctx context.Context, enterprise
DefaultPermissionPolicy: "GRANT",
DelegatedScopes: []string{"CERT_INSTALL"},
ManagedConfiguration: managedConfigJSON,
SigningKeyCerts: []*androidmanagement.ApplicationSigningKeyCert{
{
SigningKeyCertFingerprintSha256: sha256Fingerprint,
},
},
Roles: []*androidmanagement.Role{
{
RoleType: "COMPANION_APP",
},
},
}
_, err = svc.androidAPIClient.EnterprisesPoliciesModifyPolicyApplications(ctx, policyName,
[]*androidmanagement.ApplicationPolicy{fleetAgentApp})