summaryrefslogtreecommitdiff
path: root/core/java/android
diff options
context:
space:
mode:
authorPhilip P. Moltmann <moltmann@google.com>2018-12-17 20:45:40 -0800
committerPhilip P. Moltmann <moltmann@google.com>2019-01-06 17:51:33 -0800
commit7868952db36a35b5266bb4da4e983cc47b9c5331 (patch)
tree2cd1e09811f9251c8248c1583aa1d46839e40d95 /core/java/android
parentbbb539a172db58089d93332ec28790e3b2af6018 (diff)
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
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/permission/IPermissionController.aidl3
-rw-r--r--core/java/android/permission/PermissionControllerManager.java179
-rw-r--r--core/java/android/permission/PermissionControllerService.java78
-rw-r--r--core/java/android/permission/TEST_MAPPING12
4 files changed, 269 insertions, 3 deletions
diff --git a/core/java/android/permission/IPermissionController.aidl b/core/java/android/permission/IPermissionController.aidl
index 38951d5466c7..0e18b445fd01 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<String> permissionNames, boolean countOnlyGranted,
diff --git a/core/java/android/permission/PermissionControllerManager.java b/core/java/android/permission/PermissionControllerManager.java
index 66e8666a8a70..e21a6608bee0 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.
+ *
+ * <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,
@@ -206,6 +301,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}
*/
private static final class PendingGetAppPermissionRequest extends
diff --git a/core/java/android/permission/PermissionControllerService.java b/core/java/android/permission/PermissionControllerService.java
index 5dad07178e53..f621737e5ed4 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.
@@ -60,6 +68,20 @@ public abstract class PermissionControllerService extends Service {
}
/**
+ * 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.
*
* @param packageName The package for which to query.
@@ -94,6 +116,41 @@ public abstract class PermissionControllerService extends Service {
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");
checkNotNull(callback, "callback");
@@ -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()) {
diff --git a/core/java/android/permission/TEST_MAPPING b/core/java/android/permission/TEST_MAPPING
new file mode 100644
index 000000000000..ba9f36a31f2e
--- /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