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