blob: f04634ca5c61330381c4eb62be13724c7a2b0a15 [file] [log] [blame]
/*
* Copyright (C) 2016 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 com.android.packageinstaller.permission.service;
import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT;
import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED;
import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.permission.PermissionControllerManager.COUNT_ONLY_WHEN_GRANTED;
import static android.permission.PermissionControllerManager.COUNT_WHEN_SYSTEM;
import static android.permission.PermissionControllerManager.REASON_INSTALLER_POLICY_VIOLATION;
import static android.permission.PermissionControllerManager.REASON_MALWARE;
import static android.util.Xml.newSerializer;
import static com.android.packageinstaller.permission.utils.Utils.getLauncherPackages;
import static com.android.packageinstaller.permission.utils.Utils.isSystem;
import static com.android.packageinstaller.permission.utils.Utils.shouldShowPermission;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.permission.PermissionControllerService;
import android.permission.PermissionManager;
import android.permission.RuntimePermissionPresentationInfo;
import android.permission.RuntimePermissionUsageInfo;
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 com.android.packageinstaller.permission.model.AppPermissionGroup;
import com.android.packageinstaller.permission.model.AppPermissionUsage;
import com.android.packageinstaller.permission.model.AppPermissionUsage.GroupUsage;
import com.android.packageinstaller.permission.model.AppPermissions;
import com.android.packageinstaller.permission.model.Permission;
import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.packageinstaller.role.service.PermissionControllerServiceImplRoleMixin;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Calls from the system into the permission controller
*/
public final class PermissionControllerServiceImpl extends PermissionControllerService {
private static final String LOG_TAG = PermissionControllerServiceImpl.class.getSimpleName();
/**
* Expand {@code perms} by split permissions for an app with the given targetSDK.
*
* @param perms The permissions that should be expanded
* @param targetSDK The target SDK to expand for
*
* @return The expanded permissions
*/
private @NonNull ArrayList<String> addSplitPermissions(@NonNull List<String> perms,
int targetSDK) {
List<PermissionManager.SplitPermissionInfo> splitPerms =
getSystemService(PermissionManager.class).getSplitPermissions();
// Add split permissions to the request
ArrayList<String> expandedPerms = new ArrayList<>(perms);
int numReqPerms = perms.size();
for (int reqPermNum = 0; reqPermNum < numReqPerms; reqPermNum++) {
String reqPerm = perms.get(reqPermNum);
int numSplitPerms = splitPerms.size();
for (int splitPermNum = 0; splitPermNum < numSplitPerms; splitPermNum++) {
PermissionManager.SplitPermissionInfo splitPerm = splitPerms.get(splitPermNum);
if (targetSDK < splitPerm.getTargetSdk()
&& splitPerm.getSplitPermission().equals(reqPerm)) {
expandedPerms.addAll(splitPerm.getNewPermissions());
}
}
}
return expandedPerms;
}
/**
* Get the package info for a package.
*
* @param pkg The package name
*
* @return the package info or {@code null} if the package could not be found
*/
private @Nullable PackageInfo getPkgInfo(@NonNull String pkg) {
try {
return getPackageManager().getPackageInfo(pkg, GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
Log.w(LOG_TAG, pkg + " not found", e);
return null;
}
}
/**
* Given a set of permissions, find all permission groups of an app that can be revoked and that
* contain any of the permissions.
*
* @param permissions The permissions to revoke
* @param appPerms The {@link AppPermissions} for the app that is currently investigated
*
* @return The groups to revoke
*/
private @NonNull ArrayList<AppPermissionGroup> getRevocableGroupsForPermissions(
@NonNull ArrayList<String> permissions, @NonNull AppPermissions appPerms) {
ArrayList<AppPermissionGroup> groupsToRevoke = new ArrayList<>();
int numGroups = appPerms.getPermissionGroups().size();
for (int groupNum = 0; groupNum < numGroups; groupNum++) {
AppPermissionGroup group = appPerms.getPermissionGroups().get(groupNum);
// Do not override fixed permissions
if (group.isPolicyFixed() || group.isSystemFixed()) {
continue;
}
int numPerms = permissions.size();
for (int permNum = 0; permNum < numPerms; permNum++) {
String reqPerm = permissions.get(permNum);
if (group.hasPermission(reqPerm)) {
groupsToRevoke.add(group);
// If fg permissions get revoked also revoke bg permissions as bg
// permissions require fg permissions.
AppPermissionGroup bgPerms = group.getBackgroundPermissions();
if (bgPerms != null) {
groupsToRevoke.add(bgPerms);
}
} else {
AppPermissionGroup bgPerms = group.getBackgroundPermissions();
if (bgPerms != null && bgPerms.hasPermission(reqPerm)) {
groupsToRevoke.add(bgPerms);
}
}
}
}
return groupsToRevoke;
}
/**
* Revoke all permissions of some groups.
*
* @param groupsToRevoke The groups
*
* @return The permissions that were revoked
*/
private @NonNull ArrayList<String> revokePermissionGroups(
@NonNull ArrayList<AppPermissionGroup> groupsToRevoke) {
ArrayList<String> revokedPerms = new ArrayList<>();
int numGroupsToRevoke = groupsToRevoke.size();
for (int groupsToRevokeNum = 0; groupsToRevokeNum < numGroupsToRevoke;
groupsToRevokeNum++) {
AppPermissionGroup group = groupsToRevoke.get(groupsToRevokeNum);
ArrayList<Permission> perms = group.getPermissions();
// Mark the permissions as reviewed as we don't want to use to accidentally grant
// the permission during review
group.unsetReviewRequired();
int numPerms = perms.size();
for (int permNum = 0; permNum < numPerms; permNum++) {
Permission perm = perms.get(permNum);
// Only count individual permissions that are actually revoked
if (perm.isGrantedIncludingAppOp()) {
revokedPerms.add(perm.getName());
}
}
group.revokeRuntimePermissions(false);
}
return revokedPerms;
}
@Override
public @NonNull Map<String, List<String>> onRevokeRuntimePermissions(
@NonNull Map<String, List<String>> request, boolean doDryRun,
int reason, @NonNull String callerPackageName) {
// The reason parameter is not checked by platform code as this might need to be updated
// async to platform releases.
if (reason != REASON_MALWARE && reason != REASON_INSTALLER_POLICY_VIOLATION) {
Log.e(LOG_TAG, "Invalid reason " + reason);
return Collections.emptyMap();
}
PackageManager pm = getPackageManager();
PackageInfo callerPkgInfo = getPkgInfo(callerPackageName);
if (callerPkgInfo == null) {
return Collections.emptyMap();
}
int callerTargetSdk = callerPkgInfo.applicationInfo.targetSdkVersion;
Map<String, List<String>> actuallyRevokedPerms = new ArrayMap<>();
ArrayList<AppPermissions> appsWithRevokedPerms = new ArrayList<>();
for (Map.Entry<String, List<String>> appRequest : request.entrySet()) {
PackageInfo requestedPkgInfo = getPkgInfo(appRequest.getKey());
if (requestedPkgInfo == null) {
continue;
}
// Permissions are per UID. Hence permissions will be removed from all apps sharing an
// UID.
String[] pkgNames = pm.getPackagesForUid(requestedPkgInfo.applicationInfo.uid);
if (pkgNames == null) {
continue;
}
int numPkgNames = pkgNames.length;
for (int pkgNum = 0; pkgNum < numPkgNames; pkgNum++) {
String pkgName = pkgNames[pkgNum];
PackageInfo pkgInfo = getPkgInfo(pkgName);
if (pkgInfo == null) {
continue;
}
// If the revocation is because of a market policy violation only the installer can
// revoke the permissions.
if (reason == REASON_INSTALLER_POLICY_VIOLATION
&& !callerPackageName.equals(pm.getInstallerPackageName(pkgName))) {
Log.i(LOG_TAG, "Ignoring " + pkgName + " as it is not installed by "
+ callerPackageName);
continue;
}
// In rare cases the caller does not know about the permissions that have been added
// due to splits. Hence add them now.
ArrayList<String> expandedPerms = addSplitPermissions(appRequest.getValue(),
callerTargetSdk);
AppPermissions appPerms = new AppPermissions(this, pkgInfo, false, true, null);
// First find the groups that should be revoked and then revoke all permissions of
// these groups. This is needed as soon as a single permission in the group is
// granted, all other permissions get auto-granted on request.
ArrayList<AppPermissionGroup> groupsToRevoke = getRevocableGroupsForPermissions(
expandedPerms, appPerms);
ArrayList<String> revokedPerms = revokePermissionGroups(groupsToRevoke);
// In racy conditions the group might not have had granted permissions anymore
if (!revokedPerms.isEmpty()) {
actuallyRevokedPerms.put(pkgName, revokedPerms);
appsWithRevokedPerms.add(appPerms);
}
}
}
// Persist changes after we computed everything to remove
// This is necessary as we would otherwise only look at the first app of a shared UID.
if (!doDryRun) {
int numChangedApps = appsWithRevokedPerms.size();
for (int i = 0; i < numChangedApps; i++) {
appsWithRevokedPerms.get(i).persistChanges();
}
}
return actuallyRevokedPerms;
}
@Override
public void onGetRuntimePermissionsBackup(@NonNull UserHandle user,
@NonNull OutputStream backup) {
BackupHelper backupHelper = new BackupHelper(this, user);
try {
XmlSerializer serializer = newSerializer();
serializer.setOutput(backup, UTF_8.name());
backupHelper.writeState(serializer);
serializer.flush();
} catch (Exception e) {
Log.e(LOG_TAG, "Unable to write permissions backup", e);
}
}
@Override
public void onRestoreRuntimePermissionsBackup(@NonNull UserHandle user,
@NonNull InputStream backup) {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(backup, StandardCharsets.UTF_8.name());
new BackupHelper(this, user).restoreState(parser);
} catch (Exception e) {
Log.e(LOG_TAG, "Exception restoring permissions: " + e.getMessage());
}
}
@Override
public boolean onRestoreDelayedRuntimePermissionsBackup(@NonNull String packageName,
@NonNull UserHandle user) {
try {
return new BackupHelper(this, user).restoreDelayedState(packageName);
} catch (Exception e) {
Log.e(LOG_TAG, "Exception restoring delayed permissions: " + e.getMessage());
return false;
}
}
@Override
public @NonNull List<RuntimePermissionPresentationInfo> onGetAppPermissions(
@NonNull String packageName) {
return onGetAppPermissions(this, packageName);
}
/**
* Implementation of {@link PermissionControllerService#onGetAppPermissions(String)}}.
* Called by this class and the legacy implementation.
*/
static @NonNull List<RuntimePermissionPresentationInfo> onGetAppPermissions(
@NonNull Context context, @NonNull String packageName) {
final PackageInfo packageInfo;
try {
packageInfo = context.getPackageManager().getPackageInfo(packageName, GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "Error getting package:" + packageName, e);
return Collections.emptyList();
}
List<RuntimePermissionPresentationInfo> permissions = new ArrayList<>();
AppPermissions appPermissions = new AppPermissions(context, packageInfo, false, null);
for (AppPermissionGroup group : appPermissions.getPermissionGroups()) {
if (shouldShowPermission(context, group)) {
final boolean granted = group.areRuntimePermissionsGranted();
final boolean standard = Utils.OS_PKG.equals(group.getDeclaringPackage());
RuntimePermissionPresentationInfo permission =
new RuntimePermissionPresentationInfo(group.getLabel(),
granted, standard);
permissions.add(permission);
}
}
return permissions;
}
@Override
public void onRevokeRuntimePermission(@NonNull String packageName,
@NonNull String permissionName) {
try {
final PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName,
GET_PERMISSIONS);
final AppPermissions appPermissions = new AppPermissions(this, packageInfo, false,
null);
final AppPermissionGroup appPermissionGroup = appPermissions.getGroupForPermission(
permissionName);
if (appPermissionGroup != null) {
appPermissionGroup.revokeRuntimePermissions(false);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "Error getting package:" + packageName, e);
}
}
@Override
public int onCountPermissionApps(@NonNull List<String> permissionNames, int flags) {
boolean countSystem = (flags & COUNT_WHEN_SYSTEM) != 0;
boolean countOnlyGranted = (flags & COUNT_ONLY_WHEN_GRANTED) != 0;
List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(GET_PERMISSIONS);
ArraySet<String> launcherPkgs = getLauncherPackages(this);
int numApps = 0;
int numPkgs = pkgs.size();
for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
PackageInfo pkg = pkgs.get(pkgNum);
if (!countSystem && isSystem(pkg.applicationInfo, launcherPkgs)) {
continue;
}
int numPerms = permissionNames.size();
for (int permNum = 0; permNum < numPerms; permNum++) {
String perm = permissionNames.get(permNum);
AppPermissionGroup group = AppPermissionGroup.create(this, pkg,
permissionNames.get(permNum), true);
if (group == null || !shouldShowPermission(this, group)) {
continue;
}
AppPermissionGroup subGroup = null;
if (group.hasPermission(perm)) {
subGroup = group;
} else {
AppPermissionGroup bgGroup = group.getBackgroundPermissions();
if (bgGroup != null && bgGroup.hasPermission(perm)) {
subGroup = group;
}
}
if (subGroup != null) {
if (!countOnlyGranted || subGroup.areRuntimePermissionsGranted()) {
// The permission might not be granted, but some permissions of the group
// are granted. In this case the permission is granted silently when the app
// asks for it.
// Hence this is as-good-as-granted and we count it.
numApps++;
break;
}
}
}
}
return numApps;
}
@Override public @NonNull List<RuntimePermissionUsageInfo> onGetPermissionUsages(
boolean countSystem, long numMillis) {
ArraySet<String> launcherPkgs = getLauncherPackages(this);
ArrayMap<CharSequence, Integer> groupUsers = new ArrayMap<>();
long curTime = System.currentTimeMillis();
PermissionUsages usages = new PermissionUsages(this);
long filterTimeBeginMillis = Math.max(System.currentTimeMillis() - numMillis, 0);
usages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE,
PermissionUsages.USAGE_FLAG_LAST | PermissionUsages.USAGE_FLAG_HISTORICAL, null,
false, false, null, true);
List<AppPermissionUsage> appPermissionUsages = usages.getUsages();
int numApps = appPermissionUsages.size();
for (int appNum = 0; appNum < numApps; appNum++) {
AppPermissionUsage appPermissionUsage = appPermissionUsages.get(appNum);
if (appPermissionUsage.getAccessCount() <= 0) {
continue;
}
if (!countSystem && isSystem(appPermissionUsage.getApp(), launcherPkgs)) {
continue;
}
List<GroupUsage> appGroups = appPermissionUsage.getGroupUsages();
int numGroups = appGroups.size();
for (int groupNum = 0; groupNum < numGroups; groupNum++) {
GroupUsage groupUsage = appGroups.get(groupNum);
if (groupUsage.getAccessCount() <= 0) {
continue;
}
if (!shouldShowPermission(this, groupUsage.getGroup())) {
continue;
}
CharSequence groupLabel = groupUsage.getGroup().getName();
Integer numUsers = groupUsers.get(groupLabel);
if (numUsers == null) {
groupUsers.put(groupLabel, 1);
} else {
groupUsers.put(groupLabel, numUsers + 1);
}
}
}
List<RuntimePermissionUsageInfo> users = new ArrayList<>();
int numGroups = groupUsers.size();
for (int groupNum = 0; groupNum < numGroups; groupNum++) {
users.add(new RuntimePermissionUsageInfo(groupUsers.keyAt(groupNum),
groupUsers.valueAt(groupNum)));
}
return users;
}
@Override
public boolean onIsApplicationQualifiedForRole(@NonNull String roleName,
@NonNull String packageName) {
return PermissionControllerServiceImplRoleMixin.onIsApplicationQualifiedForRole(roleName,
packageName, this);
}
@Override
public boolean onSetRuntimePermissionGrantStateByDeviceAdmin(@NonNull String callerPackageName,
@NonNull String packageName, @NonNull String unexpandedPermission, int grantState) {
PackageInfo callerPkgInfo = getPkgInfo(callerPackageName);
if (callerPkgInfo == null) {
Log.w(LOG_TAG, "Cannot fix " + unexpandedPermission + " as admin "
+ callerPackageName + " cannot be found");
return false;
}
PackageInfo pkgInfo = getPkgInfo(packageName);
if (pkgInfo == null) {
Log.w(LOG_TAG, "Cannot fix " + unexpandedPermission + " as " + packageName
+ " cannot be found");
return false;
}
ArrayList<String> expandedPermissions = addSplitPermissions(
Collections.singletonList(unexpandedPermission),
callerPkgInfo.applicationInfo.targetSdkVersion);
int numPerms = expandedPermissions.size();
for (int i = 0; i < numPerms; i++) {
String perm = expandedPermissions.get(i);
AppPermissionGroup group = AppPermissionGroup.create(this, pkgInfo, perm, true);
if (group == null || group.isSystemFixed()) {
continue;
}
switch (grantState) {
case PERMISSION_GRANT_STATE_GRANTED:
group.getPermission(perm).setPolicyFixed(true);
group.grantRuntimePermissions(false, new String[]{perm});
break;
case PERMISSION_GRANT_STATE_DENIED:
group.getPermission(perm).setPolicyFixed(true);
group.revokeRuntimePermissions(false, new String[]{perm});
break;
case PERMISSION_GRANT_STATE_DEFAULT:
group.getPermission(perm).setPolicyFixed(false);
break;
default:
return false;
}
group.persistChanges(!callerPackageName.equals(packageName));
}
return true;
}
}