Fix host duplicated hosts, auto-enroll, sync with server MC (#36414)

This commit is contained in:
Dante Catalfamo
2025-12-02 14:32:09 -05:00
committed by GitHub
parent bbf8510981
commit fd949a9fc3
10 changed files with 338 additions and 97 deletions
@@ -1,11 +1,18 @@
package com.fleetdm.agent
import android.app.Application
import android.content.Context
import android.content.RestrictionsManager
import android.os.Build
import android.util.Log
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
/**
* Custom Application class for Fleet Agent.
@@ -17,12 +24,53 @@ class AgentApplication : Application() {
private const val CONFIG_CHECK_WORK_NAME = "config_check_periodic"
}
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
override fun onCreate() {
super.onCreate()
Log.i(TAG, "Fleet agent process started")
ApiClient.initialize(this)
refreshEnrollmentCredentials()
schedulePeriodicConfigCheck()
}
private fun refreshEnrollmentCredentials() {
applicationScope.launch {
try {
val restrictionsManager = getSystemService(Context.RESTRICTIONS_SERVICE)
as? RestrictionsManager
val appRestrictions = restrictionsManager?.applicationRestrictions ?: return@launch
val enrollSecret = appRestrictions.getString("enrollSecret")
val hostUUID = appRestrictions.getString("hostUUID")
val serverURL = appRestrictions.getString("serverURL")
if (enrollSecret != null && hostUUID != null && serverURL != null) {
Log.d(TAG, "Refreshing enrollment credentials from MDM config")
ApiClient.setEnrollmentCredentials(
enrollSecret = enrollSecret,
hardwareUUID = hostUUID,
baseUrl = serverURL,
computerName = "${Build.BRAND} ${Build.MODEL}",
)
// Trigger auto-enrollment if node key is missing
// This also fetches initial orbit config
val configResult = ApiClient.getOrbitConfig()
configResult.onSuccess {
Log.d(TAG, "Successfully enrolled and fetched initial orbit config")
}.onFailure { error ->
Log.w(TAG, "Auto-enrollment on startup failed: ${error.message}")
}
} else {
Log.d(TAG, "MDM enrollment credentials not available")
}
} catch (e: Exception) {
Log.e(TAG, "Error refreshing enrollment credentials", e)
}
}
}
private fun schedulePeriodicConfigCheck() {
val workRequest =
PeriodicWorkRequestBuilder<ConfigCheckWorker>(
@@ -13,11 +13,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
private val Context.credentialStore: DataStore<Preferences> by preferencesDataStore(name = "api_credentials")
@@ -27,8 +30,14 @@ object ApiClient {
private lateinit var dataStore: DataStore<Preferences>
private val API_KEY = stringPreferencesKey("api_key")
private val BASE_URL_KEY = stringPreferencesKey("base_url")
private val ENROLL_SECRET = stringPreferencesKey("enroll_secret")
private val HARDWARE_UUID = stringPreferencesKey("hardware_uuid")
private val COMPUTER_NAME = stringPreferencesKey("computer_name")
private val enrollmentMutex = Mutex()
fun initialize(context: Context) {
Log.d("fleet-apiClient", "initializing api client")
if (!::dataStore.isInitialized) {
dataStore = context.applicationContext.credentialStore
}
@@ -40,7 +49,7 @@ object ApiClient {
}
}
private suspend fun setBaseUrl(url: String) {
suspend fun setBaseUrl(url: String) {
dataStore.edit { preferences ->
preferences[BASE_URL_KEY] = url
}
@@ -60,23 +69,15 @@ object ApiClient {
suspend fun getBaseUrl(): String? = dataStore.data.first()[BASE_URL_KEY]
suspend fun <R, T> makeRequest(
private suspend fun <R, T> makeRequest(
endpoint: String,
method: String = "GET",
body: R? = null,
authenticated: Boolean = true,
bodySerializer: KSerializer<R>,
bodySerializer: KSerializer<R>? = null,
responseSerializer: KSerializer<T>,
): Result<T> = withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
try {
val apiKey = getApiKey()
if (authenticated && apiKey == null) {
return@withContext Result.failure(
Exception("API key not configured"),
)
}
val baseUrl = getBaseUrl() ?: return@withContext Result.failure(
Exception("Base URL not configured"),
)
@@ -102,16 +103,13 @@ object ApiClient {
requestMethod = method
useCaches = false
doInput = true
if (authenticated) {
setRequestProperty("Authorization", "Bearer $apiKey")
}
setRequestProperty("Content-Type", "application/json")
connectTimeout = 15000
readTimeout = 15000
if (body != null && method != "GET") {
doOutput = true
val bodyJson = json.encodeToString(value = body, serializer = bodySerializer)
val bodyJson = json.encodeToString(value = body, serializer = bodySerializer!!)
outputStream.use { it.write(bodyJson.toByteArray()) }
}
}
@@ -137,17 +135,18 @@ object ApiClient {
}
}
suspend fun enroll(baseUrl: String, enrollSecret: String, hardwareUUID: String, computerName: String): Result<EnrollResponse> {
setBaseUrl(baseUrl)
suspend fun enroll(): Result<EnrollResponse> {
val credentials = getEnrollmentCredentials()
credentials ?: return Result.failure(Exception("Credentials not set"))
val resp = makeRequest(
endpoint = "/api/fleet/orbit/enroll",
method = "POST",
body = EnrollRequest(
enrollSecret = enrollSecret,
hardwareUUID = hardwareUUID,
computerName = computerName,
enrollSecret = credentials.enrollSecret,
hardwareUUID = credentials.hardwareUUID,
hardwareSerial = credentials.hardwareUUID,
computerName = credentials.computerName,
),
authenticated = false,
bodySerializer = EnrollRequest.serializer(),
responseSerializer = EnrollResponse.serializer(),
)
@@ -160,6 +159,84 @@ object ApiClient {
return resp
}
suspend fun getOrbitConfig(): Result<OrbitConfig> {
val nodeKeyResult = getNodeKeyOrEnroll()
val orbitNodeKey = nodeKeyResult.getOrElse { error ->
return Result.failure(error)
}
return makeRequest(
endpoint = "/api/fleet/orbit/config",
method = "POST",
body = GetConfigRequest(orbitNodeKey = orbitNodeKey),
bodySerializer = GetConfigRequest.serializer(),
responseSerializer = OrbitConfig.serializer(),
)
}
suspend fun setEnrollmentCredentials(enrollSecret: String, hardwareUUID: String, computerName: String, baseUrl: String) {
dataStore.edit { preferences ->
preferences[ENROLL_SECRET] = enrollSecret
preferences[HARDWARE_UUID] = hardwareUUID
preferences[COMPUTER_NAME] = computerName
preferences[BASE_URL_KEY] = baseUrl
}
}
private suspend fun getEnrollmentCredentials(): EnrollmentCredentials? {
val prefs = dataStore.data.first()
val enrollSecret = prefs[ENROLL_SECRET]
val hardwareUUID = prefs[HARDWARE_UUID]
val computerName = prefs[COMPUTER_NAME]
val baseUrl = prefs[BASE_URL_KEY]
if (enrollSecret == null || hardwareUUID == null || computerName == null || baseUrl == null) {
return null
}
return EnrollmentCredentials(
baseUrl = baseUrl,
enrollSecret = enrollSecret,
hardwareUUID = hardwareUUID,
computerName = computerName,
)
}
private suspend fun getNodeKeyOrEnroll(): Result<String> {
enrollmentMutex.withLock {
// Check again inside lock in case another coroutine just enrolled
val existingKey = getApiKey()
if (existingKey != null) {
return Result.success(existingKey)
}
// Node key is missing, attempt auto-enrollment
Log.d("ApiClient", "Orbit node key missing, attempting auto-enrollment")
// Re-enroll
val enrollResult = enroll()
return enrollResult.fold(
onSuccess = { response ->
Log.d("ApiClient", "Auto-enrollment successful")
Result.success(response.orbitNodeKey)
},
onFailure = { error ->
Log.e("ApiClient", "Auto-enrollment failed: ${error.message}")
Result.failure(error)
},
)
}
}
private data class EnrollmentCredentials(
val baseUrl: String,
val enrollSecret: String,
val hardwareUUID: String,
val computerName: String,
)
}
@Serializable
@@ -168,6 +245,8 @@ data class EnrollRequest(
val enrollSecret: String,
@SerialName("hardware_uuid")
val hardwareUUID: String,
@SerialName("hardware_serial")
val hardwareSerial: String,
@SerialName("platform")
val platform: String = "android",
@SerialName("computer_name")
@@ -179,3 +258,78 @@ data class EnrollResponse(
@SerialName("orbit_node_key")
val orbitNodeKey: String,
)
@Serializable
private data class GetConfigRequest(
@SerialName("orbit_node_key")
val orbitNodeKey: String,
)
@Serializable
data class OrbitConfig(
@SerialName("script_execution_timeout")
val scriptExecutionTimeout: Int = 0,
@SerialName("command_line_startup_flags")
val commandLineStartupFlags: JsonElement? = null,
@SerialName("extensions")
val extensions: JsonElement? = null,
@SerialName("nudge_config")
val nudgeConfig: JsonElement? = null,
@SerialName("notifications")
val notifications: OrbitConfigNotifications = OrbitConfigNotifications(),
@SerialName("update_channels")
val updateChannels: OrbitUpdateChannels? = null,
)
@Serializable
data class OrbitConfigNotifications(
@SerialName("pending_script_execution_ids")
val pendingScriptExecutionIDs: List<String> = emptyList(),
@SerialName("pending_software_installer_ids")
val pendingSoftwareInstallerIDs: List<String> = emptyList(),
@SerialName("renew_enrollment_profile")
val renewEnrollmentProfile: Boolean = false,
@SerialName("rotate_disk_encryption_key")
val rotateDiskEncryptionKey: Boolean = false,
@SerialName("needs_mdm_migration")
val needsMDMMigration: Boolean = false,
@SerialName("run_setup_experience")
val runSetupExperience: Boolean = false,
@SerialName("run_disk_encryption_escrow")
val runDiskEncryptionEscrow: Boolean = false,
@SerialName("needs_programmatic_windows_mdm_enrollment")
val needsProgrammaticWindowsMDMEnrollment: Boolean = false,
@SerialName("windows_mdm_discovery_endpoint")
val windowsMDMDiscoveryEndpoint: String = "",
@SerialName("needs_programmatic_windows_mdm_unenrollment")
val needsProgrammaticWindowsMDMUnenrollment: Boolean = false,
@SerialName("enforce_bitlocker_encryption")
val enforceBitLockerEncryption: Boolean = false,
)
@Serializable
data class OrbitUpdateChannels(
@SerialName("orbit")
val orbit: String = "",
@SerialName("osqueryd")
val osqueryd: String = "",
@SerialName("desktop")
val desktop: String = "",
)
@@ -2,19 +2,28 @@ package com.fleetdm.agent
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
/**
* WorkManager worker that periodically checks managed configurations.
*/
class ConfigCheckWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
class ConfigCheckWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "fleet-worker"
}
override fun doWork(): Result {
override suspend fun doWork(): Result {
Log.i(TAG, "Periodic config check triggered")
val configResult = ApiClient.getOrbitConfig()
configResult.onSuccess { config ->
Log.d(TAG, "Successfully fetched orbit config")
}.onFailure { error ->
Log.e(TAG, "Failed to fetch orbit config: ${error.message}", error)
}
return Result.success()
}
}
@@ -25,7 +25,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -55,16 +54,14 @@ class MainActivity : ComponentActivity() {
val appRestrictions = restrictionsManager.applicationRestrictions
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
ApiClient.initialize(this)
setContent {
val enrollSecret by remember { mutableStateOf(appRestrictions.getString("enrollSecret")) }
val delegatedScopes by remember { mutableStateOf(dpm.getDelegatedScopes(null, packageName)) }
val delegatedScopes by remember { mutableStateOf(dpm.getDelegatedScopes(null, packageName).toList()) }
val delegatedCertScope by remember {
mutableStateOf(delegatedScopes.contains(DevicePolicyManager.DELEGATION_CERT_INSTALL))
}
val androidID by remember { mutableStateOf(Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)) }
val enrollmentSpecificID by remember { mutableStateOf(appRestrictions.getString("enrollmentSpecificID")) }
val enrollmentSpecificID by remember { mutableStateOf(appRestrictions.getString("hostUUID")) }
val certRequestList by remember {
mutableStateOf(appRestrictions.getParcelableArray("certificates", Bundle::class.java)?.toList())
}
@@ -83,16 +80,14 @@ class MainActivity : ComponentActivity() {
}
}
}
mutableStateOf(grantedPermissions)
mutableStateOf(grantedPermissions.toList())
}
val fleetBaseUrl by remember {
mutableStateOf(appRestrictions.getString("fleetBaseUrl"))
mutableStateOf(appRestrictions.getString("serverURL"))
}
var enrollBody by remember { mutableStateOf("enroll not run") }
var installedCertificates: List<CertificateInfo> by remember { mutableStateOf(listOf()) }
val apiKey by ApiClient.apiKeyFlow.collectAsState(initial = null)
val baseUrl by ApiClient.baseUrlFlow.collectAsState(initial = null)
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
installedCertificates = listKeystoreCertificates()
@@ -107,42 +102,20 @@ class MainActivity : ComponentActivity() {
) {
StatusScreen()
KeyValue("packageName", packageName)
KeyValue("versionName", packageManager.getPackageInfo(packageName, 0).versionName)
KeyValue("longVersionCode", packageManager.getPackageInfo(packageName, 0).longVersionCode.toString())
KeyValue("enrollSecret", enrollSecret)
KeyValue("delegatedScopes", delegatedScopes.toString())
KeyValue("delegated cert scope", delegatedCertScope.toString())
KeyValue("android id", androidID)
KeyValue("enrollmentSpecificID (MC)", enrollmentSpecificID)
KeyValue("fleetBaseUrl (MC)", fleetBaseUrl)
KeyValue("hostUUID (MC)", enrollmentSpecificID)
KeyValue("serverURL (MC)", fleetBaseUrl)
KeyValue("orbit_node_key (datastore)", apiKey)
KeyValue("base_url (datastore)", baseUrl)
KeyValue("certificate_ids", certIds.toString())
PermissionList(
permissionsList = permissionsList,
)
Button(onClick = {
scope.launch {
enrollBody = "launched!!"
if (enrollSecret == null) {
enrollBody = "no enroll secret"
}
if (fleetBaseUrl == null) {
enrollBody = "no fleet URL"
}
try {
Log.d("main_activity", "sending request!")
val resp = ApiClient.enroll(
baseUrl = fleetBaseUrl ?: "",
enrollSecret = enrollSecret ?: "",
hardwareUUID = enrollmentSpecificID ?: "",
computerName = Build.MODEL,
)
enrollBody = resp.toString()
} catch (e: Exception) {
enrollBody = e.toString()
}
}
}) { Text("enroll") }
Text(enrollBody)
CertificateList(certificateList = installedCertificates)
}
},
+5 -5
View File
@@ -3,14 +3,14 @@
<string name="enroll_secret_title">Enroll Secret</string>
<string name="enroll_secret_description">Secret used to enroll in a fleet instance</string>
<string name="fleet_base_url_title">Fleet Base URL</string>
<string name="fleet_base_url_description">The base URL of the fleet server</string>
<string name="server_url_title">Fleet Base URL</string>
<string name="server_url_description">The base URL of the fleet server</string>
<string name="certificates_title">Certificates</string>
<string name="certificates_description">Array of bundles containing certificate information</string>
<string name="certificate_title">Certificate</string>
<string name="certificate_description">Certificate information</string>
<string name="certificate_id_title">Certificate ID</string>
<string name="certificate_id_description">Certificate ID to be requested</string>
<string name="enrollment_specific_id_title">Enrollment-Specific ID</string>
<string name="enrollment_specific_id_description">The enrollment-specific ID for this device</string>
</resources>
<string name="host_uuid_title">Host UUID</string>
<string name="host_uuid_description">The host UUID to present to fleet during enrollment</string>
</resources>
@@ -8,16 +8,16 @@
android:description="@string/enroll_secret_description" />
<restriction
android:key="fleetBaseUrl"
android:title="@string/fleet_base_url_title"
android:key="serverURL"
android:title="@string/server_url_title"
android:restrictionType="string"
android:description="@string/fleet_base_url_description" />
android:description="@string/server_url_description" />
<restriction
android:key="enrollmentSpecificID"
android:title="@string/enrollment_specific_id_title"
android:key="hostUUID"
android:title="@string/host_uuid_title"
android:restrictionType="string"
android:description="@string/enrollment_specific_id_description" />
android:description="@string/host_uuid_description" />
<restriction
android:key="certificates"
@@ -36,4 +36,4 @@
android:restrictionType="integer" />
</restriction>
</restriction>
</restrictions>
</restrictions>
@@ -9,6 +9,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import kotlinx.coroutines.runBlocking
@RunWith(RobolectricTestRunner::class)
class ConfigCheckWorkerTest {
@@ -17,6 +18,7 @@ class ConfigCheckWorkerTest {
@Before
fun setUp() {
context = RuntimeEnvironment.getApplication()
ApiClient.initialize(context)
}
@Test
@@ -26,7 +28,9 @@ class ConfigCheckWorkerTest {
.build()
// Execute the worker
val result = worker.doWork()
val result = runBlocking {
worker.doWork()
}
assertEquals(ListenableWorker.Result.success(), result)
}
}
+3 -3
View File
@@ -2130,8 +2130,8 @@ type enrolledHostInfo struct {
// guaranteed to match a single host. For that reason, we only attempt the
// serial number lookup if Fleet MDM is enabled on the server (as we must be
// able to match by serial in this scenario, since this is the only information
// we get when enrolling hosts via Apple DEP) AND if the matched host is on the
// macOS platform (darwin).
// we get when enrolling hosts via Apple DEP or Android EMM) AND if the matched
// host is on a supported MDM platform (darwin, ios, ipados, or android).
func matchHostDuringEnrollment(
ctx context.Context,
q sqlx.QueryerContext,
@@ -2176,7 +2176,7 @@ func matchHostDuringEnrollment(
if query.Len() > 0 {
_, _ = query.WriteString(" UNION ")
}
_, _ = query.WriteString(fmt.Sprintf(`(SELECT id, last_enrolled_at, %s IS NOT NULL AS node_key_set, 2 priority, platform FROM hosts WHERE hardware_serial = ? AND (platform = 'darwin' OR platform = 'ios' OR platform = 'ipados') ORDER BY id LIMIT 1)`, nodeKeyColumn))
_, _ = query.WriteString(fmt.Sprintf(`(SELECT id, last_enrolled_at, %s IS NOT NULL AS node_key_set, 2 priority, platform FROM hosts WHERE hardware_serial = ? AND (platform = 'darwin' OR platform = 'ios' OR platform = 'ipados' OR platform = 'android') ORDER BY id LIMIT 1)`, nodeKeyColumn))
args = append(args, serial)
}
+6 -6
View File
@@ -10205,16 +10205,16 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) {
// Scenario C:
// - Fleet with MDM enabled.
// - Two linux|darwin|windows hosts with the same hardware identifiers (e.g. two cloned VMs).
// - Two linux|darwin|windows|android hosts with the same hardware identifiers (e.g. two cloned VMs).
// - fleetd running with host identifier set to instance.
// - orbit and osquery of the two hosts enroll in mixed order.
//
// For Linux and Windows this scenario behaves as expected. The two hosts are enrolled separately.
//
// For macOS:
// For macOS, iOS, iPadOS, and Android:
// Somewhat unexpected output of this scenario is that two hosts are enrolled as one
// because MDM makes the effort to match by hardware serial.
// Using fleetd's `--host-identifier=instance` with Fleet's MDM enabled is not compatible on macOS.
// Using fleetd's `--host-identifier=instance` with Fleet's MDM enabled is not compatible on these platforms.
scenarioC := func(platform string) {
dupUUID := uuid.New().String()
dupHWSerial := uuid.New().String()
@@ -10264,15 +10264,15 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Equal(t, h2Orbit.ID, h2Osquery.ID)
if platform == "darwin" {
if platform == "darwin" || platform == "ios" || platform == "ipados" || platform == "android" {
// This is a expected output of this scenario because MDM makes
// the effort to match by hardware serial.
// the effort to match by hardware serial for these platforms.
require.Equal(t, h1Orbit.ID, h2Orbit.ID)
} else {
require.NotEqual(t, h1Orbit.ID, h2Orbit.ID)
}
}
for _, platform := range []string{"ubuntu", "windows", "darwin"} {
for _, platform := range []string{"ubuntu", "windows", "darwin", "ios", "ipados", "android"} {
platform := platform
t.Run("scenarioC_"+platform, func(t *testing.T) {
scenarioC(platform)
+69 -16
View File
@@ -3,8 +3,10 @@ package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"slices"
"strings"
"github.com/go-json-experiment/json"
@@ -19,6 +21,28 @@ var (
androidProjectID string
)
const (
cmdEnterprisesDelete = "enterprises.delete"
cmdEnterprisesList = "enterprises.list"
cmdEnterprisesWebTokensCreate = "enterprises.webTokens.create"
cmdApplicationsGet = "applications.get"
cmdPoliciesList = "policies.list"
cmdDevicesList = "devices.list"
cmdDevicesDelete = "devices.delete"
cmdDevicesRelinquish = "devices.issueCommand.RELINQUISH_OWNERSHIP"
)
var commands = []string{
cmdEnterprisesDelete,
cmdEnterprisesList,
cmdEnterprisesWebTokensCreate,
cmdApplicationsGet,
cmdPoliciesList,
cmdDevicesList,
cmdDevicesDelete,
cmdDevicesRelinquish,
}
func main() {
if androidServiceCredentials == "" {
log.Fatal("FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS must be set")
@@ -37,17 +61,26 @@ func main() {
log.Fatal("project_id not found in android service credentials")
}
command := flag.String("command", "", "")
command := flag.String("command", "", strings.Join(commands, "\n"))
enterpriseID := flag.String("enterprise_id", "", "")
deviceID := flag.String("device_id", "", "")
policyID := flag.String("policy_id", "", "")
flag.Parse()
if !slices.Contains(commands, *command) {
flag.Usage()
os.Exit(1)
}
// Normalize enterprise_id by stripping "enterprises/" prefix if present
if *enterpriseID != "" {
*enterpriseID = strings.TrimPrefix(*enterpriseID, "enterprises/")
}
if slices.Index(commands, *command) == -1 {
log.Fatalf("Command must be one of: %s", strings.Join(commands, ", "))
}
ctx := context.Background()
mgmt, err := androidmanagement.NewService(ctx, option.WithCredentialsJSON([]byte(androidServiceCredentials)))
if err != nil {
@@ -55,21 +88,23 @@ func main() {
}
switch *command {
case "enterprises.delete":
case cmdEnterprisesDelete:
enterprisesDelete(mgmt, *enterpriseID)
case "enterprises.list":
case cmdEnterprisesList:
enterprisesList(mgmt)
case "enterprises.webTokens.create":
case cmdEnterprisesWebTokensCreate:
enterprisesWebTokensCreate(mgmt, *enterpriseID)
case "policies.list":
case cmdApplicationsGet:
applicationsGet(mgmt, *enterpriseID, flag.Arg(0))
case cmdPoliciesList:
policiesList(mgmt, *enterpriseID)
case "policies.delete":
policiesDelete(mgmt, *enterpriseID, *policyID)
case "devices.list":
case cmdDevicesList:
devicesList(mgmt, *enterpriseID)
case "devices.delete":
case cmdDevicesDelete:
devicesDelete(mgmt, *enterpriseID, *deviceID)
case "devices.issueCommand.RELINQUISH_OWNERSHIP":
case cmdDevicesRelinquish:
devicesRelinquishOwnership(mgmt, *enterpriseID, *deviceID)
default:
log.Fatalf("Unknown command: %s", *command)
@@ -113,9 +148,29 @@ func policiesList(mgmt *androidmanagement.Service, enterpriseID string) {
log.Printf("No policies found")
return
}
for _, policy := range result.Policies {
log.Printf("Policy: %+v", *policy)
b, err := json.Marshal(result.Policies, jsontext.WithIndent(" "))
if err != nil {
log.Fatalf("Error marshalling policies: %v", err)
}
fmt.Println(string(b))
}
func applicationsGet(mgmt *androidmanagement.Service, enterpriseID string, applicationID string) {
if enterpriseID == "" {
log.Fatalf("enterprise_id must be set")
}
if applicationID == "" {
log.Fatal("application ID argument missing")
}
result, err := mgmt.Enterprises.Applications.Get(fmt.Sprintf("enterprises/%s/applications/%s", enterpriseID, applicationID)).Do()
if err != nil {
log.Fatalf("Error getting application: %v", err)
}
b, err := json.Marshal(result, jsontext.WithIndent(" "))
if err != nil {
log.Fatalf("Error marshalling application: %v", err)
}
fmt.Println(string(b))
}
func policiesDelete(mgmt *androidmanagement.Service, enterpriseID, policyID string) {
@@ -141,13 +196,11 @@ func devicesList(mgmt *androidmanagement.Service, enterpriseID string) {
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))
b, err := json.Marshal(result.Devices, jsontext.WithIndent(" "))
if err != nil {
log.Fatalf("Error marshalling devices: %v", err)
}
fmt.Println(string(b))
log.Printf("Total devices: %d", len(result.Devices))
}