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 android.permission.cts; 18 19 import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION; 20 import static android.Manifest.permission.ACCESS_COARSE_LOCATION; 21 import static android.Manifest.permission.ACCESS_FINE_LOCATION; 22 import static android.Manifest.permission.BODY_SENSORS; 23 import static android.Manifest.permission.READ_CALENDAR; 24 import static android.Manifest.permission.READ_CONTACTS; 25 import static android.Manifest.permission.WRITE_CALENDAR; 26 import static android.app.AppOpsManager.MODE_ALLOWED; 27 import static android.app.AppOpsManager.MODE_FOREGROUND; 28 import static android.app.AppOpsManager.permissionToOp; 29 import static android.content.pm.PackageManager.PERMISSION_DENIED; 30 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 31 import static android.permission.PermissionControllerManager.COUNT_ONLY_WHEN_GRANTED; 32 import static android.permission.PermissionControllerManager.REASON_INSTALLER_POLICY_VIOLATION; 33 import static android.permission.PermissionControllerManager.REASON_MALWARE; 34 import static android.permission.cts.PermissionUtils.grantPermission; 35 import static android.permission.cts.PermissionUtils.isGranted; 36 import static android.permission.cts.PermissionUtils.isPermissionGranted; 37 38 import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity; 39 import static com.android.compatibility.common.util.SystemUtil.eventually; 40 import static com.android.compatibility.common.util.SystemUtil.runShellCommand; 41 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; 42 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 43 44 import static com.google.common.truth.Truth.assertThat; 45 46 import static java.util.Collections.singletonList; 47 48 import android.app.AppOpsManager; 49 import android.app.UiAutomation; 50 import android.content.Context; 51 import android.content.pm.PermissionGroupInfo; 52 import android.permission.PermissionControllerManager; 53 import android.permission.RuntimePermissionPresentationInfo; 54 import android.platform.test.annotations.AppModeFull; 55 56 import androidx.annotation.NonNull; 57 import androidx.test.InstrumentationRegistry; 58 import androidx.test.runner.AndroidJUnit4; 59 60 import org.junit.After; 61 import org.junit.AfterClass; 62 import org.junit.Before; 63 import org.junit.BeforeClass; 64 import org.junit.Test; 65 import org.junit.runner.RunWith; 66 67 import java.util.ArrayList; 68 import java.util.Collections; 69 import java.util.List; 70 import java.util.Map; 71 import java.util.concurrent.CompletableFuture; 72 import java.util.concurrent.Executor; 73 import java.util.concurrent.atomic.AtomicBoolean; 74 import java.util.concurrent.atomic.AtomicReference; 75 76 /** 77 * Test {@link PermissionControllerManager} 78 */ 79 @RunWith(AndroidJUnit4.class) 80 @AppModeFull(reason = "Instant apps cannot talk to permission controller") 81 public class PermissionControllerTest { 82 private static final String APK = 83 "/data/local/tmp/cts-permission/CtsAppThatAccessesLocationOnCommand.apk"; 84 private static final String APP = "android.permission.cts.appthataccesseslocation"; 85 private static final String APK2 = 86 "/data/local/tmp/cts-permission/" 87 + "CtsAppThatRequestsCalendarContactsBodySensorCustomPermission.apk"; 88 private static final String APP2 = "android.permission.cts.appthatrequestcustompermission"; 89 private static final String CUSTOM_PERMISSION = 90 "android.permission.cts.appthatrequestcustompermission.TEST_PERMISSION"; 91 92 private static final UiAutomation sUiAutomation = 93 InstrumentationRegistry.getInstrumentation().getUiAutomation(); 94 private static final Context sContext = InstrumentationRegistry.getTargetContext(); 95 private static final PermissionControllerManager sController = 96 sContext.getSystemService(PermissionControllerManager.class); 97 98 @Before 99 @After resetAppState()100 public void resetAppState() { 101 runWithShellPermissionIdentity(() -> { 102 sUiAutomation.grantRuntimePermission(APP, ACCESS_FINE_LOCATION); 103 sUiAutomation.grantRuntimePermission(APP, ACCESS_BACKGROUND_LOCATION); 104 setAppOp(APP, ACCESS_FINE_LOCATION, MODE_ALLOWED); 105 }); 106 } 107 108 @BeforeClass installApp()109 public static void installApp() { 110 runShellCommandOrThrow("pm install -r -g " + APK); 111 runShellCommandOrThrow("pm install -r " + APK2); 112 } 113 114 @AfterClass uninstallApp()115 public static void uninstallApp() { 116 runShellCommand("pm uninstall " + APP); 117 runShellCommand("pm uninstall " + APP2); 118 } 119 revokePermissions( @onNull Map<String, List<String>> request, boolean doDryRun, int reason, @NonNull Executor executor)120 private @NonNull Map<String, List<String>> revokePermissions( 121 @NonNull Map<String, List<String>> request, boolean doDryRun, int reason, 122 @NonNull Executor executor) throws Exception { 123 AtomicReference<Map<String, List<String>>> result = new AtomicReference<>(); 124 125 sController.revokeRuntimePermissions(request, doDryRun, reason, executor, 126 new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() { 127 @Override 128 public void onRevokeRuntimePermissions(@NonNull Map<String, List<String>> r) { 129 synchronized (result) { 130 result.set(r); 131 result.notifyAll(); 132 } 133 } 134 }); 135 136 synchronized (result) { 137 while (result.get() == null) { 138 result.wait(); 139 } 140 } 141 142 return result.get(); 143 } 144 revokePermissions( @onNull Map<String, List<String>> request, boolean doDryRun, boolean adoptShell)145 private @NonNull Map<String, List<String>> revokePermissions( 146 @NonNull Map<String, List<String>> request, boolean doDryRun, boolean adoptShell) 147 throws Exception { 148 if (adoptShell) { 149 Map<String, List<String>> revokeRet = 150 callWithShellPermissionIdentity(() -> revokePermissions( 151 request, doDryRun, REASON_MALWARE, sContext.getMainExecutor())); 152 return revokeRet; 153 } 154 return revokePermissions(request, doDryRun, REASON_MALWARE, sContext.getMainExecutor()); 155 } 156 revokePermissions( @onNull Map<String, List<String>> request, boolean doDryRun)157 private @NonNull Map<String, List<String>> revokePermissions( 158 @NonNull Map<String, List<String>> request, boolean doDryRun) throws Exception { 159 return revokePermissions(request, doDryRun, true); 160 } 161 setAppOp(@onNull String pkg, @NonNull String perm, int mode)162 private void setAppOp(@NonNull String pkg, @NonNull String perm, int mode) throws Exception { 163 sContext.getSystemService(AppOpsManager.class).setUidMode(permissionToOp(perm), 164 sContext.getPackageManager().getPackageUid(pkg, 0), mode); 165 } 166 buildRevokeRequest(@onNull String app, @NonNull String permission)167 private Map<String, List<String>> buildRevokeRequest(@NonNull String app, 168 @NonNull String permission) { 169 return Collections.singletonMap(app, singletonList(permission)); 170 } 171 assertRuntimePermissionLabelsAreValid(List<String> runtimePermissions, List<RuntimePermissionPresentationInfo> permissionInfos, int expectedRuntimeGranted, String app)172 private void assertRuntimePermissionLabelsAreValid(List<String> runtimePermissions, 173 List<RuntimePermissionPresentationInfo> permissionInfos, int expectedRuntimeGranted, 174 String app) throws Exception { 175 int numRuntimeGranted = 0; 176 for (String permission : runtimePermissions) { 177 if (isPermissionGranted(app, permission)) { 178 numRuntimeGranted++; 179 } 180 } 181 assertThat(numRuntimeGranted).isEqualTo(expectedRuntimeGranted); 182 183 ArrayList<CharSequence> maybeStandardPermissionLabels = new ArrayList<>(); 184 ArrayList<CharSequence> nonStandardPermissionLabels = new ArrayList<>(); 185 for (PermissionGroupInfo permGroup : sContext.getPackageManager().getAllPermissionGroups( 186 0)) { 187 CharSequence permissionGroupLabel = permGroup.loadLabel(sContext.getPackageManager()); 188 if (permGroup.packageName.equals("android")) { 189 maybeStandardPermissionLabels.add(permissionGroupLabel); 190 } else { 191 nonStandardPermissionLabels.add(permissionGroupLabel); 192 } 193 } 194 195 int numInfosGranted = 0; 196 197 for (RuntimePermissionPresentationInfo permissionInfo : permissionInfos) { 198 CharSequence permissionGroupLabel = permissionInfo.getLabel(); 199 200 // PermissionInfo should be included in exactly one of existing (possibly) standard 201 // or nonstandard permission groups 202 if (permissionInfo.isStandard()) { 203 assertThat(maybeStandardPermissionLabels).contains(permissionGroupLabel); 204 } else { 205 assertThat(nonStandardPermissionLabels).contains(permissionGroupLabel); 206 } 207 if (permissionInfo.isGranted()) { 208 numInfosGranted++; 209 } 210 } 211 212 // Each permissionInfo represents one or more runtime permissions, but we don't have a 213 // mapping, so we check that we have at least as many runtimePermissions as permissionInfos 214 assertThat(numRuntimeGranted).isAtLeast(numInfosGranted); 215 } 216 217 @Test revokePermissionsDryRunSinglePermission()218 public void revokePermissionsDryRunSinglePermission() throws Exception { 219 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION); 220 221 Map<String, List<String>> result = revokePermissions(request, true); 222 223 assertThat(result.size()).isEqualTo(1); 224 assertThat(result.get(APP)).isNotNull(); 225 assertThat(result.get(APP)).containsExactly(ACCESS_BACKGROUND_LOCATION); 226 } 227 228 @Test revokePermissionsSinglePermission()229 public void revokePermissionsSinglePermission() throws Exception { 230 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION); 231 232 revokePermissions(request, false); 233 234 assertThat(sContext.getPackageManager().checkPermission(ACCESS_BACKGROUND_LOCATION, 235 APP)).isEqualTo(PERMISSION_DENIED); 236 } 237 238 @Test revokePermissionsDoNotAlreadyRevokedPermission()239 public void revokePermissionsDoNotAlreadyRevokedPermission() throws Exception { 240 // Properly revoke the permission 241 runWithShellPermissionIdentity(() -> { 242 sUiAutomation.revokeRuntimePermission(APP, ACCESS_BACKGROUND_LOCATION); 243 setAppOp(APP, ACCESS_FINE_LOCATION, MODE_FOREGROUND); 244 }); 245 246 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION); 247 Map<String, List<String>> result = revokePermissions(request, false); 248 249 assertThat(result).isEmpty(); 250 } 251 252 @Test revokePermissionsDryRunForegroundPermission()253 public void revokePermissionsDryRunForegroundPermission() throws Exception { 254 assertThat(sContext.getPackageManager().checkPermission(ACCESS_FINE_LOCATION, 255 APP)).isEqualTo(PERMISSION_GRANTED); 256 257 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_FINE_LOCATION); 258 Map<String, List<String>> result = revokePermissions(request, true); 259 260 assertThat(result.size()).isEqualTo(1); 261 assertThat(result.get(APP)).isNotNull(); 262 assertThat(result.get(APP)).containsExactly(ACCESS_FINE_LOCATION, 263 ACCESS_BACKGROUND_LOCATION, ACCESS_COARSE_LOCATION); 264 } 265 266 @Test revokePermissionsUnrequestedPermission()267 public void revokePermissionsUnrequestedPermission() throws Exception { 268 Map<String, List<String>> request = buildRevokeRequest(APP, READ_CONTACTS); 269 270 Map<String, List<String>> result = revokePermissions(request, false); 271 272 assertThat(result).isEmpty(); 273 } 274 275 @Test revokeFromUnknownPackage()276 public void revokeFromUnknownPackage() throws Exception { 277 Map<String, List<String>> request = buildRevokeRequest("invalid.app", READ_CONTACTS); 278 279 Map<String, List<String>> result = revokePermissions(request, false); 280 281 assertThat(result).isEmpty(); 282 } 283 284 @Test revokePermissionsFromUnknownPermission()285 public void revokePermissionsFromUnknownPermission() throws Exception { 286 Map<String, List<String>> request = buildRevokeRequest(APP, "unknown.permission"); 287 288 Map<String, List<String>> result = revokePermissions(request, false); 289 290 assertThat(result).isEmpty(); 291 } 292 293 @Test revokePermissionsPolicyViolationFromWrongPackage()294 public void revokePermissionsPolicyViolationFromWrongPackage() throws Exception { 295 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_FINE_LOCATION); 296 Map<String, List<String>> result = callWithShellPermissionIdentity( 297 () -> revokePermissions(request, 298 false, REASON_INSTALLER_POLICY_VIOLATION, sContext.getMainExecutor())); 299 assertThat(result).isEmpty(); 300 } 301 302 @Test revokePermissionsWithExecutorForCallback()303 public void revokePermissionsWithExecutorForCallback() throws Exception { 304 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION); 305 306 AtomicBoolean wasRunOnExecutor = new AtomicBoolean(); 307 runWithShellPermissionIdentity(() -> 308 revokePermissions(request, true, REASON_MALWARE, command -> { 309 wasRunOnExecutor.set(true); 310 command.run(); 311 })); 312 313 assertThat(wasRunOnExecutor.get()).isTrue(); 314 } 315 316 @Test(expected = NullPointerException.class) revokePermissionsWithNullPkg()317 public void revokePermissionsWithNullPkg() throws Exception { 318 Map<String, List<String>> request = Collections.singletonMap(null, 319 singletonList(ACCESS_FINE_LOCATION)); 320 321 revokePermissions(request, true); 322 } 323 324 @Test(expected = NullPointerException.class) revokePermissionsWithNullPermissions()325 public void revokePermissionsWithNullPermissions() throws Exception { 326 Map<String, List<String>> request = Collections.singletonMap(APP, null); 327 328 revokePermissions(request, true); 329 } 330 331 @Test(expected = NullPointerException.class) revokePermissionsWithNullPermission()332 public void revokePermissionsWithNullPermission() throws Exception { 333 Map<String, List<String>> request = Collections.singletonMap(APP, 334 singletonList(null)); 335 336 revokePermissions(request, true); 337 } 338 339 @Test(expected = NullPointerException.class) revokePermissionsWithNullRequests()340 public void revokePermissionsWithNullRequests() { 341 sController.revokeRuntimePermissions(null, false, REASON_MALWARE, 342 sContext.getMainExecutor(), 343 new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() { 344 @Override 345 public void onRevokeRuntimePermissions( 346 @NonNull Map<String, List<String>> revoked) { 347 } 348 }); 349 } 350 351 @Test(expected = NullPointerException.class) revokePermissionsWithNullCallback()352 public void revokePermissionsWithNullCallback() { 353 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION); 354 355 sController.revokeRuntimePermissions(request, false, REASON_MALWARE, 356 sContext.getMainExecutor(), null); 357 } 358 359 @Test(expected = NullPointerException.class) revokePermissionsWithNullExecutor()360 public void revokePermissionsWithNullExecutor() { 361 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION); 362 363 sController.revokeRuntimePermissions(request, false, REASON_MALWARE, null, 364 new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() { 365 @Override 366 public void onRevokeRuntimePermissions( 367 @NonNull Map<String, List<String>> revoked) { 368 369 } 370 }); 371 } 372 373 @Test(expected = SecurityException.class) revokePermissionsWithoutPermission()374 public void revokePermissionsWithoutPermission() throws Exception { 375 Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION); 376 377 // This will fail as the test-app does not have the required permission 378 revokePermissions(request, true, false); 379 } 380 381 @Test getAppPermissionsForApp()382 public void getAppPermissionsForApp() throws Exception { 383 CompletableFuture<List<RuntimePermissionPresentationInfo>> futurePermissionInfos = 384 new CompletableFuture<>(); 385 386 List<String> runtimePermissions; 387 List<RuntimePermissionPresentationInfo> permissionInfos; 388 389 sUiAutomation.adoptShellPermissionIdentity(); 390 try { 391 sController.getAppPermissions(APP, futurePermissionInfos::complete, null); 392 runtimePermissions = PermissionUtils.getRuntimePermissions(APP); 393 assertThat(runtimePermissions).isNotEmpty(); 394 permissionInfos = futurePermissionInfos.get(); 395 } finally { 396 sUiAutomation.dropShellPermissionIdentity(); 397 } 398 399 assertRuntimePermissionLabelsAreValid(runtimePermissions, permissionInfos, 3, APP); 400 } 401 402 @Test getAppPermissionsForCustomApp()403 public void getAppPermissionsForCustomApp() throws Exception { 404 CompletableFuture<List<RuntimePermissionPresentationInfo>> futurePermissionInfos = 405 new CompletableFuture<>(); 406 407 // Grant all requested permissions except READ_CALENDAR 408 sUiAutomation.grantRuntimePermission(APP2, CUSTOM_PERMISSION); 409 PermissionUtils.grantPermission(APP2, BODY_SENSORS); 410 PermissionUtils.grantPermission(APP2, READ_CONTACTS); 411 PermissionUtils.grantPermission(APP2, WRITE_CALENDAR); 412 413 List<String> runtimePermissions; 414 List<RuntimePermissionPresentationInfo> permissionInfos; 415 sUiAutomation.adoptShellPermissionIdentity(); 416 try { 417 sController.getAppPermissions(APP2, futurePermissionInfos::complete, null); 418 runtimePermissions = PermissionUtils.getRuntimePermissions(APP2); 419 420 permissionInfos = futurePermissionInfos.get(); 421 } finally { 422 sUiAutomation.dropShellPermissionIdentity(); 423 } 424 425 assertThat(permissionInfos).isNotEmpty(); 426 assertThat(runtimePermissions.size()).isEqualTo(6); 427 assertRuntimePermissionLabelsAreValid(runtimePermissions, permissionInfos, 4, APP2); 428 } 429 430 @Test revokePermissionAutomaticallyExtendsToWholeGroup()431 public void revokePermissionAutomaticallyExtendsToWholeGroup() throws Exception { 432 grantPermission(APP2, READ_CALENDAR); 433 grantPermission(APP2, WRITE_CALENDAR); 434 435 runWithShellPermissionIdentity( 436 () -> { 437 sController.revokeRuntimePermission(APP2, READ_CALENDAR); 438 439 eventually(() -> { 440 assertThat(isGranted(APP2, READ_CALENDAR)).isEqualTo(false); 441 // revokePermission automatically extends the revocation to whole group 442 assertThat(isGranted(APP2, WRITE_CALENDAR)).isEqualTo(false); 443 }); 444 }); 445 } 446 447 @Test revokePermissionCustom()448 public void revokePermissionCustom() throws Exception { 449 sUiAutomation.grantRuntimePermission(APP2, CUSTOM_PERMISSION); 450 451 runWithShellPermissionIdentity( 452 () -> { 453 sController.revokeRuntimePermission(APP2, CUSTOM_PERMISSION); 454 455 eventually(() -> { 456 assertThat(isPermissionGranted(APP2, CUSTOM_PERMISSION)).isEqualTo(false); 457 }); 458 }); 459 } 460 461 @Test revokePermissionWithInvalidPkg()462 public void revokePermissionWithInvalidPkg() throws Exception { 463 // No return value, call is ignored 464 runWithShellPermissionIdentity( 465 () -> sController.revokeRuntimePermission("invalid.package", READ_CALENDAR)); 466 } 467 468 @Test revokePermissionWithInvalidPermission()469 public void revokePermissionWithInvalidPermission() throws Exception { 470 // No return value, call is ignored 471 runWithShellPermissionIdentity( 472 () -> sController.revokeRuntimePermission(APP2, "invalid.permission")); 473 } 474 475 @Test(expected = NullPointerException.class) revokePermissionWithNullPkg()476 public void revokePermissionWithNullPkg() throws Exception { 477 sController.revokeRuntimePermission(null, READ_CALENDAR); 478 } 479 480 @Test(expected = NullPointerException.class) revokePermissionWithNullPermission()481 public void revokePermissionWithNullPermission() throws Exception { 482 sController.revokeRuntimePermission(APP2, null); 483 } 484 485 // TODO: Add more tests for countPermissionAppsGranted when the method can be safely called 486 // multiple times in a row 487 488 @Test countPermissionAppsGranted()489 public void countPermissionAppsGranted() { 490 runWithShellPermissionIdentity( 491 () -> { 492 CompletableFuture<Integer> numApps = new CompletableFuture<>(); 493 494 sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION), 495 COUNT_ONLY_WHEN_GRANTED, numApps::complete, null); 496 497 // TODO: Better would be to count before, grant a permission, count again and 498 // then compare before and after 499 assertThat(numApps.get()).isAtLeast(1); 500 }); 501 } 502 503 @Test(expected = NullPointerException.class) countPermissionAppsNullPermission()504 public void countPermissionAppsNullPermission() { 505 sController.countPermissionApps(null, 0, (n) -> { }, null); 506 } 507 508 @Test(expected = IllegalArgumentException.class) countPermissionAppsInvalidFlags()509 public void countPermissionAppsInvalidFlags() { 510 sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION), -1, (n) -> { }, null); 511 } 512 513 @Test(expected = NullPointerException.class) countPermissionAppsNullCallback()514 public void countPermissionAppsNullCallback() { 515 sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION), 0, null, null); 516 } 517 } 518