Fix host duplicated hosts, auto-enroll, sync with server MC (#36414)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user