Allow apps to bulk revoke permissions with the correct semantics

Test: atest --test-mapping frameworks/base/core/java/android/permission/:presubmit
Fixes: 120269238
Change-Id: Ib9eb244f1c89c09eee1f39e3abb65c1189f7a6f4
This commit is contained in:
Philip P. Moltmann
2018-12-17 20:45:40 -08:00
parent bbb539a172
commit 7868952db3
6 changed files with 296 additions and 3 deletions

View File

@@ -4609,6 +4609,17 @@ package android.os.storage {
package android.permission {
public final class PermissionControllerManager {
method public void revokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>, boolean, int, java.util.concurrent.Executor, android.permission.PermissionControllerManager.OnRevokeRuntimePermissionsCallback);
field public static final int REASON_INSTALLER_POLICY_VIOLATION = 2; // 0x2
field public static final int REASON_MALWARE = 1; // 0x1
}
public static abstract class PermissionControllerManager.OnRevokeRuntimePermissionsCallback {
ctor public PermissionControllerManager.OnRevokeRuntimePermissionsCallback();
method public abstract void onRevokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>);
}
public abstract class PermissionControllerService extends android.app.Service {
ctor public PermissionControllerService();
method public final void attachBaseContext(android.content.Context);
@@ -4616,6 +4627,7 @@ package android.permission {
method public abstract int onCountPermissionApps(java.util.List<java.lang.String>, boolean, boolean);
method public abstract java.util.List<android.permission.RuntimePermissionPresentationInfo> onGetAppPermissions(java.lang.String);
method public abstract void onRevokeRuntimePermission(java.lang.String, java.lang.String);
method public abstract java.util.Map<java.lang.String, java.util.List<java.lang.String>> onRevokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>, boolean, int, java.lang.String);
field public static final java.lang.String SERVICE_INTERFACE = "android.permission.PermissionControllerService";
}

View File

