blob: 9026262db8971ec941523fcb46147ecc76621689 [file] [log] [blame]
/*
* Copyright (C) 2019 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.server.policy;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_MEDIA_STORAGE;
import static android.app.AppOpsManager.OP_LEGACY_STORAGE;
import static android.app.AppOpsManager.OP_NONE;
import static android.content.pm.PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION;
import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT;
import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT;
import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static java.lang.Integer.min;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.os.storage.StorageManagerInternal;
import android.provider.DeviceConfig;
import com.android.server.LocalServices;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import java.util.Arrays;
import java.util.HashSet;
/**
* The behavior of soft restricted permissions is different for each permission. This class collects
* the policies in one place.
*
* This is the twin of
* {@link com.android.packageinstaller.permission.utils.SoftRestrictedPermissionPolicy}
*/
public abstract class SoftRestrictedPermissionPolicy {
private static final int FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT =
FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT
| FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT
| FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT;
private static final SoftRestrictedPermissionPolicy DUMMY_POLICY =
new SoftRestrictedPermissionPolicy() {
@Override
public boolean mayGrantPermission() {
return true;
}
};
private static final HashSet<String> sForcedScopedStorageAppWhitelist = new HashSet<>(
Arrays.asList(getForcedScopedStorageAppWhitelist()));
/**
* TargetSDK is per package. To make sure two apps int the same shared UID do not fight over
* what to set, always compute the combined targetSDK.
*
* @param context A context
* @param appInfo The app that is changed
* @param user The user the app belongs to
*
* @return The minimum targetSDK of all apps sharing the uid of the app
*/
private static int getMinimumTargetSDK(@NonNull Context context,
@NonNull ApplicationInfo appInfo, @NonNull UserHandle user) {
PackageManager pm = context.getPackageManager();
int minimumTargetSDK = appInfo.targetSdkVersion;
String[] uidPkgs = pm.getPackagesForUid(appInfo.uid);
if (uidPkgs != null) {
for (String uidPkg : uidPkgs) {
if (!uidPkg.equals(appInfo.packageName)) {
ApplicationInfo uidPkgInfo;
try {
uidPkgInfo = pm.getApplicationInfoAsUser(uidPkg, 0, user);
} catch (PackageManager.NameNotFoundException e) {
continue;
}
minimumTargetSDK = min(minimumTargetSDK, uidPkgInfo.targetSdkVersion);
}
}
}
return minimumTargetSDK;
}
/**
* Get the policy for a soft restricted permission.
*
* @param context A context to use
* @param appInfo The application the permission belongs to.
* @param user The user the app belongs to.
* @param permission The name of the permission
*
* @return The policy for this permission
*/
public static @NonNull SoftRestrictedPermissionPolicy forPermission(@NonNull Context context,
@Nullable ApplicationInfo appInfo, @Nullable AndroidPackage pkg,
@Nullable UserHandle user, @NonNull String permission) {
switch (permission) {
// Storage uses a special app op to decide the mount state and supports soft restriction
// where the restricted state allows the permission but only for accessing the medial
// collections.
case READ_EXTERNAL_STORAGE: {
final boolean isWhiteListed;
boolean shouldApplyRestriction;
final int targetSDK;
final boolean hasLegacyExternalStorage;
final boolean hasRequestedLegacyExternalStorage;
final boolean hasRequestedPreserveLegacyExternalStorage;
final boolean hasWriteMediaStorageGrantedForUid;
final boolean isForcedScopedStorage;
if (appInfo != null) {
PackageManager pm = context.getPackageManager();
StorageManagerInternal smInternal =
LocalServices.getService(StorageManagerInternal.class);
int flags = pm.getPermissionFlags(permission, appInfo.packageName, user);
isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0;
hasLegacyExternalStorage = smInternal.hasLegacyExternalStorage(appInfo.uid);
hasRequestedLegacyExternalStorage = hasUidRequestedLegacyExternalStorage(
appInfo.uid, context);
hasWriteMediaStorageGrantedForUid = hasWriteMediaStorageGrantedForUid(
appInfo.uid, context);
hasRequestedPreserveLegacyExternalStorage =
pkg.hasPreserveLegacyExternalStorage();
targetSDK = getMinimumTargetSDK(context, appInfo, user);
shouldApplyRestriction = (flags & FLAG_PERMISSION_APPLY_RESTRICTION) != 0;
isForcedScopedStorage = sForcedScopedStorageAppWhitelist
.contains(appInfo.packageName);
} else {
isWhiteListed = false;
shouldApplyRestriction = false;
targetSDK = 0;
hasLegacyExternalStorage = false;
hasRequestedLegacyExternalStorage = false;
hasRequestedPreserveLegacyExternalStorage = false;
hasWriteMediaStorageGrantedForUid = false;
isForcedScopedStorage = false;
}
// We have a check in PermissionPolicyService.PermissionToOpSynchroniser.setUidMode
// to prevent apps losing files in legacy storage, because we are holding the
// package manager lock here. If we ever remove this policy that check should be
// removed as well.
return new SoftRestrictedPermissionPolicy() {
@Override
public boolean mayGrantPermission() {
return isWhiteListed || targetSDK >= Build.VERSION_CODES.Q;
}
@Override
public int getExtraAppOpCode() {
return OP_LEGACY_STORAGE;
}
@Override
public boolean mayAllowExtraAppOp() {
// The only way to get LEGACY_STORAGE (if you didn't already have it)
// is that all of the following must be true:
// 1. The flag shouldn't be restricted
if (shouldApplyRestriction) {
return false;
}
// 2. The app shouldn't be in sForcedScopedStorageAppWhitelist
if (isForcedScopedStorage) {
return false;
}
// 3. The app has WRITE_MEDIA_STORAGE, OR
// the app already has legacy external storage or requested it,
// and is < R.
return hasWriteMediaStorageGrantedForUid
|| ((hasLegacyExternalStorage || hasRequestedLegacyExternalStorage)
&& targetSDK < Build.VERSION_CODES.R);
}
@Override
public boolean mayDenyExtraAppOpIfGranted() {
// If you're an app targeting < R, you can keep the app op for
// as long as you meet the conditions required to acquire it.
if (targetSDK < Build.VERSION_CODES.R) {
return !mayAllowExtraAppOp();
}
// For an app targeting R, the only way to lose LEGACY_STORAGE if you
// already had it is in one or more of the following conditions:
// 1. The flag became restricted
if (shouldApplyRestriction) {
return true;
}
// The package is now a part of the forced scoped storage whitelist
if (isForcedScopedStorage) {
return true;
}
// The package doesn't have WRITE_MEDIA_STORAGE,
// AND didn't request legacy storage to be preserved
if (!hasWriteMediaStorageGrantedForUid
&& !hasRequestedPreserveLegacyExternalStorage) {
return true;
}
return false;
}
};
}
case WRITE_EXTERNAL_STORAGE: {
final boolean isWhiteListed;
final int targetSDK;
if (appInfo != null) {
final int flags = context.getPackageManager().getPermissionFlags(permission,
appInfo.packageName, user);
isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0;
targetSDK = getMinimumTargetSDK(context, appInfo, user);
} else {
isWhiteListed = false;
targetSDK = 0;
}
return new SoftRestrictedPermissionPolicy() {
@Override
public boolean mayGrantPermission() {
return isWhiteListed || targetSDK >= Build.VERSION_CODES.Q;
}
};
}
default:
return DUMMY_POLICY;
}
}
private static boolean hasUidRequestedLegacyExternalStorage(int uid, @NonNull Context context) {
PackageManager packageManager = context.getPackageManager();
String[] packageNames = packageManager.getPackagesForUid(uid);
if (packageNames == null) {
return false;
}
UserHandle user = UserHandle.getUserHandleForUid(uid);
for (String packageName : packageNames) {
ApplicationInfo applicationInfo;
try {
applicationInfo = packageManager.getApplicationInfoAsUser(packageName, 0, user);
} catch (PackageManager.NameNotFoundException e) {
continue;
}
if (applicationInfo.hasRequestedLegacyExternalStorage()) {
return true;
}
}
return false;
}
private static boolean hasWriteMediaStorageGrantedForUid(int uid, @NonNull Context context) {
PackageManager packageManager = context.getPackageManager();
String[] packageNames = packageManager.getPackagesForUid(uid);
if (packageNames == null) {
return false;
}
for (String packageName : packageNames) {
if (packageManager.checkPermission(WRITE_MEDIA_STORAGE, packageName)
== PERMISSION_GRANTED) {
return true;
}
}
return false;
}
private static String[] getForcedScopedStorageAppWhitelist() {
final String rawList = DeviceConfig.getString(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
StorageManager.PROP_FORCED_SCOPED_STORAGE_WHITELIST, /*defaultValue*/"");
if (rawList == null || rawList.equals("")) {
return new String[0];
}
return rawList.split(",");
}
/**
* @return If the permission can be granted
*/
public abstract boolean mayGrantPermission();
/**
* @return An app op to be changed based on the state of the permission or
* {@link AppOpsManager#OP_NONE} if not app-op should be set.
*/
public int getExtraAppOpCode() {
return OP_NONE;
}
/**
* @return Whether the {@link #getExtraAppOpCode() app op} may be granted.
*/
public boolean mayAllowExtraAppOp() {
return false;
}
/**
* @return Whether the {@link #getExtraAppOpCode() app op} may be denied if was granted.
*/
public boolean mayDenyExtraAppOpIfGranted() {
return false;
}
}