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