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.location.LocationManager;
33 import android.os.Build;
34 import android.os.UserHandle;
35 import android.telephony.LocationAccessPolicy;
36 
37 import org.junit.Before;
38 import org.junit.Test;
39 import org.junit.runner.RunWith;
40 import org.junit.runners.Parameterized;
41 import org.mockito.Mock;
42 import org.mockito.MockitoAnnotations;
43 
44 import java.util.ArrayList;
45 import java.util.Collection;
46 import java.util.List;
47 
48 @RunWith(Parameterized.class)
49 public class LocationAccessPolicyTest {
50     private static class Scenario {
51         static class Builder {
52             private int mAppSdkLevel;
53             private boolean mAppHasFineManifest = false;
54             private boolean mAppHasCoarseManifest = false;
55             private int mFineAppOp = AppOpsManager.MODE_IGNORED;
56             private int mCoarseAppOp = AppOpsManager.MODE_IGNORED;
57             private boolean mIsDynamicLocationEnabled;
58             private LocationAccessPolicy.LocationPermissionQuery mQuery;
59             private LocationAccessPolicy.LocationPermissionResult mExpectedResult;
60             private String mName;
61 
setAppSdkLevel(int appSdkLevel)62             public Builder setAppSdkLevel(int appSdkLevel) {
63                 mAppSdkLevel = appSdkLevel;
64                 return this;
65             }
66 
setAppHasFineManifest(boolean appHasFineManifest)67             public Builder setAppHasFineManifest(boolean appHasFineManifest) {
68                 mAppHasFineManifest = appHasFineManifest;
69                 return this;
70             }
71 
setAppHasCoarseManifest( boolean appHasCoarseManifest)72             public Builder setAppHasCoarseManifest(
73                     boolean appHasCoarseManifest) {
74                 mAppHasCoarseManifest = appHasCoarseManifest;
75                 return this;
76             }
77 
setFineAppOp(int fineAppOp)78             public Builder setFineAppOp(int fineAppOp) {
79                 mFineAppOp = fineAppOp;
80                 return this;
81             }
82 
setCoarseAppOp(int coarseAppOp)83             public Builder setCoarseAppOp(int coarseAppOp) {
84                 mCoarseAppOp = coarseAppOp;
85                 return this;
86             }
87 
setIsDynamicLocationEnabled( boolean isDynamicLocationEnabled)88             public Builder setIsDynamicLocationEnabled(
89                     boolean isDynamicLocationEnabled) {
90                 mIsDynamicLocationEnabled = isDynamicLocationEnabled;
91                 return this;
92             }
93 
setQuery( LocationAccessPolicy.LocationPermissionQuery query)94             public Builder setQuery(
95                     LocationAccessPolicy.LocationPermissionQuery query) {
96                 mQuery = query;
97                 return this;
98             }
99 
setExpectedResult( LocationAccessPolicy.LocationPermissionResult expectedResult)100             public Builder setExpectedResult(
101                     LocationAccessPolicy.LocationPermissionResult expectedResult) {
102                 mExpectedResult = expectedResult;
103                 return this;
104             }
105 
setName(String name)106             public Builder setName(String name) {
107                 mName = name;
108                 return this;
109             }
110 
build()111             public Scenario build() {
112                 return new Scenario(mAppSdkLevel, mAppHasFineManifest, mAppHasCoarseManifest,
113                         mFineAppOp, mCoarseAppOp, mIsDynamicLocationEnabled, mQuery,
114                         mExpectedResult, mName);
115             }
116         }
117         int appSdkLevel;
118         boolean appHasFineManifest;
119         boolean appHasCoarseManifest;
120         int fineAppOp;
121         int coarseAppOp;
122         boolean isDynamicLocationEnabled;
123         LocationAccessPolicy.LocationPermissionQuery query;
124         LocationAccessPolicy.LocationPermissionResult expectedResult;
125         String name;
126 
Scenario(int appSdkLevel, boolean appHasFineManifest, boolean appHasCoarseManifest, int fineAppOp, int coarseAppOp, boolean isDynamicLocationEnabled, LocationAccessPolicy.LocationPermissionQuery query, LocationAccessPolicy.LocationPermissionResult expectedResult, String name)127         private Scenario(int appSdkLevel, boolean appHasFineManifest, boolean appHasCoarseManifest,
128                 int fineAppOp, int coarseAppOp,
129                 boolean isDynamicLocationEnabled,
130                 LocationAccessPolicy.LocationPermissionQuery query,
131                 LocationAccessPolicy.LocationPermissionResult expectedResult,
132                 String name) {
133             this.appSdkLevel = appSdkLevel;
134             this.appHasFineManifest = appHasFineManifest;
135             this.appHasCoarseManifest = appHasFineManifest || appHasCoarseManifest;
136             this.fineAppOp = fineAppOp;
137             this.coarseAppOp = coarseAppOp == AppOpsManager.MODE_ALLOWED ? coarseAppOp : fineAppOp;
138             this.isDynamicLocationEnabled = isDynamicLocationEnabled;
139             this.query = query;
140             this.expectedResult = expectedResult;
141             this.name = name;
142         }
143 
144         @Override
toString()145         public String toString() {
146             return name;
147         }
148     }
149 
150     private static final int TESTING_UID = 10001;
151     private static final int TESTING_PID = 8009;
152 
153     @Mock Context mContext;
154     @Mock AppOpsManager mAppOpsManager;
155     @Mock LocationManager mLocationManager;
156     @Mock PackageManager mPackageManager;
157     Scenario mScenario;
158 
159     @Before
setUp()160     public void setUp() {
161         MockitoAnnotations.initMocks(this);
162         mockContextSystemService(AppOpsManager.class, mAppOpsManager);
163         mockContextSystemService(LocationManager.class, mLocationManager);
164         mockContextSystemService(PackageManager.class, mPackageManager);
165         when(mContext.getPackageManager()).thenReturn(mPackageManager);
166     }
167 
mockContextSystemService(Class<T> clazz , T obj)168     private <T> void mockContextSystemService(Class<T> clazz , T obj) {
169         when(mContext.getSystemServiceName(eq(clazz))).thenReturn(clazz.getSimpleName());
170         when(mContext.getSystemService(clazz.getSimpleName())).thenReturn(obj);
171     }
172 
LocationAccessPolicyTest(Scenario scenario)173     public LocationAccessPolicyTest(Scenario scenario) {
174         mScenario = scenario;
175     }
176 
177 
178     @Test
test()179     public void test() {
180         setupScenario(mScenario);
181         assertEquals(mScenario.expectedResult,
182                 LocationAccessPolicy.checkLocationPermission(mContext, mScenario.query));
183     }
184 
setupScenario(Scenario s)185     private void setupScenario(Scenario s) {
186         when(mContext.checkPermission(eq(Manifest.permission.ACCESS_FINE_LOCATION),
187                 anyInt(), anyInt())).thenReturn(s.appHasFineManifest
188                 ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
189 
190         when(mContext.checkPermission(eq(Manifest.permission.ACCESS_COARSE_LOCATION),
191                 anyInt(), anyInt())).thenReturn(s.appHasCoarseManifest
192                 ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
193 
194         when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_FINE_LOCATION),
195                 anyInt(), anyString(), nullable(String.class), nullable(String.class)))
196                 .thenReturn(s.fineAppOp);
197         when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_COARSE_LOCATION),
198                 anyInt(), anyString(), nullable(String.class), nullable(String.class)))
199                 .thenReturn(s.coarseAppOp);
200 
201         // set this permission to denied by default, and only allow for the proper pid/uid
202         // combination
203         when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
204                 anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_DENIED);
205         if (s.isDynamicLocationEnabled) {
206             when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class))).thenReturn(true);
207             when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
208                     eq(TESTING_PID), eq(TESTING_UID)))
209                     .thenReturn(PackageManager.PERMISSION_GRANTED);
210         } else {
211             when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class)))
212                     .thenReturn(false);
213         }
214 
215         ApplicationInfo fakeAppInfo = new ApplicationInfo();
216         fakeAppInfo.targetSdkVersion = s.appSdkLevel;
217 
218         try {
219             when(mPackageManager.getApplicationInfo(anyString(), anyInt()))
220                     .thenReturn(fakeAppInfo);
221         } catch (Exception e) {
222             // this is a formality
223         }
224     }
225 
getDefaultQueryBuilder()226     private static LocationAccessPolicy.LocationPermissionQuery.Builder getDefaultQueryBuilder() {
227         return new LocationAccessPolicy.LocationPermissionQuery.Builder()
228                 .setMethod("test")
229                 .setCallingPackage("com.android.test")
230                 .setCallingFeatureId(null)
231                 .setCallingPid(TESTING_PID)
232                 .setCallingUid(TESTING_UID);
233     }
234 
235     @Parameterized.Parameters(name = "{0}")
getScenarios()236     public static Collection<Scenario> getScenarios() {
237         List<Scenario> scenarios = new ArrayList<>();
238         scenarios.add(new Scenario.Builder()
239                 .setName("System location is off")
240                 .setAppHasFineManifest(true)
241                 .setFineAppOp(AppOpsManager.MODE_ALLOWED)
242                 .setAppSdkLevel(Build.VERSION_CODES.P)
243                 .setIsDynamicLocationEnabled(false)
244                 .setQuery(getDefaultQueryBuilder()
245                         .setMinSdkVersionForFine(Build.VERSION_CODES.N)
246                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
247                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_SOFT)
248                 .build());
249 
250         scenarios.add(new Scenario.Builder()
251                 .setName("App on latest SDK level has all proper permissions for fine")
252                 .setAppHasFineManifest(true)
253                 .setFineAppOp(AppOpsManager.MODE_ALLOWED)
254                 .setAppSdkLevel(Build.VERSION_CODES.P)
255                 .setIsDynamicLocationEnabled(true)
256                 .setQuery(getDefaultQueryBuilder()
257                         .setMinSdkVersionForFine(Build.VERSION_CODES.N)
258                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
259                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
260                 .build());
261 
262         scenarios.add(new Scenario.Builder()
263                 .setName("App on older SDK level missing permissions for fine but has coarse")
264                 .setAppHasCoarseManifest(true)
265                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
266                 .setAppSdkLevel(Build.VERSION_CODES.JELLY_BEAN)
267                 .setIsDynamicLocationEnabled(true)
268                 .setQuery(getDefaultQueryBuilder()
269                         .setMinSdkVersionForFine(Build.VERSION_CODES.M)
270                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.JELLY_BEAN).build())
271                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
272                 .build());
273 
274         scenarios.add(new Scenario.Builder()
275                 .setName("App on latest SDK level missing fine app ops permission")
276                 .setAppHasFineManifest(true)
277                 .setFineAppOp(AppOpsManager.MODE_ERRORED)
278                 .setAppSdkLevel(Build.VERSION_CODES.P)
279                 .setIsDynamicLocationEnabled(true)
280                 .setQuery(getDefaultQueryBuilder()
281                         .setMinSdkVersionForFine(Build.VERSION_CODES.N)
282                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
283                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
284                 .build());
285 
286         scenarios.add(new Scenario.Builder()
287                 .setName("App has coarse permission but fine permission isn't being enforced yet")
288                 .setAppHasCoarseManifest(true)
289                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
290                 .setAppSdkLevel(LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
291                 .setIsDynamicLocationEnabled(true)
292                 .setQuery(getDefaultQueryBuilder()
293                         .setMinSdkVersionForFine(
294                                 LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
295                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
296                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
297                 .build());
298 
299         scenarios.add(new Scenario.Builder()
300                 .setName("App on latest SDK level has coarse but missing fine when fine is req.")
301                 .setAppHasCoarseManifest(true)
302                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
303                 .setAppSdkLevel(Build.VERSION_CODES.P)
304                 .setIsDynamicLocationEnabled(true)
305                 .setQuery(getDefaultQueryBuilder()
306                         .setMinSdkVersionForFine(Build.VERSION_CODES.P)
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 on latest SDK level has MODE_IGNORED for app ops on fine")
313                 .setAppHasCoarseManifest(true)
314                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
315                 .setFineAppOp(AppOpsManager.MODE_IGNORED)
316                 .setAppSdkLevel(Build.VERSION_CODES.P)
317                 .setIsDynamicLocationEnabled(true)
318                 .setQuery(getDefaultQueryBuilder()
319                         .setMinSdkVersionForFine(Build.VERSION_CODES.P)
320                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build())
321                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
322                 .build());
323 
324         scenarios.add(new Scenario.Builder()
325                 .setName("App has no permissions but it's sdk level grandfathers it in")
326                 .setAppSdkLevel(Build.VERSION_CODES.N)
327                 .setIsDynamicLocationEnabled(true)
328                 .setQuery(getDefaultQueryBuilder()
329                         .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
330                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build())
331                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
332                 .build());
333 
334         scenarios.add(new Scenario.Builder()
335                 .setName("App on latest SDK level has proper permissions for coarse")
336                 .setAppHasCoarseManifest(true)
337                 .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
338                 .setAppSdkLevel(Build.VERSION_CODES.P)
339                 .setIsDynamicLocationEnabled(true)
340                 .setQuery(getDefaultQueryBuilder()
341                         .setMinSdkVersionForCoarse(Build.VERSION_CODES.P).build())
342                 .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
343                 .build());
344         return scenarios;
345     }
346 }
347