diff options
| author | Philip P. Moltmann <moltmann@google.com> | 2018-12-17 20:45:40 -0800 |
|---|---|---|
| committer | Philip P. Moltmann <moltmann@google.com> | 2019-01-06 17:51:33 -0800 |
| commit | 7868952db36a35b5266bb4da4e983cc47b9c5331 (patch) | |
| tree | 2cd1e09811f9251c8248c1583aa1d46839e40d95 /core/java/android | |
| parent | bbb539a172db58089d93332ec28790e3b2af6018 (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')
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 |
