Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 981cde18 authored by Nikita Dubrovsky's avatar Nikita Dubrovsky
Browse files

When autofilling content, grant URI permissions to the target app

Bug: 168341541
Test: atest CtsAutoFillServiceTestCases:DatasetTest
Test: Manually tested image suggestions when the target app doesn't have media store permissions
Change-Id: I42546c76e45ec404e8d8de3bf63ca094d8a494c6
parent 2b5f2d80
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -406,6 +406,8 @@ public final class Dataset implements Parcelable {
         * authentication.
         *
         * @throws IllegalStateException if {@link #build()} was already called.
         * @throws IllegalArgumentException if the provided content
         * {@link ClipData.Item#getIntent() contains an intent}
         *
         * @return this builder.
         *
@@ -416,6 +418,12 @@ public final class Dataset implements Parcelable {
        @SuppressLint("MissingGetterMatchingBuilder")
        public @NonNull Builder setContent(@NonNull AutofillId id, @Nullable ClipData content) {
            throwIfDestroyed();
            if (content != null) {
                for (int i = 0; i < content.getItemCount(); i++) {
                    Preconditions.checkArgument(content.getItemAt(i).getIntent() == null,
                            "Content items cannot contain an Intent: content=" + content);
                }
            }
            setLifeTheUniverseAndEverything(id, null, null, null, null);
            mFieldContent = content;
            return this;
+8 −1
Original line number Diff line number Diff line
@@ -1246,8 +1246,10 @@ final class AutofillManagerServiceImpl
                            mRemoteAugmentedAutofillService = null;
                        }
                    };
            final int serviceUid = mRemoteAugmentedAutofillServiceInfo.applicationInfo.uid;
            mRemoteAugmentedAutofillService = new RemoteAugmentedAutofillService(getContext(),
                    componentName, mUserId, callbacks, mMaster.isInstantServiceAllowed(),
                    serviceUid, componentName,
                    mUserId, callbacks, mMaster.isInstantServiceAllowed(),
                    mMaster.verbose, mMaster.mAugmentedServiceIdleUnbindTimeoutMs,
                    mMaster.mAugmentedServiceRequestTimeoutMs);
        }
@@ -1255,6 +1257,11 @@ final class AutofillManagerServiceImpl
        return mRemoteAugmentedAutofillService;
    }

    @GuardedBy("mLock")
    @Nullable RemoteAugmentedAutofillService getRemoteAugmentedAutofillServiceIfCreatedLocked() {
        return mRemoteAugmentedAutofillService;
    }

    /**
     * Called when the {@link AutofillManagerService#mAugmentedAutofillResolver}
     * changed (among other places).
+257 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.autofill;

import static android.content.ContentResolver.SCHEME_CONTENT;

import static com.android.server.autofill.Helper.sVerbose;

import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.IUriGrantsManager;
import android.app.UriGrantsManager;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.uri.UriGrantsManagerInternal;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * Grants and revokes URI permissions for content-based autofill suggestions.
 *
 * <p>Note that the system cannot just hand out grants directly; it must always do so on behalf of
 * an owner (see {@link com.android.server.uri.UriGrantsManagerService}). For autofill, the owner
 * is the autofill service provider that creates a given autofill suggestion containing a content
 * URI. Therefore, this manager class must be instantiated with the service uid of the provider for
 * which it will manage URI grants.
 *
 * <p>To dump the state of this class, use {@code adb shell dumpsys autofill}.
 *
 * <p>To dump all active URI permissions, use {@code adb shell dumpsys activity permissions}.
 */
final class AutofillUriGrantsManager {
    private static final String TAG = AutofillUriGrantsManager.class.getSimpleName();

    private final int mSourceUid;
    @UserIdInt
    private final int mSourceUserId;
    @NonNull
    private final IBinder mPermissionOwner;
    @NonNull
    private final UriGrantsManagerInternal mUgmInternal;
    @NonNull
    private final IUriGrantsManager mUgm;

    // We use a local lock here for simplicity, since the synchronized code does not depend on
    // any other resources (the "hold and wait" condition required for deadlock is not present).
    // If this changes in the future, instead of using a local lock this should be updated to
    // use the shared lock from AutofillManagerServiceImpl.
    @NonNull
    private final Object mLock;

    // Tracks the URIs that have been granted to each package. For each URI, the map stores the
    // activities that triggered the grant. This allows revoking permissions only once all
    // activities that triggered the grant are finished.
    @NonNull
    @GuardedBy("mLock")
    private final ArrayMap<String, List<Pair<Uri, String>>> mActiveGrantsByPackage;

    /**
     * Creates a new instance of the manager.
     *
     * @param serviceUid The uid of the autofill service provider for which this manager is being
     * created. URI grants will be requested on behalf of this uid (ie, this uid will be passed as
     * the {@code fromUid} to {@link IUriGrantsManager#grantUriPermissionFromOwner}).
     */
    AutofillUriGrantsManager(int serviceUid) {
        mSourceUid = serviceUid;
        mSourceUserId = UserHandle.getUserId(mSourceUid);
        mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
        mPermissionOwner = mUgmInternal.newUriPermissionOwner("autofill-" + serviceUid);
        mUgm = UriGrantsManager.getService();
        mLock = new Object();
        mActiveGrantsByPackage = new ArrayMap<>(0);
    }

    public void grantUriPermissions(@NonNull ComponentName targetActivity,
            @UserIdInt int targetUserId, @NonNull ClipData clip) {
        String targetPkg = targetActivity.getPackageName();
        for (int i = 0; i < clip.getItemCount(); i++) {
            ClipData.Item item = clip.getItemAt(i);
            Uri uri = item.getUri();
            if (uri == null || !SCHEME_CONTENT.equals(uri.getScheme())) {
                continue;
            }
            if (grantUriPermissions(targetPkg, targetUserId, uri)) {
                addToActiveGrants(uri, targetActivity);
            }
        }
    }

    public void revokeUriPermissions(@NonNull ComponentName targetActivity,
            @UserIdInt int targetUserId) {
        String targetPkg = targetActivity.getPackageName();
        Set<Uri> urisWhoseGrantsShouldBeRevoked = removeFromActiveGrants(targetActivity);
        for (Uri uri : urisWhoseGrantsShouldBeRevoked) {
            revokeUriPermissions(targetPkg, targetUserId, uri);
        }
    }

    private boolean grantUriPermissions(@NonNull String targetPkg, @UserIdInt int targetUserId,
            @NonNull Uri uri) {
        final int sourceUserId = ContentProvider.getUserIdFromUri(uri, mSourceUserId);
        if (sVerbose) {
            Slog.v(TAG, "Granting URI permissions: uri=" + uri
                    + ", sourceUid=" + mSourceUid + ", sourceUserId=" + sourceUserId
                    + ", targetPkg=" + targetPkg + ", targetUserId=" + targetUserId);
        }
        final Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(uri);
        final long ident = Binder.clearCallingIdentity();
        try {
            mUgm.grantUriPermissionFromOwner(
                    mPermissionOwner,
                    mSourceUid,
                    targetPkg,
                    uriWithoutUserId,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
                    sourceUserId,
                    targetUserId);
            return true;
        } catch (RemoteException e) {
            Slog.e(TAG, "Granting URI permissions failed: uri=" + uri
                    + ", sourceUid=" + mSourceUid + ", sourceUserId=" + sourceUserId
                    + ", targetPkg=" + targetPkg + ", targetUserId=" + targetUserId, e);
            return false;
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    private void revokeUriPermissions(@NonNull String targetPkg, @UserIdInt int targetUserId,
            @NonNull Uri uri) {
        final int sourceUserId = ContentProvider.getUserIdFromUri(uri, mSourceUserId);
        if (sVerbose) {
            Slog.v(TAG, "Revoking URI permissions: uri=" + uri
                    + ", sourceUid=" + mSourceUid + ", sourceUserId=" + sourceUserId
                    + ", target=" + targetPkg + ", targetUserId=" + targetUserId);
        }
        final Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(uri);
        final long ident = Binder.clearCallingIdentity();
        try {
            mUgmInternal.revokeUriPermissionFromOwner(
                    mPermissionOwner,
                    uriWithoutUserId,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
                    sourceUserId,
                    targetPkg,
                    targetUserId);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    private void addToActiveGrants(@NonNull Uri uri, @NonNull ComponentName targetActivity) {
        synchronized (mLock) {
            String packageName = targetActivity.getPackageName();
            List<Pair<Uri, String>> uris = mActiveGrantsByPackage.computeIfAbsent(packageName,
                    k -> new ArrayList<>(1));
            uris.add(Pair.create(uri, targetActivity.getClassName()));
        }
    }

    private Set<Uri> removeFromActiveGrants(@NonNull ComponentName targetActivity) {
        synchronized (mLock) {
            String targetPackageName = targetActivity.getPackageName();
            List<Pair<Uri, String>> uris = mActiveGrantsByPackage.get(targetPackageName);
            if (uris == null || uris.isEmpty()) {
                return Collections.emptySet();
            }

            // Collect all URIs whose grant was triggered by the target activity.
            String targetActivityClassName = targetActivity.getClassName();
            Set<Uri> urisWhoseGrantsShouldBeRevoked = new ArraySet<>(1);
            for (Iterator<Pair<Uri, String>> iter = uris.iterator(); iter.hasNext(); ) {
                Pair<Uri, String> uriAndActivity = iter.next();
                if (uriAndActivity.second.equals(targetActivityClassName)) {
                    urisWhoseGrantsShouldBeRevoked.add(uriAndActivity.first);
                    iter.remove();
                }
            }

            // A URI grant may have been triggered by more than one activity for the same package.
            // We should not revoke a grant if it was triggered by multiple activities and one or
            // more of those activities is still alive. Therefore we do a second pass and prune
            // the set of URIs to be revoked if an additional activity that triggered its grant
            // is still present.
            for (Pair<Uri, String> uriAndActivity : uris) {
                urisWhoseGrantsShouldBeRevoked.remove(uriAndActivity.first);
            }

            // If there are no remaining URIs granted to the package, drop the entry from the map.
            if (uris.isEmpty()) {
                mActiveGrantsByPackage.remove(targetPackageName);
            }
            return urisWhoseGrantsShouldBeRevoked;
        }
    }

    /**
     * Dump the active URI grants.
     */
    public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
        synchronized (mLock) {
            if (mActiveGrantsByPackage.isEmpty()) {
                pw.print(prefix); pw.println("URI grants: none");
                return;
            }
            pw.print(prefix); pw.println("URI grants:");
            final String prefix2 = prefix + "  ";
            final String prefix3 = prefix2 + "  ";
            for (int i = mActiveGrantsByPackage.size() - 1; i >= 0; i--) {
                String packageName = mActiveGrantsByPackage.keyAt(i);
                pw.print(prefix2); pw.println(packageName);
                List<Pair<Uri, String>> uris = mActiveGrantsByPackage.valueAt(i);
                if (uris == null || uris.isEmpty())  {
                    continue;
                }
                for (Pair<Uri, String> uriAndActivity : uris) {
                    pw.print(prefix3);
                    pw.println(uriAndActivity.first + ": " + uriAndActivity.second);
                }
            }
        }
    }
}
+20 −4
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.IResultReceiver;
import com.android.server.autofill.ui.InlineFillUi;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
@@ -74,8 +75,9 @@ final class RemoteAugmentedAutofillService
    private final int mRequestTimeoutMs;
    private final ComponentName mComponentName;
    private final RemoteAugmentedAutofillServiceCallbacks mCallbacks;
    private final AutofillUriGrantsManager mUriGrantsManager;

    RemoteAugmentedAutofillService(Context context, ComponentName serviceName,
    RemoteAugmentedAutofillService(Context context, int serviceUid, ComponentName serviceName,
            int userId, RemoteAugmentedAutofillServiceCallbacks callbacks,
            boolean bindInstantServiceAllowed, boolean verbose, int idleUnbindTimeoutMs,
            int requestTimeoutMs) {
@@ -87,6 +89,7 @@ final class RemoteAugmentedAutofillService
        mRequestTimeoutMs = requestTimeoutMs;
        mComponentName = serviceName;
        mCallbacks = callbacks;
        mUriGrantsManager = new AutofillUriGrantsManager(serviceUid);

        // Bind right away.
        connect();
@@ -121,6 +124,10 @@ final class RemoteAugmentedAutofillService
        return mComponentName;
    }

    public AutofillUriGrantsManager getAutofillUriGrantsManager() {
        return mUriGrantsManager;
    }

    @Override // from ServiceConnector.Impl
    protected void onServiceConnectionStatusChanged(
            IAugmentedAutofillService service, boolean connected) {
@@ -173,8 +180,8 @@ final class RemoteAugmentedAutofillService
                                    maybeRequestShowInlineSuggestions(sessionId,
                                            inlineSuggestionsRequest, inlineSuggestionsData,
                                            clientState, focusedId, focusedValue,
                                            inlineSuggestionsCallback,
                                            client, onErrorCallback, remoteRenderService, userId);
                                            inlineSuggestionsCallback, client, onErrorCallback,
                                            remoteRenderService, userId, activityComponent);
                                    if (!showingFillWindow) {
                                        requestAutofill.complete(null);
                                    }
@@ -245,7 +252,8 @@ final class RemoteAugmentedAutofillService
            @Nullable Function<InlineFillUi, Boolean> inlineSuggestionsCallback,
            @NonNull IAutoFillManagerClient client, @NonNull Runnable onErrorCallback,
            @Nullable RemoteInlineSuggestionRenderService remoteRenderService,
            int userId) {
            int userId,
            @NonNull ComponentName targetActivity) {
        if (inlineSuggestionsData == null || inlineSuggestionsData.isEmpty()
                || inlineSuggestionsCallback == null || request == null
                || remoteRenderService == null) {
@@ -299,6 +307,8 @@ final class RemoteAugmentedAutofillService
                                    final ArrayList<AutofillId> fieldIds = dataset.getFieldIds();
                                    final ClipData content = dataset.getFieldContent();
                                    if (content != null) {
                                        mUriGrantsManager.grantUriPermissions(
                                                targetActivity, userId, content);
                                        final AutofillId fieldId = fieldIds.get(0);
                                        if (sDebug) {
                                            Slog.d(TAG, "Calling client autofillContent(): "
@@ -358,6 +368,12 @@ final class RemoteAugmentedAutofillService
                + ComponentName.flattenToShortString(mComponentName) + "]";
    }

    @Override
    public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
        super.dump(prefix, pw);
        mUriGrantsManager.dump(prefix, pw);
    }

    /**
     * Called by {@link Session} when it's time to destroy all augmented autofill requests.
     */
+27 −0
Original line number Diff line number Diff line
@@ -1555,6 +1555,19 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
            return;
        }

        // Get a handle to the RemoteAugmentedAutofillService. In
        // AutofillManagerServiceImpl.updateRemoteAugmentedAutofillService() we invalidate sessions
        // whenever the service changes, so there should never be a case when we get here and the
        // remote service instance is not present or different.
        final RemoteAugmentedAutofillService remoteAugmentedAutofillService =
                mService.getRemoteAugmentedAutofillServiceIfCreatedLocked();
        if (remoteAugmentedAutofillService == null) {
            Slog.e(TAG, "Can't fill after auth: RemoteAugmentedAutofillService is null");
            mService.resetLastAugmentedAutofillResponse();
            removeFromServiceLocked();
            return;
        }

        // Update state to ensure that after filling the field here we don't end up firing another
        // autofill request that will end up showing the same suggestions to the user again. When
        // the auth activity came up, the field for which the suggestions were shown lost focus and
@@ -1567,6 +1580,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
        final Bundle clientState = data.getBundle(AutofillManager.EXTRA_CLIENT_STATE);
        mService.logAugmentedAutofillSelected(id, dataset.getId(), clientState);

        // For any content URIs, grant URI permissions to the target app before filling.
        if (content != null) {
            final AutofillUriGrantsManager autofillUgm =
                    remoteAugmentedAutofillService.getAutofillUriGrantsManager();
            autofillUgm.grantUriPermissions(mComponentName, userId, content);
        }

        // Fill the value into the field.
        if (sDebug) {
            Slog.d(TAG, "Filling after auth: fieldId=" + fieldId + ", value=" + value
@@ -3987,6 +4007,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
        if (remoteRenderService != null) {
            remoteRenderService.destroySuggestionViews(userId, id);
        }
        final RemoteAugmentedAutofillService remoteAugmentedAutofillService =
                mService.getRemoteAugmentedAutofillServiceIfCreatedLocked();
        if (remoteAugmentedAutofillService != null) {
            final AutofillUriGrantsManager autofillUgm =
                    remoteAugmentedAutofillService.getAutofillUriGrantsManager();
            autofillUgm.revokeUriPermissions(mComponentName, userId);
        }

        mDestroyed = true;