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