From 7868952db36a35b5266bb4da4e983cc47b9c5331 Mon Sep 17 00:00:00 2001 From: "Philip P. Moltmann" Date: Mon, 17 Dec 2018 20:45:40 -0800 Subject: [PATCH] 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 --- api/system-current.txt | 12 ++ api/test-current.txt | 15 ++ .../permission/IPermissionController.aidl | 3 + .../PermissionControllerManager.java | 179 +++++++++++++++++- .../PermissionControllerService.java | 78 ++++++++ core/java/android/permission/TEST_MAPPING | 12 ++ 6 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 core/java/android/permission/TEST_MAPPING diff --git a/api/system-current.txt b/api/system-current.txt index 10d483d1bfcec..5ee66f55200df 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4609,6 +4609,17 @@ package android.os.storage { package android.permission { + public final class PermissionControllerManager { + method public void revokeRuntimePermissions(java.util.Map>, 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>); + } + 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, boolean, boolean); method public abstract java.util.List onGetAppPermissions(java.lang.String); method public abstract void onRevokeRuntimePermission(java.lang.String, java.lang.String); + method public abstract java.util.Map> onRevokeRuntimePermissions(java.util.Map>, boolean, int, java.lang.String); field public static final java.lang.String SERVICE_INTERFACE = "android.permission.PermissionControllerService"; } diff --git a/api/test-current.txt b/api/test-current.txt index 1401cbb4211e3..de643501d16c4 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -985,6 +985,21 @@ package android.os.strictmode { } +package android.permission { + + public final class PermissionControllerManager { + method public void revokeRuntimePermissions(java.util.Map>, 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>); + } + +} + package android.print { public final class PrintJobInfo implements android.os.Parcelable { diff --git a/core/java/android/permission/IPermissionController.aidl b/core/java/android/permission/IPermissionController.aidl index 38951d5466c76..0e18b445fd015 100644 --- a/core/java/android/permission/IPermissionController.aidl +++ b/core/java/android/permission/IPermissionController.aidl @@ -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 permissionNames, boolean countOnlyGranted, diff --git a/core/java/android/permission/PermissionControllerManager.java b/core/java/android/permission/PermissionControllerManager.java index 66e8666a8a705..e21a6608bee05 100644 --- a/core/java/android/permission/PermissionControllerManager.java +++ b/core/java/android/permission/PermissionControllerManager.java @@ -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. + * + *

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>} + */ + public abstract void onRevokeRuntimePermissions(@NonNull Map> 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>} + * @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> 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> 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 permissionNames, @@ -205,6 +300,84 @@ public final class PermissionControllerManager { } } + /** + * Request for {@link #revokeRuntimePermissions} + */ + private static final class PendingRevokeRuntimePermissionRequest extends + AbstractRemoteService.PendingRequest { + private final @NonNull Map> 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> 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> revoked = new ArrayMap<>(); + try { + Bundle bundleizedRevoked = result.getBundle(KEY_RESULT); + + for (String packageName : bundleizedRevoked.keySet()) { + Preconditions.checkNotNull(packageName); + + ArrayList 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> 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} */ diff --git a/core/java/android/permission/PermissionControllerService.java b/core/java/android/permission/PermissionControllerService.java index 5dad07178e53d..f621737e5ed40 100644 --- a/core/java/android/permission/PermissionControllerService.java +++ b/core/java/android/permission/PermissionControllerService.java @@ -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>} + * @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>} + */ + public abstract @NonNull Map> onRevokeRuntimePermissions( + @NonNull Map> 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> request = new ArrayMap<>(); + for (String packageName : bundleizedRequest.keySet()) { + Preconditions.checkNotNull(packageName); + + ArrayList 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> requests, + boolean doDryRun, @PermissionControllerManager.Reason int reason, + @NonNull String callerPackageName, @NonNull RemoteCallback callback) { + Map> revoked = onRevokeRuntimePermissions(requests, + doDryRun, reason, callerPackageName); + + checkNotNull(revoked); + Bundle bundledizedRevoked = new Bundle(); + for (Map.Entry> 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 permissions = onGetAppPermissions(packageName); if (permissions != null && !permissions.isEmpty()) { diff --git a/core/java/android/permission/TEST_MAPPING b/core/java/android/permission/TEST_MAPPING new file mode 100644 index 0000000000000..ba9f36a31f2e9 --- /dev/null +++ b/core/java/android/permission/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "CtsPermissionTestCases", + "options": [ + { + "include-filter": "android.permission.cts.PermissionControllerTest" + } + ] + } + ] +} \ No newline at end of file