1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.phone;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.ArgumentMatchers.anyInt;
22 import static org.mockito.ArgumentMatchers.anyString;
23 import static org.mockito.ArgumentMatchers.eq;
24 import static org.mockito.ArgumentMatchers.nullable;
25 import static org.mockito.Mockito.when;
26 
27 import android.Manifest;
28 import android.app.AppOpsManager;
29 import android.content.Context;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageManager;
32 import android.content.res.Resources;
33 import android.location.LocationManager;
34 import android.os.Build;
35 import android.os.UserHandle;
36 import android.telephony.LocationAccessPolicy;
37 
38 import org.junit.Before;
39 import org.junit.Test;
40 import org.junit.runner.RunWith;
41 import org.junit.runners.Parameterized;
42 import org.mockito.Mock;
43 import org.mockito.MockitoAnnotations;
44 
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.List;
48 
49 @RunWith(Parameterized.class)
50 public class LocationAccessPolicyTest {
51     private static class Scenario {
52         static class Builder {
53             private int mAppSdkLevel;
54             private boolean mAppHasFineManifest = false;
55             private boolean mAppHasCoarseManifest = false;
56             private int mFineAppOp = AppOpsManager.MODE_IGNORED;
57             private int mCoarseAppOp = AppOpsManager.MODE_IGNORED;
58             private boolean mIsDynamicLocationEnabled;
59             private LocationAccessPolicy.LocationPermissionQuery mQuery;
60             private LocationAccessPolicy.LocationPermissionResult mExpectedResult;
61             private String mName;
62 
setAppSdkLevel(int appSdkLevel)63             public Builder setAppSdkLevel(int appSdkLevel) {
64                 mAppSdkLevel = appSdkLevel;
65                 return this;
66             }
67 
setAppHasFineManifest(boolean appHasFineManifest)68             public Builder setAppHasFineManifest(boolean appHasFineManifest) {
69                 mAppHasFineManifest = appHasFineManifest;
70                 return this;
71             }
72 
setAppHasCoarseManifest( boolean appHasCoarseManifest)73             public Builder setAppHasCoarseManifest(
74                     boolean appHasCoarseManifest) {
75                 mAppHasCoarseManifest = appHasCoarseManifest;
76                 return this;
77             }
78 
setFineAppOp(int fineAppOp)79             public Builder setFineAppOp(int fineAppOp) {
80                 mFineAppOp = fineAppOp;
81                 return this;
82             }
83 
setCoarseAppOp(int coarseAppOp)84             public Builder setCoarseAppOp(int coarseAppOp) {
85                 mCoarseAppOp = coarseAppOp;
86                 return this;
87             }
88 
setIsDynamicLocationEnabled( boolean isDynamicLocationEnabled)89             public Builder setIsDynamicLocationEnabled(
90                     boolean isDynamicLocationEnabled) {
91                 mIsDynamicLocationEnabled = isDynamicLocationEnabled;
92                 return this;
93             }
94 
setQuery( LocationAccessPolicy.LocationPermissionQuery query)95             public Builder setQuery(
96                     LocationAccessPolicy.LocationPermissionQuery query) {
97                 mQuery = query;
98                 return this;
99             }
100 
setExpectedResult( LocationAccessPolicy.LocationPermissionResult expectedResult)101             public Builder setExpectedResult(
102                     LocationAccessPolicy.LocationPermissionResult expectedResult) {
103                 mExpectedResult = expectedResult;
104                 return this;
105             }
106 
setName(String name)107             public Builder setName(String name) {
108                 mName = name;
109                 return this;
110             }
111 
build()112             public Scenario build() {
113                 return new Scenario(mAppSdkLevel, mAppHasFineManifest, mAppHasCoarseManifest,
114                         mFineAppOp, mCoarseAppOp, mIsDynamicLocationEnabled, mQuery,
115                         mExpectedResult, mName);
116             }
117         }
118         int appSdkLevel;
119         boolean appHasFineManifest;
120         boolean appHasCoarseManifest;
121         int fineAppOp;
122         int coarseAppOp;
123         boolean isDynamicLocationEnabled;
124         LocationAccessPolicy.LocationPermissionQuery query;
125         LocationAccessPolicy.LocationPermissionResult expectedResult;
126         String name;
127 
Scenario(int appSdkLevel, boolean appHasFineManifest, boolean appHasCoarseManifest, int fineAppOp, int coarseAppOp, boolean isDynamicLocationEnabled, LocationAccessPolicy.LocationPermissionQuery query, LocationAccessPolicy.LocationPermissionResult expectedResult, String name)128         private Scenario(int appSdkLevel, boolean appHasFineManifest, boolean appHasCoarseManifest,
129                 int fineAppOp, int coarseAppOp,
130                 boolean isDynamicLocationEnabled,
131                 LocationAccessPolicy.LocationPermissionQuery query,
132                 LocationAccessPolicy.LocationPermissionResult expectedResult,
133                 String name) {
134             this.appSdkLevel = appSdkLevel;
135             this.appHasFineManifest = appHasFineManifest;
136             this.appHasCoarseManifest = appHasFineManifest || appHasCoarseManifest;
137             this.fineAppOp = fineAppOp;
138             this.coarseAppOp = coarseAppOp == AppOpsManager.MODE_ALLOWED ? coarseAppOp : fineAppOp;
139             this.isDynamicLocationEnabled = isDynamicLocationEnabled;
140             this.query = query;
141             this.expectedResult = expectedResult;
142             this.name = name;
143         }
144 
145         @Override
toString()146         public String toString() {
147             return name;
148         }
149     }
150 
151     private static final int TESTING_UID = 10001;
152     private static final int TESTING_PID = 8009;
153     private static final String TESTING_CALLING_PACKAGE = "com.android.phone";
154 
155     @Mock Context mContext;
156     @Mock AppOpsManager mAppOpsManager;
157     @Mock LocationManager mLocationManager;
158     @Mock PackageManager mPackageManager;
159     @Mock Resources mResources;
160     Scenario mScenario;
161 
162     @Before
setUp()163     public void setUp() {
164         MockitoAnnotations.initMocks(this);
165         mockContextSystemService(AppOpsManager.class, mAppOpsManager);
166         mockContextSystemService(LocationManager.class, mLocationManager);
167         mockContextSystemService(PackageManager.class, mPackageManager);
168         when(mContext.getPackageManager()).thenReturn(mPackageManager);
169         when(mContext.getResources()).thenReturn(mResources);
170         when(mResources.getStringArray(
171                 com.android.internal.R.array.config_serviceStateLocationAllowedPackages))
172                 .thenReturn(new String[]{TESTING_CALLING_PACKAGE});
173     }
174 
mockContextSystemService(Class<T> clazz , T obj)175     private <T> void mockContextSystemService(Class<T> clazz , T obj) {
176         when(mContext.getSystemServiceName(eq(clazz))).thenReturn(clazz.getSimpleName());
177         when(mContext.getSystemService(clazz.getSimpleName())).thenReturn(obj);
178     }
179 
LocationAccessPolicyTest(Scenario scenario)180     public LocationAccessPolicyTest(Scenario scenario) {
181         mScenario = scenario;
182     }
183 
184 
185     @Test
test()186     public void test() {
187         setupScenario(mScenario);
188         assertEquals(mScenario.expectedResult,
189                 LocationAccessPolicy.checkLocationPermission(mContext, mScenario.query));
190     }
191 
setupScenario(Scenario s)192     private void setupScenario(Scenario s) {
193         when(mContext.checkPermission(eq(Manifest.permission.ACCESS_FINE_LOCATION),
194                 anyInt(), anyInt())).thenReturn(s.appHasFineManifest
195                 ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
196 
197         when(mContext.checkPermission(eq(Manifest.permission.ACCESS_COARSE_LOCATION),
198                 anyInt(), anyInt())).thenReturn(s.appHasCoarseManifest
199                 ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
200 
201         when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_FINE_LOCATION),
202                 anyInt(), anyString(), nullable(String.class), nullable(String.class)))
203                 .thenReturn(s.fineAppOp);
204         when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_COARSE_LOCATION),
205                 anyInt(), anyString(), nullable(String.class), nullable(String.class)))
206                 .thenReturn(s.coarseAppOp);
207 
208         // set this permission to denied by default, and only allow for the proper pid/uid
209         // combination
210         when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
211                 anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_DENIED);
212         if (s.isDynamicLocationEnabled) {
213             when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class))).thenReturn(true);
214             when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
215                     eq(TESTING_PID), eq(TESTING_UID)))
216                     .thenReturn(PackageManager.PERMISSION_GRANTED);
217         } else {
218             when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class)))
219                     .thenReturn(false);
220         }
221 
222         ApplicationInfo fakeAppInfo = new ApplicationInfo();
223         fakeAppInfo.targetSdkVersion = s.appSdkLevel;
224 
225         try {
226             when(mPackageManager.getApplicationInfo(anyString(), anyInt()))
227                     .thenReturn(fakeAppInfo);
228         } catch (Exception e) {
229             // this is a formality
230         }
231     }
232 
getDefaultQueryBuilder()233     private static LocationAccessPolicy.LocationPermissionQuery.Builder getDefaultQueryBuilder() {
234         return new LocationAccessPolicy.LocationPermissionQuery.Builder()
235                 .setMethod("test")
236                 .setCallingPackage("com.android.test")
237                 .setCallingFeatureId(null)
238                 .setCallingPid(TESTING_PID)
239                 .setCallingUid(TESTING_UID);
240     }
241 
242     @Parameterized.Parameters(name = "{0}")
getScenarios()243     public static Collection<Scenario> getScenarios() {
244         List<Scenario> scenarios = new ArrayList<>();
245         scenarios.add(new Scenario.Builder()
246                 .setName("System location is off")
247                 .setAppHasFineManifest(true)
248                 .setFineAppOp(AppOpsManager.MODE_ALLOWED)
249                 .setAppSdkLevel(Build.VERSION_CODES.P)
250                 .setIsDynamicLocationEnabled(false)
251                 .setQuery(getDefaultQueryBuilder()
252                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N)
253                         .setMinSdkVersionForFine(Build.VERSION_CODES.N)
254                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
255                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_SOFT)
256                 .build());
257 
258         scenarios.add(new Scenario.Builder()
259                 .setName("System location is off but package is allowlisted for location")
260                 .setAppHasFineManifest(true)
261                 .setFineAppOp(AppOpsManager.MODE_ALLOWED)
262                 .setAppSdkLevel(Build.VERSION_CODES.P)
263                 .setIsDynamicLocationEnabled(false)
264                 .setQuery(getDefaultQueryBuilder()
265                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N)
266                         .setMinSdkVersionForFine(Build.VERSION_CODES.N)
267                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N)
268                         .setCallingPackage(TESTING_CALLING_PACKAGE).build())
269                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
270                 .build());
271 
272         scenarios.add(new Scenario.Builder()
273                 .setName("App on latest SDK level has all proper permissions for fine")
274                 .setAppHasFineManifest(true)
275                 .setFineAppOp(AppOpsManager.MODE_ALLOWED)
276                 .setAppSdkLevel(Build.VERSION_CODES.P)
277                 .setIsDynamicLocationEnabled(true)
278                 .setQuery(getDefaultQueryBuilder()
279                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N)
280                         .setMinSdkVersionForFine(Build.VERSION_CODES.N)
281                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
282                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
283                 .build());
284 
285         scenarios.add(new Scenario.Builder()
286                 .setName("App on older SDK level missing permissions for fine but has coarse")
287                 .setAppHasCoarseManifest(true)
288                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
289                 .setAppSdkLevel(Build.VERSION_CODES.JELLY_BEAN)
290                 .setIsDynamicLocationEnabled(true)
291                 .setQuery(getDefaultQueryBuilder()
292                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.JELLY_BEAN)
293                         .setMinSdkVersionForFine(Build.VERSION_CODES.M)
294                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.JELLY_BEAN).build())
295                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
296                 .build());
297 
298         scenarios.add(new Scenario.Builder()
299                 .setName("App on latest SDK level missing fine app ops permission")
300                 .setAppHasFineManifest(true)
301                 .setFineAppOp(AppOpsManager.MODE_ERRORED)
302                 .setAppSdkLevel(Build.VERSION_CODES.P)
303                 .setIsDynamicLocationEnabled(true)
304                 .setQuery(getDefaultQueryBuilder()
305                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N)
306                         .setMinSdkVersionForFine(Build.VERSION_CODES.N)
307                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
308                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
309                 .build());
310 
311         scenarios.add(new Scenario.Builder()
312                 .setName("App has coarse permission but fine permission isn't being enforced yet")
313                 .setAppHasCoarseManifest(true)
314                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
315                 .setAppSdkLevel(LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
316                 .setIsDynamicLocationEnabled(true)
317                 .setQuery(getDefaultQueryBuilder()
318                         .setMinSdkVersionForFine(
319                                 LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
320                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N)
321                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
322                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
323                 .build());
324 
325         scenarios.add(new Scenario.Builder()
326                 .setName("App on latest SDK level has coarse but missing fine when fine is req.")
327                 .setAppHasCoarseManifest(true)
328                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
329                 .setAppSdkLevel(Build.VERSION_CODES.P)
330                 .setIsDynamicLocationEnabled(true)
331                 .setQuery(getDefaultQueryBuilder()
332                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.N)
333                         .setMinSdkVersionForFine(Build.VERSION_CODES.P)
334                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
335                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
336                 .build());
337 
338         scenarios.add(new Scenario.Builder()
339                 .setName("App on latest SDK level has MODE_IGNORED for app ops on fine")
340                 .setAppHasCoarseManifest(true)
341                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
342                 .setFineAppOp(AppOpsManager.MODE_IGNORED)
343                 .setAppSdkLevel(Build.VERSION_CODES.P)
344                 .setIsDynamicLocationEnabled(true)
345                 .setQuery(getDefaultQueryBuilder()
346                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.O)
347                         .setMinSdkVersionForFine(Build.VERSION_CODES.P)
348                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build())
349                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
350                 .build());
351 
352         scenarios.add(new Scenario.Builder()
353                 .setName("App has no permissions but it's sdk level grandfathers it in")
354                 .setAppSdkLevel(Build.VERSION_CODES.N)
355                 .setIsDynamicLocationEnabled(true)
356                 .setQuery(getDefaultQueryBuilder()
357                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.O)
358                         .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
359                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build())
360                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
361                 .build());
362 
363         scenarios.add(new Scenario.Builder()
364                 .setName("App on latest SDK level has proper permissions for coarse")
365                 .setAppHasCoarseManifest(true)
366                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
367                 .setAppSdkLevel(Build.VERSION_CODES.P)
368                 .setIsDynamicLocationEnabled(true)
369                 .setQuery(getDefaultQueryBuilder()
370                         .setMinSdkVersionForEnforcement(Build.VERSION_CODES.P)
371                         .setMinSdkVersionForFine(
372                                 LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
373                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.P).build())
374                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
375                 .build());
376         return scenarios;
377     }
378 }
379