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

Commit 6411a21a authored by Cassie Wang's avatar Cassie Wang Committed by Android (Google) Code Review
Browse files

Merge "Add a platform version of VisibilityStore."

parents 15f17656 2a966f89
Loading
Loading
Loading
Loading
+68 −103
Original line number Diff line number Diff line
/*
 * Copyright 2020 The Android Open Source Project
 * Copyright (C) 2021 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.
@@ -14,9 +14,13 @@
 * limitations under the License.
 */

// TODO(b/169883602): This is purposely a different package from the path so that it can access
// AppSearchImpl's methods without having to make them public. This should be moved into a proper
// package once AppSearchImpl-VisibilityStore's dependencies are refactored.
package com.android.server.appsearch.external.localstorage;

import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
@@ -25,10 +29,10 @@ import android.app.appsearch.exceptions.AppSearchException;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Process;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;

import androidx.annotation.RequiresApi;
import android.util.Log;

import com.android.internal.util.Preconditions;

@@ -56,8 +60,16 @@ import java.util.Set;
 *
 * <p>NOTE: This class holds an instance of AppSearchImpl and AppSearchImpl holds an instance of
 * this class. Take care to not cause any circular dependencies.
 *
 * @hide
 */
class VisibilityStore {
public class VisibilityStore {

    private static final String TAG = "AppSearchVisibilityStore";

    /** No-op user id that won't have any visibility settings. */
    public static final int NO_OP_USER_ID = -1;

