1 package com.android.onboarding.bedsteadonboarding.permissions 2 3 import android.content.Context 4 import android.os.Build 5 import android.util.Log 6 7 /** 8 * Security Mechanism implemented is as follows: 9 * 10 * Only instrumented apps running on a debuggable device should be able to set Test configs using 11 * [TestContentProvider]. Any app including production apps can query [TestContentProvider] to fetch 12 * Test Configs. In production scenarios, no configuration will be set, and so no test logic should 13 * be executed. 14 * 15 * However since [TestContentProvider] is exported so we want to restrict who can call it to set 16 * test configs. So using [TestPermissions#canCallerExecuteTestFunctionality()] we will check if the 17 * device has debuggable android build and if so then whether the caller uid is that of an 18 * instrumented app. Only if both are true will we allow the caller to manipulate test configs. 19 */ 20 object TestPermissions { 21 22 private const val TAG = "TestPermissions" 23 24 /** 25 * Returns true if the calling uid is permitted to utilise test-only functionality in Onboarding. 26 * 27 * This will only return true in situations where it is safe for the caller to use test-only 28 * functionality. Additional security checks (such as for rooted devices) should not be performed 29 * if this returns true. 30 * 31 * It is not possible to query in-general if we are running within a test. The standard pattern is 32 * to expose some test-only capability (guarded with this method) which sets state that can be 33 * queried later. For example, exposing a test-only API to override an allow-list - rather than 34 * having the production code skip the allowlist when running in a test. 35 * 36 * @param context context of the current application from which the function is called. 37 * @param uid userid of the caller against which to check if they have permissions to execute 38 * test-only functionality in Onboarding. 39 */ canCallerExecuteTestFunctionalitynull40 fun canCallerExecuteTestFunctionality(context: Context, uid: Int): Boolean = 41 isRunningOnDebuggableDevice() && uidIsInstrumented(context, uid) 42 43 /** 44 * Returns true if the code is executed during a Robolectric test or on a real device with 45 * debugging enabled. 46 */ 47 fun isRunningOnDebuggableDevice(): Boolean { 48 return try { 49 ("robolectric" == Build.FINGERPRINT) || 50 Build::class.java.getDeclaredField("IS_DEBUGGABLE").get(null) as Boolean 51 } catch (t: Throwable) { 52 Log.e( 53 TAG, 54 "Test Permission not granted since if the build is debuggable could not be determined", 55 ) 56 false 57 } 58 } 59 60 /** 61 * Returns if the caller with userid [uid] is associated with an instrumented app. 62 * 63 * @param context context of the current application from which the function is called. 64 * @param uid userid of the caller for which to check if the associated process is instrumented. 65 */ uidIsInstrumentednull66 fun uidIsInstrumented(context: Context, uid: Int): Boolean { 67 try { 68 // Get all the app packages associated with [uid]. 69 val packagesForUid = context.packageManager.getPackagesForUid(uid) ?: arrayOf() 70 for (packageForUid in packagesForUid) { 71 // Check if for the [packageForUid], there exists any [InstrumentationInfo]. This will 72 // only be the case when the app with userId [packageForUid] is instrumented. 73 val instrumentationInfo = 74 context.packageManager.queryInstrumentation( 75 packageForUid, 76 /** flags= */ 77 0, 78 ) 79 // Returns true if at least one package with [uid] is instrumented. 80 if (instrumentationInfo.isNotEmpty()) return true 81 } 82 } catch (t: Throwable) { 83 Log.e(TAG, "Got error while determining if the caller is an instrumented app", t) 84 } 85 return false 86 } 87 } 88