blob: c2becc749a7bbf1fbfcdcf96559e1b43f620e9cd [file] [log] [blame]
/*
* Copyright (C) 2015 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.permission2.cts;
import static android.content.pm.PermissionInfo.FLAG_INSTALLED;
import static android.content.pm.PermissionInfo.PROTECTION_MASK_BASE;
import static android.os.Build.VERSION.SECURITY_PATCH;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
import android.os.Build;
import android.os.storage.StorageManager;
import android.platform.test.annotations.AppModeFull;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Xml;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Tests for permission policy on the platform.
*/
@AppModeFull(reason = "Instant apps cannot read the system servers permission")
@RunWith(AndroidJUnit4.class)
public class PermissionPolicyTest {
private static final Date HIDE_NON_SYSTEM_OVERLAY_WINDOWS_PATCH_DATE = parseDate("2017-11-01");
private static final String HIDE_NON_SYSTEM_OVERLAY_WINDOWS_PERMISSION
= "android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS";
private static final String LOG_TAG = "PermissionProtectionTest";
private static final String PLATFORM_PACKAGE_NAME = "android";
private static final String PLATFORM_ROOT_NAMESPACE = "android.";
private static final String AUTOMOTIVE_SERVICE_PACKAGE_NAME = "com.android.car";
private static final String TAG_PERMISSION = "permission";
private static final String TAG_PERMISSION_GROUP = "permission-group";
private static final String ATTR_NAME = "name";
private static final String ATTR_PERMISSION_GROUP = "permissionGroup";
private static final String ATTR_PERMISSION_FLAGS = "permissionFlags";
private static final String ATTR_PROTECTION_LEVEL = "protectionLevel";
private static final String ATTR_BACKGROUND_PERMISSION = "backgroundPermission";
private static final String CAMERA_OPEN_CLOSE_LISTENER_PERMISSION =
"android.permission.CAMERA_OPEN_CLOSE_LISTENER";
private static final Context sContext =
InstrumentationRegistry.getInstrumentation().getTargetContext();
@Test
public void platformPermissionPolicyIsUnaltered() throws Exception {
Map<String, PermissionInfo> declaredPermissionsMap =
getPermissionsForPackage(sContext, PLATFORM_PACKAGE_NAME);
List<String> offendingList = new ArrayList<>();
List<PermissionGroupInfo> declaredGroups = sContext.getPackageManager()
.getAllPermissionGroups(0);
Set<String> declaredGroupsSet = new ArraySet<>();
for (PermissionGroupInfo declaredGroup : declaredGroups) {
declaredGroupsSet.add(declaredGroup.name);
}
Set<String> expectedPermissionGroups = loadExpectedPermissionGroupNames(
R.raw.android_manifest);
List<ExpectedPermissionInfo> expectedPermissions = loadExpectedPermissions(
R.raw.android_manifest);
if (sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
expectedPermissions.addAll(loadExpectedPermissions(R.raw.automotive_android_manifest));
declaredPermissionsMap.putAll(
getPermissionsForPackage(sContext, AUTOMOTIVE_SERVICE_PACKAGE_NAME));
}
for (ExpectedPermissionInfo expectedPermission : expectedPermissions) {
String expectedPermissionName = expectedPermission.name;
if (shouldSkipPermission(expectedPermissionName)) {
continue;
}
// Skip CAMERA OPEN_CLOSE_LISTENER_PERMISSION check for Android Q
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q
&& expectedPermissionName.equals(CAMERA_OPEN_CLOSE_LISTENER_PERMISSION)) {
declaredPermissionsMap.remove(expectedPermissionName);
continue;
}
// OEMs cannot remove permissions
PermissionInfo declaredPermission = declaredPermissionsMap.get(expectedPermissionName);
if (declaredPermission == null) {
offendingList.add("Permission " + expectedPermissionName + " must be declared");
continue;
}
// We want to end up with OEM defined permissions and groups to check their namespace
declaredPermissionsMap.remove(expectedPermissionName);
// OEMs cannot change permission protection
final int expectedProtection = expectedPermission.protectionLevel
& PROTECTION_MASK_BASE;
final int declaredProtection = declaredPermission.protectionLevel
& PROTECTION_MASK_BASE;
if (expectedProtection != declaredProtection) {
offendingList.add(
String.format(
"Permission %s invalid protection level %x, expected %x",
expectedPermissionName, declaredProtection, expectedProtection));
}
// OEMs cannot change permission flags
final int expectedFlags = expectedPermission.flags;
final int declaredFlags = (declaredPermission.flags & ~FLAG_INSTALLED);
if (expectedFlags != declaredFlags) {
offendingList.add(
String.format(
"Permission %s invalid flags %x, expected %x",
expectedPermissionName,
declaredFlags,
expectedFlags));
}
// OEMs cannot change permission protection flags
final int expectedProtectionFlags =
expectedPermission.protectionLevel & ~PROTECTION_MASK_BASE;
final int declaredProtectionFlags = declaredPermission.getProtectionFlags();
if (expectedProtectionFlags != declaredProtectionFlags) {
offendingList.add(
String.format(
"Permission %s invalid enforced protection %x, expected %x",
expectedPermissionName,
declaredProtectionFlags,
expectedProtectionFlags));
}
// OEMs cannot change permission grouping
if ((declaredPermission.protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) != 0) {
if (!Objects.equals(expectedPermission.group, declaredPermission.group)) {
offendingList.add(
"Permission " + expectedPermissionName + " not in correct group "
+ "(expected=" + expectedPermission.group + " actual="
+ declaredPermission.group);
}
if (declaredPermission.group != null
&& !declaredGroupsSet.contains(declaredPermission.group)) {
offendingList.add(
"Permission group " + expectedPermission.group + " must be defined");
}
}
// OEMs cannot change background permission mapping
if (!Objects.equals(expectedPermission.backgroundPermission,
declaredPermission.backgroundPermission)) {
offendingList.add(
String.format(
"Permission %s invalid background permission %s, expected %s",
expectedPermissionName,
declaredPermission.backgroundPermission,
expectedPermission.backgroundPermission));
}
}
// OEMs cannot define permissions in the platform namespace
for (String permission : declaredPermissionsMap.keySet()) {
if (permission.startsWith(PLATFORM_ROOT_NAMESPACE)) {
final PermissionInfo permInfo = declaredPermissionsMap.get(permission);
offendingList.add(
"Cannot define permission " + permission
+ ", package " + permInfo.packageName
+ " in android namespace");
}
}
// OEMs cannot define groups in the platform namespace
for (PermissionGroupInfo declaredGroup : declaredGroups) {
if (!expectedPermissionGroups.contains(declaredGroup.name)) {
if (declaredGroup.name != null) {
if (declaredGroup.packageName.equals(PLATFORM_PACKAGE_NAME)
&& declaredGroup.name.startsWith(PLATFORM_ROOT_NAMESPACE)) {
offendingList.add(
"Cannot define group " + declaredGroup.name
+ ", package " + declaredGroup.packageName
+ " in android namespace");
}
}
}
}
// OEMs cannot define new ephemeral permissions
for (String permission : declaredPermissionsMap.keySet()) {
PermissionInfo info = declaredPermissionsMap.get(permission);
if ((info.protectionLevel & PermissionInfo.PROTECTION_FLAG_INSTANT) != 0) {
offendingList.add("Cannot define new instant permission " + permission);
}
}
// Fail on any offending item
assertThat(offendingList).named("list of offending permissions").isEmpty();
}
private List<ExpectedPermissionInfo> loadExpectedPermissions(int resourceId) throws Exception {
List<ExpectedPermissionInfo> permissions = new ArrayList<>();
try (InputStream in = sContext.getResources().openRawResource(resourceId)) {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(in, null);
final int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
if (TAG_PERMISSION.equals(parser.getName())) {
ExpectedPermissionInfo permissionInfo = new ExpectedPermissionInfo(
parser.getAttributeValue(null, ATTR_NAME),
parser.getAttributeValue(null, ATTR_PERMISSION_GROUP),
parser.getAttributeValue(null, ATTR_BACKGROUND_PERMISSION),
parsePermissionFlags(
parser.getAttributeValue(null, ATTR_PERMISSION_FLAGS)),
parseProtectionLevel(
parser.getAttributeValue(null, ATTR_PROTECTION_LEVEL)));
permissions.add(permissionInfo);
} else {
Log.e(LOG_TAG, "Unknown tag " + parser.getName());
}
}
}
// STOPSHIP: remove this once isolated storage is always enabled
if (!StorageManager.hasIsolatedStorage()) {
Iterator<ExpectedPermissionInfo> it = permissions.iterator();
while (it.hasNext()) {
final ExpectedPermissionInfo pi = it.next();
switch (pi.name) {
case android.Manifest.permission.ACCESS_MEDIA_LOCATION:
case android.Manifest.permission.WRITE_OBB:
it.remove();
break;
}
}
}
return permissions;
}
private Set<String> loadExpectedPermissionGroupNames(int resourceId) throws Exception {
ArraySet<String> permissionGroups = new ArraySet<>();
try (InputStream in = sContext.getResources().openRawResource(resourceId)) {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(in, null);
final int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
if (TAG_PERMISSION_GROUP.equals(parser.getName())) {
permissionGroups.add(parser.getAttributeValue(null, ATTR_NAME));
} else {
Log.e(LOG_TAG, "Unknown tag " + parser.getName());
}
}
}
return permissionGroups;
}
private static int parsePermissionFlags(@Nullable String permissionFlagsString) {
if (permissionFlagsString == null) {
return 0;
}
int protectionFlags = 0;
String[] fragments = permissionFlagsString.split("\\|");
for (String fragment : fragments) {
switch (fragment.trim()) {
case "removed": {
protectionFlags |= PermissionInfo.FLAG_REMOVED;
} break;
case "costsMoney": {
protectionFlags |= PermissionInfo.FLAG_COSTS_MONEY;
} break;
case "hardRestricted": {
protectionFlags |= PermissionInfo.FLAG_HARD_RESTRICTED;
} break;
case "immutablyRestricted": {
protectionFlags |= PermissionInfo.FLAG_IMMUTABLY_RESTRICTED;
} break;
case "softRestricted": {
protectionFlags |= PermissionInfo.FLAG_SOFT_RESTRICTED;
} break;
}
}
return protectionFlags;
}
private static int parseProtectionLevel(String protectionLevelString) {
int protectionLevel = 0;
String[] fragments = protectionLevelString.split("\\|");
for (String fragment : fragments) {
switch (fragment.trim()) {
case "normal": {
protectionLevel |= PermissionInfo.PROTECTION_NORMAL;
} break;
case "dangerous": {
protectionLevel |= PermissionInfo.PROTECTION_DANGEROUS;
} break;
case "signature": {
protectionLevel |= PermissionInfo.PROTECTION_SIGNATURE;
} break;
case "signatureOrSystem": {
protectionLevel |= PermissionInfo.PROTECTION_SIGNATURE;
protectionLevel |= PermissionInfo.PROTECTION_FLAG_SYSTEM;
} break;
case "system": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_SYSTEM;
} break;
case "installer": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_INSTALLER;
} break;
case "verifier": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_VERIFIER;
} break;
case "preinstalled": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_PREINSTALLED;
} break;
case "pre23": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_PRE23;
} break;
case "appop": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_APPOP;
} break;
case "development": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_DEVELOPMENT;
} break;
case "privileged": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_PRIVILEGED;
} break;
case "oem": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_OEM;
} break;
case "vendorPrivileged": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED;
} break;
case "setup": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_SETUP;
} break;
case "textClassifier": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER;
} break;
case "wellbeing": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_WELLBEING;
} break;
case "configurator": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_CONFIGURATOR;
} break;
case "incidentReportApprover": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_INCIDENT_REPORT_APPROVER;
} break;
case "documenter": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_DOCUMENTER;
} break;
case "appPredictor": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_APP_PREDICTOR;
} break;
case "instant": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_INSTANT;
} break;
case "runtime": {
protectionLevel |= PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY;
} break;
}
}
return protectionLevel;
}
private static Map<String, PermissionInfo> getPermissionsForPackage(Context context, String pkg)
throws NameNotFoundException {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS);
Map<String, PermissionInfo> declaredPermissionsMap = new ArrayMap<>();
for (PermissionInfo declaredPermission : packageInfo.permissions) {
declaredPermissionsMap.put(declaredPermission.name, declaredPermission);
}
return declaredPermissionsMap;
}
private static Date parseDate(String date) {
Date patchDate = new Date();
try {
SimpleDateFormat template = new SimpleDateFormat("yyyy-MM-dd");
patchDate = template.parse(date);
} catch (ParseException e) {
}
return patchDate;
}
private boolean shouldSkipPermission(String permissionName) {
return parseDate(SECURITY_PATCH).before(HIDE_NON_SYSTEM_OVERLAY_WINDOWS_PATCH_DATE) &&
HIDE_NON_SYSTEM_OVERLAY_WINDOWS_PERMISSION.equals(permissionName);
}
private class ExpectedPermissionInfo {
final @NonNull String name;
final @Nullable String group;
final @Nullable String backgroundPermission;
final int flags;
final int protectionLevel;
private ExpectedPermissionInfo(@NonNull String name, @Nullable String group,
@Nullable String backgroundPermission, int flags, int protectionLevel) {
this.name = name;
this.group = group;
this.backgroundPermission = backgroundPermission;
this.flags = flags;
this.protectionLevel = protectionLevel;
}
}
}