blob: 7c23156ef67ea35775cd46a74cc8ca368d94f119 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.permission.cts;
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.BODY_SENSORS;
import static android.Manifest.permission.READ_CALENDAR;
import static android.Manifest.permission.READ_CONTACTS;
import static android.Manifest.permission.WRITE_CALENDAR;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_FOREGROUND;
import static android.app.AppOpsManager.permissionToOp;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.permission.PermissionControllerManager.COUNT_ONLY_WHEN_GRANTED;
import static android.permission.PermissionControllerManager.REASON_INSTALLER_POLICY_VIOLATION;
import static android.permission.PermissionControllerManager.REASON_MALWARE;
import static android.permission.cts.PermissionUtils.grantPermission;
import static android.permission.cts.PermissionUtils.isGranted;
import static android.permission.cts.PermissionUtils.isPermissionGranted;
import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
import static com.android.compatibility.common.util.SystemUtil.eventually;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Collections.singletonList;
import android.app.AppOpsManager;
import android.app.UiAutomation;
import android.content.Context;
import android.content.pm.PermissionGroupInfo;
import android.permission.PermissionControllerManager;
import android.permission.RuntimePermissionPresentationInfo;
import android.platform.test.annotations.AppModeFull;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* Test {@link PermissionControllerManager}
*/
@RunWith(AndroidJUnit4.class)
@AppModeFull(reason = "Instant apps cannot talk to permission controller")
public class PermissionControllerTest {
private static final String APK =
"/data/local/tmp/cts/permissions/CtsAppThatAccessesLocationOnCommand.apk";
private static final String APP = "android.permission.cts.appthataccesseslocation";
private static final String APK2 =
"/data/local/tmp/cts/permissions/"
+ "CtsAppThatRequestsCalendarContactsBodySensorCustomPermission.apk";
private static final String APP2 = "android.permission.cts.appthatrequestcustompermission";
private static final String CUSTOM_PERMISSION =
"android.permission.cts.appthatrequestcustompermission.TEST_PERMISSION";
private static final UiAutomation sUiAutomation =
InstrumentationRegistry.getInstrumentation().getUiAutomation();
private static final Context sContext = InstrumentationRegistry.getTargetContext();
private static final PermissionControllerManager sController =
sContext.getSystemService(PermissionControllerManager.class);
@Before
@After
public void resetAppState() {
runWithShellPermissionIdentity(() -> {
sUiAutomation.grantRuntimePermission(APP, ACCESS_FINE_LOCATION);
sUiAutomation.grantRuntimePermission(APP, ACCESS_BACKGROUND_LOCATION);
setAppOp(APP, ACCESS_FINE_LOCATION, MODE_ALLOWED);
});
}
@BeforeClass
public static void installApp() {
runShellCommand("pm install -r -g " + APK);
runShellCommand("pm install -r " + APK2);
}
@AfterClass
public static void uninstallApp() {
runShellCommand("pm uninstall " + APP);
runShellCommand("pm uninstall " + APP2);
}
private @NonNull Map<String, List<String>> revokePermissions(
@NonNull Map<String, List<String>> request, boolean doDryRun, int reason,
@NonNull Executor executor) throws Exception {
AtomicReference<Map<String, List<String>>> result = new AtomicReference<>();
sController.revokeRuntimePermissions(request, doDryRun, reason, executor,
new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() {
@Override
public void onRevokeRuntimePermissions(@NonNull Map<String, List<String>> r) {
synchronized (result) {
result.set(r);
result.notifyAll();
}
}
});
synchronized (result) {
while (result.get() == null) {
result.wait();
}
}
return result.get();
}
private @NonNull Map<String, List<String>> revokePermissions(
@NonNull Map<String, List<String>> request, boolean doDryRun, boolean adoptShell)
throws Exception {
if (adoptShell) {
Map<String, List<String>> revokeRet =
callWithShellPermissionIdentity(() -> revokePermissions(
request, doDryRun, REASON_MALWARE, sContext.getMainExecutor()));
return revokeRet;
}
return revokePermissions(request, doDryRun, REASON_MALWARE, sContext.getMainExecutor());
}
private @NonNull Map<String, List<String>> revokePermissions(
@NonNull Map<String, List<String>> request, boolean doDryRun) throws Exception {
return revokePermissions(request, doDryRun, true);
}
private void setAppOp(@NonNull String pkg, @NonNull String perm, int mode) throws Exception {
sContext.getSystemService(AppOpsManager.class).setUidMode(permissionToOp(perm),
sContext.getPackageManager().getPackageUid(pkg, 0), mode);
}
private Map<String, List<String>> buildRevokeRequest(@NonNull String app,
@NonNull String permission) {
return Collections.singletonMap(app, singletonList(permission));
}
private void assertRuntimePermissionLabelsAreValid(List<String> runtimePermissions,
List<RuntimePermissionPresentationInfo> permissionInfos, int expectedRuntimeGranted,
String app) throws Exception {
int numRuntimeGranted = 0;
for (String permission : runtimePermissions) {
if (isPermissionGranted(app, permission)) {
numRuntimeGranted++;
}
}
assertThat(numRuntimeGranted).isEqualTo(expectedRuntimeGranted);
ArrayList<CharSequence> maybeStandardPermissionLabels = new ArrayList<>();
ArrayList<CharSequence> nonStandardPermissionLabels = new ArrayList<>();
for (PermissionGroupInfo permGroup : sContext.getPackageManager().getAllPermissionGroups(
0)) {
CharSequence permissionGroupLabel = permGroup.loadLabel(sContext.getPackageManager());
if (permGroup.packageName.equals("android")) {
maybeStandardPermissionLabels.add(permissionGroupLabel);
} else {
nonStandardPermissionLabels.add(permissionGroupLabel);
}
}
int numInfosGranted = 0;
for (RuntimePermissionPresentationInfo permissionInfo : permissionInfos) {
CharSequence permissionGroupLabel = permissionInfo.getLabel();
// PermissionInfo should be included in exactly one of existing (possibly) standard
// or nonstandard permission groups
if (permissionInfo.isStandard()) {
assertThat(maybeStandardPermissionLabels).contains(permissionGroupLabel);
} else {
assertThat(nonStandardPermissionLabels).contains(permissionGroupLabel);
}
if (permissionInfo.isGranted()) {
numInfosGranted++;
}
}
// Each permissionInfo represents one or more runtime permissions, but we don't have a
// mapping, so we check that we have at least as many runtimePermissions as permissionInfos
assertThat(numRuntimeGranted).isAtLeast(numInfosGranted);
}
@Test
public void revokePermissionsDryRunSinglePermission() throws Exception {
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
Map<String, List<String>> result = revokePermissions(request, true);
assertThat(result.size()).isEqualTo(1);
assertThat(result.get(APP)).isNotNull();
assertThat(result.get(APP)).containsExactly(ACCESS_BACKGROUND_LOCATION);
}
@Test
public void revokePermissionsSinglePermission() throws Exception {
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
revokePermissions(request, false);
assertThat(sContext.getPackageManager().checkPermission(ACCESS_BACKGROUND_LOCATION,
APP)).isEqualTo(PERMISSION_DENIED);
}
@Test
public void revokePermissionsDoNotAlreadyRevokedPermission() throws Exception {
// Properly revoke the permission
runWithShellPermissionIdentity(() -> {
sUiAutomation.revokeRuntimePermission(APP, ACCESS_BACKGROUND_LOCATION);
setAppOp(APP, ACCESS_FINE_LOCATION, MODE_FOREGROUND);
});
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
Map<String, List<String>> result = revokePermissions(request, false);
assertThat(result).isEmpty();
}
@Test
public void revokePermissionsDryRunForegroundPermission() throws Exception {
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_FINE_LOCATION);
Map<String, List<String>> result = revokePermissions(request, true);
assertThat(result.size()).isEqualTo(1);
assertThat(result.get(APP)).isNotNull();
assertThat(result.get(APP)).containsExactly(ACCESS_FINE_LOCATION,
ACCESS_BACKGROUND_LOCATION, ACCESS_COARSE_LOCATION);
}
@Test
public void revokePermissionsUnrequestedPermission() throws Exception {
Map<String, List<String>> request = buildRevokeRequest(APP, READ_CONTACTS);
Map<String, List<String>> result = revokePermissions(request, false);
assertThat(result).isEmpty();
}
@Test
public void revokeFromUnknownPackage() throws Exception {
Map<String, List<String>> request = buildRevokeRequest("invalid.app", READ_CONTACTS);
Map<String, List<String>> result = revokePermissions(request, false);
assertThat(result).isEmpty();
}
@Test
public void revokePermissionsFromUnknownPermission() throws Exception {
Map<String, List<String>> request = buildRevokeRequest(APP, "unknown.permission");
Map<String, List<String>> result = revokePermissions(request, false);
assertThat(result).isEmpty();
}
@Test
public void revokePermissionsPolicyViolationFromWrongPackage() throws Exception {
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_FINE_LOCATION);
Map<String, List<String>> result = callWithShellPermissionIdentity(
() -> revokePermissions(request,
false, REASON_INSTALLER_POLICY_VIOLATION, sContext.getMainExecutor()));
assertThat(result).isEmpty();
}
@Test
public void revokePermissionsWithExecutorForCallback() throws Exception {
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
AtomicBoolean wasRunOnExecutor = new AtomicBoolean();
runWithShellPermissionIdentity(() ->
revokePermissions(request, true, REASON_MALWARE, command -> {
wasRunOnExecutor.set(true);
command.run();
}));
assertThat(wasRunOnExecutor.get()).isTrue();
}
@Test(expected = NullPointerException.class)
public void revokePermissionsWithNullPkg() throws Exception {
Map<String, List<String>> request = Collections.singletonMap(null,
singletonList(ACCESS_FINE_LOCATION));
revokePermissions(request, true);
}
@Test(expected = NullPointerException.class)
public void revokePermissionsWithNullPermissions() throws Exception {
Map<String, List<String>> request = Collections.singletonMap(APP, null);
revokePermissions(request, true);
}
@Test(expected = NullPointerException.class)
public void revokePermissionsWithNullPermission() throws Exception {
Map<String, List<String>> request = Collections.singletonMap(APP,
singletonList(null));
revokePermissions(request, true);
}
@Test(expected = NullPointerException.class)
public void revokePermissionsWithNullRequests() {
sController.revokeRuntimePermissions(null, false, REASON_MALWARE,
sContext.getMainExecutor(),
new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() {
@Override
public void onRevokeRuntimePermissions(
@NonNull Map<String, List<String>> revoked) {
}
});
}
@Test(expected = NullPointerException.class)
public void revokePermissionsWithNullCallback() {
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
sController.revokeRuntimePermissions(request, false, REASON_MALWARE,
sContext.getMainExecutor(), null);
}
@Test(expected = NullPointerException.class)
public void revokePermissionsWithNullExecutor() {
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
sController.revokeRuntimePermissions(request, false, REASON_MALWARE, null,
new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() {
@Override
public void onRevokeRuntimePermissions(
@NonNull Map<String, List<String>> revoked) {
}
});
}
@Test(expected = SecurityException.class)
public void revokePermissionsWithoutPermission() throws Exception {
Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
// This will fail as the test-app does not have the required permission
revokePermissions(request, true, false);
}
@Test
public void getAppPermissionsForApp() throws Exception {
CompletableFuture<List<RuntimePermissionPresentationInfo>> futurePermissionInfos =
new CompletableFuture<>();
List<String> runtimePermissions;
List<RuntimePermissionPresentationInfo> permissionInfos;
sUiAutomation.adoptShellPermissionIdentity();
try {
sController.getAppPermissions(APP, futurePermissionInfos::complete, null);
runtimePermissions = PermissionUtils.getRuntimePermissions(APP);
assertThat(runtimePermissions).isNotEmpty();
permissionInfos = futurePermissionInfos.get();
} finally {
sUiAutomation.dropShellPermissionIdentity();
}
assertRuntimePermissionLabelsAreValid(runtimePermissions, permissionInfos, 3, APP);
}
@Test
public void getAppPermissionsForCustomApp() throws Exception {
CompletableFuture<List<RuntimePermissionPresentationInfo>> futurePermissionInfos =
new CompletableFuture<>();
// Grant all requested permissions except READ_CALENDAR
sUiAutomation.grantRuntimePermission(APP2, CUSTOM_PERMISSION);
PermissionUtils.grantPermission(APP2, BODY_SENSORS);
PermissionUtils.grantPermission(APP2, READ_CONTACTS);
PermissionUtils.grantPermission(APP2, WRITE_CALENDAR);
List<String> runtimePermissions;
List<RuntimePermissionPresentationInfo> permissionInfos;
sUiAutomation.adoptShellPermissionIdentity();
try {
sController.getAppPermissions(APP2, futurePermissionInfos::complete, null);
runtimePermissions = PermissionUtils.getRuntimePermissions(APP2);
permissionInfos = futurePermissionInfos.get();
} finally {
sUiAutomation.dropShellPermissionIdentity();
}
assertThat(permissionInfos).isNotEmpty();
assertThat(runtimePermissions.size()).isEqualTo(5);
assertRuntimePermissionLabelsAreValid(runtimePermissions, permissionInfos, 4, APP2);
}
@Test
public void revokePermissionAutomaticallyExtendsToWholeGroup() throws Exception {
grantPermission(APP2, READ_CALENDAR);
grantPermission(APP2, WRITE_CALENDAR);
runWithShellPermissionIdentity(
() -> {
sController.revokeRuntimePermission(APP2, READ_CALENDAR);
eventually(() -> {
assertThat(isGranted(APP2, READ_CALENDAR)).isEqualTo(false);
// revokePermission automatically extends the revocation to whole group
assertThat(isGranted(APP2, WRITE_CALENDAR)).isEqualTo(false);
});
});
}
@Test
public void revokePermissionCustom() throws Exception {
sUiAutomation.grantRuntimePermission(APP2, CUSTOM_PERMISSION);
runWithShellPermissionIdentity(
() -> {
sController.revokeRuntimePermission(APP2, CUSTOM_PERMISSION);
eventually(() -> {
assertThat(isPermissionGranted(APP2, CUSTOM_PERMISSION)).isEqualTo(false);
});
});
}
@Test
public void revokePermissionWithInvalidPkg() throws Exception {
// No return value, call is ignored
runWithShellPermissionIdentity(
() -> sController.revokeRuntimePermission("invalid.package", READ_CALENDAR));
}
@Test
public void revokePermissionWithInvalidPermission() throws Exception {
// No return value, call is ignored
runWithShellPermissionIdentity(
() -> sController.revokeRuntimePermission(APP2, "invalid.permission"));
}
@Test(expected = NullPointerException.class)
public void revokePermissionWithNullPkg() throws Exception {
sController.revokeRuntimePermission(null, READ_CALENDAR);
}
@Test(expected = NullPointerException.class)
public void revokePermissionWithNullPermission() throws Exception {
sController.revokeRuntimePermission(APP2, null);
}
// TODO: Add more tests for countPermissionAppsGranted when the method can be safely called
// multiple times in a row
@Test
public void countPermissionAppsGranted() {
runWithShellPermissionIdentity(
() -> {
CompletableFuture<Integer> numApps = new CompletableFuture<>();
sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION),
COUNT_ONLY_WHEN_GRANTED, numApps::complete, null);
// TODO: Better would be to count before, grant a permission, count again and
// then compare before and after
assertThat(numApps.get()).isAtLeast(1);
});
}
@Test(expected = NullPointerException.class)
public void countPermissionAppsNullPermission() {
sController.countPermissionApps(null, 0, (n) -> { }, null);
}
@Test(expected = IllegalArgumentException.class)
public void countPermissionAppsInvalidFlags() {
sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION), -1, (n) -> { }, null);
}
@Test(expected = NullPointerException.class)
public void countPermissionAppsNullCallback() {
sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION), 0, null, null);
}
}