    /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
    private static final String VISIBILITY_TYPE = "VisibilityType";

@@ -124,8 +136,8 @@ class VisibilityStore {
                    .build();

    /**
     * These cannot have any of the special characters used by AppSearchImpl (e.g. {@link
     * AppSearchImpl#PACKAGE_DELIMITER} or {@link AppSearchImpl#DATABASE_DELIMITER}.
     * These cannot have any of the special characters used by AppSearchImpl (e.g. {@code
     * AppSearchImpl#PACKAGE_DELIMITER} or {@code AppSearchImpl#DATABASE_DELIMITER}.
     */
    static final String PACKAGE_NAME = "VS#Pkg";

@@ -149,11 +161,15 @@ class VisibilityStore {

    private final AppSearchImpl mAppSearchImpl;

    // Context of the system service.
    private final Context mContext;

    // User ID of the caller who we're checking visibility settings for.
    private final int mUserId;

    // UID of the package that has platform-query privileges, i.e. can query for all
    // platform-surfaceable content.
    private int mGlobalQuerierPackageUid;
    private int mGlobalQuerierUid;

    /**
     * Maps prefixes to the set of schemas that are platform-hidden within that prefix. All schemas
@@ -180,20 +196,15 @@ class VisibilityStore {
     *
     * @param appSearchImpl AppSearchImpl instance
     */
    VisibilityStore(
    public VisibilityStore(
            @NonNull AppSearchImpl appSearchImpl,
            @NonNull Context context,
            @UserIdInt int userId,
            @NonNull String globalQuerierPackage) {
        mAppSearchImpl = appSearchImpl;
        mContext = context;
        mGlobalQuerierPackageUid = Process.INVALID_UID;

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            // This should always pass since we should only allow platform access on S+ (the first
            // version that AppSearch is offered on).
            mGlobalQuerierPackageUid =
                    Api24Impl.getGlobalQuerierPackageUid(context, globalQuerierPackage);
        }
        mUserId = userId;
        mGlobalQuerierUid = getGlobalQuerierUid(globalQuerierPackage);
    }

    /**
@@ -357,7 +368,9 @@ class VisibilityStore {
        Preconditions.checkNotNull(prefix);
        Preconditions.checkNotNull(prefixedSchema);

        if (callerUid == mGlobalQuerierPackageUid
        // We compare appIds here rather than direct uids because the package's uid may change based
        // on the user that's running.
        if (UserHandle.isSameApp(mGlobalQuerierUid, callerUid)
                && isSchemaPlatformSurfaceable(prefix, prefixedSchema)) {
            return true;
        }
@@ -414,26 +427,21 @@ class VisibilityStore {
            return false;
        }

        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) {
            // PackageManager.hasSigningCertificate is only available on P+
            // This should never fail since we should only allow package access on S+ (the first
            // version that AppSearch is offered on). But just in case, default to no package
            // access.
            return false;
        }

        for (PackageIdentifier packageIdentifier : packageIdentifiers) {
            // Check that the caller uid matches this allowlisted PackageIdentifier.
            if (Api24Impl.getPackageUid(mContext, packageIdentifier.getPackageName())
                    != callerUid) {
            // TODO(b/169883602): Consider caching the UIDs of packages. Looking this up in the
            // package manager could be costly. We would also need to update the cache on
            // package-removals.
            if (getPackageUidAsUser(packageIdentifier.getPackageName()) != callerUid) {
                continue;
            }

            // Check that the package also has the matching certificate
            if (Api28Impl.hasSigningCertificate(
                    mContext,
            if (mContext.getPackageManager()
                    .hasSigningCertificate(
                            packageIdentifier.getPackageName(),
                    packageIdentifier.getSha256Certificate())) {
                            packageIdentifier.getSha256Certificate(),
                            PackageManager.CERT_INPUT_SHA256)) {
                // The caller has the right package name and right certificate!
                return true;
            }
@@ -448,7 +456,7 @@ class VisibilityStore {
     *
     * <p>{@link #initialize()} must be called after this.
     */
    void handleReset() {
    public void handleReset() {
        mNotPlatformSurfaceableMap.clear();
        mPackageAccessibleMap.clear();
    }
@@ -464,83 +472,40 @@ class VisibilityStore {
    }

    /**
     * Wrapper class around API 24 methods.
     *
     * <p>Even though wrapping a call to a method from an API above minSdk inside an SDK_INT check
     * makes it runtime safe, it is not optimal. When ART tries to optimize a class, it will do so
     * regardless of the execution path, and will fail if it tries to resolve a method at a higher
     * API if that method is being referenced somewhere in the class, even if that method would
     * never be called at runtime due to the SDK_INT check. ART will however only try to optimize a
     * class the first time it's referenced at runtime, this means if we wrap our above minSdk
     * method calls inside classes that are only referenced at runtime at the appropriate API level,
     * then we guarantee the ability to resolve all the methods.
     */
    @RequiresApi(24)
    private static class Api24Impl {
        private Api24Impl() {}

        /**
         * Finds the UID of the {@code globalQuerierPackage}. {@code globalQuerierPackage} must be a
     * Finds the uid of the {@code globalQuerierPackage}. {@code globalQuerierPackage} must be a
     * pre-installed, system app. Returns {@link Process#INVALID_UID} if unable to find the UID.
     */
        static int getGlobalQuerierPackageUid(
                @NonNull Context context, @NonNull String globalQuerierPackage) {
    private int getGlobalQuerierUid(@NonNull String globalQuerierPackage) {
        try {
                // TODO(b/169883602): In framework, this should be UserHandle.isSameApp or
                //  packageManager.getPackageUidAsUser().
            int flags =
                    PackageManager.MATCH_DISABLED_COMPONENTS
                            | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
                            | PackageManager.MATCH_SYSTEM_ONLY;
                return context.getPackageManager().getPackageUid(globalQuerierPackage, flags);
            // It doesn't matter that we're using the caller's userId here. We'll eventually check
            // that the two uids in question belong to the same appId.
            return mContext.getPackageManager()
                    .getPackageUidAsUser(globalQuerierPackage, flags, mUserId);
        } catch (PackageManager.NameNotFoundException e) {
            // Global querier doesn't exist.
            Log.i(
                    TAG,
                    "AppSearch global querier package not found on device:  '"
                            + globalQuerierPackage
                            + "'");
        }
        return Process.INVALID_UID;
    }

    /**
         * Finds the UID of the {@code packageName}. Returns {@link Process#INVALID_UID} if unable
         * to find the UID.
     * Finds the UID of the {@code packageName}. Returns {@link Process#INVALID_UID} if unable to
     * find the UID.
     */
        static int getPackageUid(@NonNull Context context, @NonNull String packageName) {
    private int getPackageUidAsUser(@NonNull String packageName) {
        try {
                // TODO(b/169883602): In framework, this should be UserHandle.isSameApp or
                //  packageManager.getPackageUidAsUser().
                return context.getPackageManager().getPackageUid(packageName, /*flags=*/ 0);
            return mContext.getPackageManager().getPackageUidAsUser(packageName, mUserId);
        } catch (PackageManager.NameNotFoundException e) {
                // Global querier doesn't exist.
            // Package doesn't exist, continue
        }
        return Process.INVALID_UID;
    }
}

    /**
     * Wrapper class around API 28 methods.
     *
     * <p>Even though wrapping a call to a method from an API above minSdk inside an SDK_INT check
     * makes it runtime safe, it is not optimal. When ART tries to optimize a class, it will do so
     * regardless of the execution path, and will fail if it tries to resolve a method at a higher
     * API if that method is being referenced somewhere in the class, even if that method would
     * never be called at runtime due to the SDK_INT check. ART will however only try to optimize a
     * class the first time it's referenced at runtime, this means if we wrap our above minSdk
     * method calls inside classes that are only referenced at runtime at the appropriate API level,
     * then we guarantee the ability to resolve all the methods.
     */
    @RequiresApi(28)
    private static class Api28Impl {
        private Api28Impl() {}

        /**
         * Returns whether the {@code packageName} has been signed with {@code sha256Certificate}.
         */
        static boolean hasSigningCertificate(
                @NonNull Context context,
                @NonNull String packageName,
                @NonNull byte[] sha256Certificate) {
            return context.getPackageManager()
                    .hasSigningCertificate(
                            packageName, sha256Certificate, PackageManager.CERT_INPUT_SHA256);
        }
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -192,7 +192,8 @@ public final class AppSearchImpl {
            mIcingSearchEngineLocked = new IcingSearchEngine(options);

            mVisibilityStoreLocked =
                    new VisibilityStore(this, context, globalQuerierPackage);
                    new VisibilityStore(
                            this, context, userId, globalQuerierPackage);

            InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize();
            SchemaProto schemaProto;
+426 −0

File added.

Preview size limit exceeded, changes collapsed.

+88 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.
 */

// TODO(b/169883602): This is purposely a different package from the path so that AppSearchImplTest
// can use it without an extra import. This should be moved into a proper package once
// AppSearchImpl-VisibilityStore's dependencies are refactored.
package com.android.server.appsearch.external.localstorage;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.pm.PackageManager;

import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/** Mock to help test package name, UID, and certificate verification. */
public class MockPackageManager {

    @Mock private PackageManager mMockPackageManager;

    public MockPackageManager() {
        MockitoAnnotations.initMocks(this);
    }

    @NonNull
    public PackageManager getMockPackageManager() {
        return mMockPackageManager;
    }

    /** Mock a NameNotFoundException if the package name isn't installed. */
    public void mockThrowsNameNotFoundException(String packageName) {
        try {
            when(mMockPackageManager.getPackageUidAsUser(eq(packageName), /*userId=*/ anyInt()))
                    .thenThrow(new PackageManager.NameNotFoundException());
            when(mMockPackageManager.getPackageUidAsUser(
                            eq(packageName), /*flags=*/ anyInt(), /*userId=*/ anyInt()))
                    .thenThrow(new PackageManager.NameNotFoundException());
        } catch (PackageManager.NameNotFoundException e) {
            // Shouldn't ever happen since we're mocking the exception
            e.printStackTrace();
        }
    }

    /** Mocks that {@code uid} contains the {@code packageName} */
    public void mockGetPackageUidAsUser(String packageName, @UserIdInt int callerUserId, int uid) {
        try {
            when(mMockPackageManager.getPackageUidAsUser(eq(packageName), eq(callerUserId)))
                    .thenReturn(uid);
            when(mMockPackageManager.getPackageUidAsUser(
                            eq(packageName), /*flags=*/ anyInt(), eq(callerUserId)))
                    .thenReturn(uid);
        } catch (PackageManager.NameNotFoundException e) {
            // Shouldn't ever happen since we're mocking the method.
            e.printStackTrace();
        }
    }

    /** Mocks that {@code packageName} has been signed with {@code sha256Cert}. */
    public void mockAddSigningCertificate(String packageName, byte[] sha256Cert) {
        when(mMockPackageManager.hasSigningCertificate(
                        packageName, sha256Cert, PackageManager.CERT_INPUT_SHA256))
                .thenReturn(true);
    }

    /** Mocks that {@code packageName} has NOT been signed with {@code sha256Cert}. */
    public void mockRemoveSigningCertificate(String packageName, byte[] sha256Cert) {
        when(mMockPackageManager.hasSigningCertificate(
                        packageName, sha256Cert, PackageManager.CERT_INPUT_SHA256))
                .thenReturn(false);
    }
}
+324 −0

File added.

Preview size limit exceeded, changes collapsed.