@@ -985,6 +985,21 @@ package android.os.strictmode {
}
package android.permission {
public final class PermissionControllerManager {
method public void revokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>, boolean, int, java.util.concurrent.Executor, android.permission.PermissionControllerManager.OnRevokeRuntimePermissionsCallback);
field public static final int REASON_INSTALLER_POLICY_VIOLATION = 2; // 0x2
field public static final int REASON_MALWARE = 1; // 0x1
}
public static abstract class PermissionControllerManager.OnRevokeRuntimePermissionsCallback {
ctor public PermissionControllerManager.OnRevokeRuntimePermissionsCallback();
method public abstract void onRevokeRuntimePermissions(java.util.Map<java.lang.String, java.util.List<java.lang.String>>);
}
}
package android.print {
public final class PrintJobInfo implements android.os.Parcelable {

View File

@@ -17,6 +17,7 @@
package android.permission;
import android.os.RemoteCallback;
import android.os.Bundle;
/**
* Interface for system apps to communication with the permission controller.
@@ -24,6 +25,8 @@ import android.os.RemoteCallback;
* @hide
*/
oneway interface IPermissionController {
void revokeRuntimePermissions(in Bundle request, boolean doDryRun, int reason,
String callerPackageName, in RemoteCallback callback);
void getAppPermissions(String packageName, in RemoteCallback callback);
void revokeRuntimePermission(String packageName, String permissionName);
void countPermissionApps(in List<String> permissionNames, boolean countOnlyGranted,

View File

@@ -22,46 +22,97 @@ import static com.android.internal.util.Preconditions.checkCollectionElementsNot
import static com.android.internal.util.Preconditions.checkNotNull;
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService;
import com.android.internal.infra.AbstractRemoteService;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Interface for communicating with the permission controller from system apps. All UI operations
* regarding permissions and any changes to the permission state should flow through this
* interface.
* Interface for communicating with the permission controller.
*
* @hide
*/
@TestApi
@SystemApi
@SystemService(Context.PERMISSION_CONTROLLER_SERVICE)
public final class PermissionControllerManager {
private static final String TAG = PermissionControllerManager.class.getSimpleName();
/**
* The key for retrieving the result from the returned bundle.
*
* @hide
*/
public static final String KEY_RESULT =
"android.permission.PermissionControllerManager.key.result";
/** @hide */
@IntDef(prefix = { "REASON_" }, value = {
REASON_MALWARE,
REASON_INSTALLER_POLICY_VIOLATION,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Reason {}
/** The permissions are revoked because the apps holding the permissions are malware */
public static final int REASON_MALWARE = 1;
/**
* The permissions are revoked because the apps holding the permissions violate a policy of the
* app that installed it.
*
* <p>If this reason is used only permissions of apps that are installed by the caller of the
* API can be revoked.
*/
public static final int REASON_INSTALLER_POLICY_VIOLATION = 2;
/**
* Callback for delivering the result of {@link #revokeRuntimePermissions}.
*/
public abstract static class OnRevokeRuntimePermissionsCallback {
/**
* The result for {@link #revokeRuntimePermissions}.
*
* @param revoked The actually revoked permissions as
* {@code Map<packageName, List<permission>>}
*/
public abstract void onRevokeRuntimePermissions(@NonNull Map<String, List<String>> revoked);
}
/**
* Callback for delivering the result of {@link #getAppPermissions}.
*
* @hide
*/
public interface OnGetAppPermissionResultCallback {
/**
@@ -75,6 +126,8 @@ public final class PermissionControllerManager {
/**
* Callback for delivering the result of {@link #countPermissionApps}.
*
* @hide
*/
public interface OnCountPermissionAppsResultCallback {
/**
@@ -86,23 +139,61 @@ public final class PermissionControllerManager {
void onCountPermissionApps(int numApps);
}
private final @NonNull Context mContext;
private final RemoteService mRemoteService;
/** @hide */
public PermissionControllerManager(@NonNull Context context) {
Intent intent = new Intent(SERVICE_INTERFACE);
intent.setPackage(context.getPackageManager().getPermissionControllerPackageName());
ResolveInfo serviceInfo = context.getPackageManager().resolveService(intent, 0);
mContext = context;
mRemoteService = new RemoteService(context,
serviceInfo.getComponentInfo().getComponentName());
}
/**
* Revoke a set of runtime permissions for various apps.
*
* @param request The permissions to revoke as {@code Map<packageName, List<permission>>}
* @param doDryRun Compute the permissions that would be revoked, but not actually revoke them
* @param reason Why the permission should be revoked
* @param executor Executor on which to invoke the callback
* @param callback Callback to receive the result
*/
@RequiresPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS)
public void revokeRuntimePermissions(@NonNull Map<String, List<String>> request,
boolean doDryRun, @Reason int reason, @NonNull @CallbackExecutor Executor executor,
@NonNull OnRevokeRuntimePermissionsCallback callback) {
// Check input to fail immediately instead of inside the async request
checkNotNull(executor);
checkNotNull(callback);
checkNotNull(request);
for (Map.Entry<String, List<String>> appRequest : request.entrySet()) {
checkNotNull(appRequest.getKey());
checkCollectionElementsNotNull(appRequest.getValue(), "permissions");
}
// Check required permission to fail immediately instead of inside the oneway binder call
if (mContext.checkSelfPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS
+ " required");
}
mRemoteService.scheduleRequest(new PendingRevokeRuntimePermissionRequest(mRemoteService,
request, doDryRun, reason, mContext.getPackageName(), executor, callback));
}
/**
* Gets the runtime permissions for an app.
*
* @param packageName The package for which to query.
* @param callback Callback to receive the result.
* @param handler Handler on which to invoke the callback.
*
* @hide
*/
@RequiresPermission(Manifest.permission.GET_RUNTIME_PERMISSIONS)
public void getAppPermissions(@NonNull String packageName,
@@ -119,6 +210,8 @@ public final class PermissionControllerManager {
*
* @param packageName The package for which to revoke
* @param permissionName The permission to revoke
*
* @hide
*/
@RequiresPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS)
public void revokeRuntimePermission(@NonNull String packageName,
@@ -138,6 +231,8 @@ public final class PermissionControllerManager {
* @param countSystem Also count system apps
* @param callback Callback to receive the result
* @param handler Handler on which to invoke the callback
*
* @hide
*/
@RequiresPermission(Manifest.permission.GET_RUNTIME_PERMISSIONS)
public void countPermissionApps(@NonNull List<String> permissionNames,
@@ -205,6 +300,84 @@ public final class PermissionControllerManager {
}
}
/**
* Request for {@link #revokeRuntimePermissions}
*/
private static final class PendingRevokeRuntimePermissionRequest extends
AbstractRemoteService.PendingRequest<RemoteService, IPermissionController> {
private final @NonNull Map<String, List<String>> mRequest;
private final boolean mDoDryRun;
private final int mReason;
private final @NonNull String mCallingPackage;
private final @NonNull OnRevokeRuntimePermissionsCallback mCallback;
private final @NonNull RemoteCallback mRemoteCallback;
private PendingRevokeRuntimePermissionRequest(@NonNull RemoteService service,
@NonNull Map<String, List<String>> request, boolean doDryRun,
@Reason int reason, @NonNull String callingPackage,
@NonNull @CallbackExecutor Executor executor,
@NonNull OnRevokeRuntimePermissionsCallback callback) {
super(service);
mRequest = request;
mDoDryRun = doDryRun;
mReason = reason;
mCallingPackage = callingPackage;
mCallback = callback;
mRemoteCallback = new RemoteCallback(result -> executor.execute(() -> {
long token = Binder.clearCallingIdentity();
try {
Map<String, List<String>> revoked = new ArrayMap<>();
try {
Bundle bundleizedRevoked = result.getBundle(KEY_RESULT);
for (String packageName : bundleizedRevoked.keySet()) {
Preconditions.checkNotNull(packageName);
ArrayList<String> permissions =
bundleizedRevoked.getStringArrayList(packageName);
Preconditions.checkCollectionElementsNotNull(permissions,
"permissions");
revoked.put(packageName, permissions);
}
} catch (Exception e) {
Log.e(TAG, "Could not read result when revoking runtime permissions", e);
}
callback.onRevokeRuntimePermissions(revoked);
} finally {
Binder.restoreCallingIdentity(token);
finish();
}
}), null);
}
@Override
protected void onTimeout(RemoteService remoteService) {
mCallback.onRevokeRuntimePermissions(Collections.emptyMap());
}
@Override
public void run() {
Bundle bundledizedRequest = new Bundle();
for (Map.Entry<String, List<String>> appRequest : mRequest.entrySet()) {
bundledizedRequest.putStringArrayList(appRequest.getKey(),
new ArrayList<>(appRequest.getValue()));
}
try {
getService().getServiceInterface().revokeRuntimePermissions(bundledizedRequest,
mDoDryRun, mReason, mCallingPackage, mRemoteCallback);
} catch (RemoteException e) {
Log.e(TAG, "Error revoking runtime permission", e);
}
}
}
/**
* Request for {@link #getAppPermissions}
*/

View File

@@ -16,6 +16,7 @@
package android.permission;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkCollectionElementsNotNull;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -26,12 +27,19 @@ import android.annotation.SystemApi;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteCallback;
import android.util.ArrayMap;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* This service is meant to be implemented by the app controlling permissions.
@@ -59,6 +67,20 @@ public abstract class PermissionControllerService extends Service {
mHandler = new Handler(base.getMainLooper());
}
/**
* Revoke a set of runtime permissions for various apps.
*
* @param requests The permissions to revoke as {@code Map<packageName, List<permission>>}
* @param doDryRun Compute the permissions that would be revoked, but not actually revoke them
* @param reason Why the permission should be revoked
* @param callerPackageName The package name of the calling app
*
* @return the actually removed permissions as {@code Map<packageName, List<permission>>}
*/
public abstract @NonNull Map<String, List<String>> onRevokeRuntimePermissions(
@NonNull Map<String, List<String>> requests, boolean doDryRun,
@PermissionControllerManager.Reason int reason, @NonNull String callerPackageName);
/**
* Gets the runtime permissions for an app.
*
@@ -93,6 +115,41 @@ public abstract class PermissionControllerService extends Service {
@Override
public final IBinder onBind(Intent intent) {
return new IPermissionController.Stub() {
@Override
public void revokeRuntimePermissions(
Bundle bundleizedRequest, boolean doDryRun, int reason,
String callerPackageName, RemoteCallback callback) {
checkNotNull(bundleizedRequest, "bundleizedRequest");
checkNotNull(callerPackageName);
checkNotNull(callback);
Map<String, List<String>> request = new ArrayMap<>();
for (String packageName : bundleizedRequest.keySet()) {
Preconditions.checkNotNull(packageName);
ArrayList<String> permissions =
bundleizedRequest.getStringArrayList(packageName);
Preconditions.checkCollectionElementsNotNull(permissions, "permissions");
request.put(packageName, permissions);
}
enforceCallingPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, null);
// Verify callerPackageName
try {
PackageInfo pkgInfo = getPackageManager().getPackageInfo(callerPackageName, 0);
checkArgument(getCallingUid() == pkgInfo.applicationInfo.uid);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
mHandler.sendMessage(obtainMessage(
PermissionControllerService::revokeRuntimePermissions,
PermissionControllerService.this, request, doDryRun, reason,
callerPackageName, callback));
}
@Override
public void getAppPermissions(String packageName, RemoteCallback callback) {
checkNotNull(packageName, "packageName");
@@ -133,6 +190,27 @@ public abstract class PermissionControllerService extends Service {
};
}
private void revokeRuntimePermissions(@NonNull Map<String, List<String>> requests,
boolean doDryRun, @PermissionControllerManager.Reason int reason,
@NonNull String callerPackageName, @NonNull RemoteCallback callback) {
Map<String, List<String>> revoked = onRevokeRuntimePermissions(requests,
doDryRun, reason, callerPackageName);
checkNotNull(revoked);
Bundle bundledizedRevoked = new Bundle();
for (Map.Entry<String, List<String>> appRevocation : revoked.entrySet()) {
checkNotNull(appRevocation.getKey());
checkCollectionElementsNotNull(appRevocation.getValue(), "permissions");
bundledizedRevoked.putStringArrayList(appRevocation.getKey(),
new ArrayList<>(appRevocation.getValue()));
}
Bundle result = new Bundle();
result.putBundle(PermissionControllerManager.KEY_RESULT, bundledizedRevoked);
callback.sendResult(result);
}
private void getAppPermissions(@NonNull String packageName, @NonNull RemoteCallback callback) {
List<RuntimePermissionPresentationInfo> permissions = onGetAppPermissions(packageName);
if (permissions != null && !permissions.isEmpty()) {

View File

@@ -0,0 +1,12 @@
{
"presubmit": [
{
"name": "CtsPermissionTestCases",
"options": [
{
"include-filter": "android.permission.cts.PermissionControllerTest"
}
]
}
]
}