diff --git a/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java b/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java index 24d64a04affda..64dc972d301c0 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java @@ -1,5 +1,5 @@ /* - * 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; * *

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, - packageIdentifier.getPackageName(), - packageIdentifier.getSha256Certificate())) { + if (mContext.getPackageManager() + .hasSigningCertificate( + packageIdentifier.getPackageName(), + packageIdentifier.getSha256Certificate(), + PackageManager.CERT_INPUT_SHA256)) { // The caller has the right package name and right certificate! return true; } @@ -448,7 +456,7 @@ class VisibilityStore { * *

{@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. - * - *

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. + * 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. */ - @RequiresApi(24) - private static class Api24Impl { - private Api24Impl() {} - - /** - * 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) { - 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); - } catch (PackageManager.NameNotFoundException e) { - // Global querier doesn't exist. - } - return Process.INVALID_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) { - try { - // TODO(b/169883602): In framework, this should be UserHandle.isSameApp or - // packageManager.getPackageUidAsUser(). - return context.getPackageManager().getPackageUid(packageName, /*flags=*/ 0); - } catch (PackageManager.NameNotFoundException e) { - // Global querier doesn't exist. - } - return Process.INVALID_UID; + private int getGlobalQuerierUid(@NonNull String globalQuerierPackage) { + try { + int flags = + PackageManager.MATCH_DISABLED_COMPONENTS + | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS + | PackageManager.MATCH_SYSTEM_ONLY; + // 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; } /** - * Wrapper class around API 28 methods. - * - *

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. + * Finds the UID of the {@code packageName}. Returns {@link Process#INVALID_UID} if unable to + * find the UID. */ - @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); + private int getPackageUidAsUser(@NonNull String packageName) { + try { + return mContext.getPackageManager().getPackageUidAsUser(packageName, mUserId); + } catch (PackageManager.NameNotFoundException e) { + // Package doesn't exist, continue } + return Process.INVALID_UID; } } diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java index b1d0ab422e58f..651ccd9fd3b83 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java @@ -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; diff --git a/services/tests/servicestests/src/com/android/server/appsearch/AppSearchImplPlatformTest.java b/services/tests/servicestests/src/com/android/server/appsearch/AppSearchImplPlatformTest.java new file mode 100644 index 0000000000000..b5f49124483eb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appsearch/AppSearchImplPlatformTest.java @@ -0,0 +1,426 @@ +/* + * 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 it can access +// AppSearchImpl's methods without having to make them public. This should be replaced by proper +// global query integration tests that can test AppSearchImpl-VisibilityStore integration logic. +package com.android.server.appsearch.external.localstorage; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.appsearch.AppSearchSchema; +import android.app.appsearch.GenericDocument; +import android.app.appsearch.PackageIdentifier; +import android.app.appsearch.SearchResultPage; +import android.app.appsearch.SearchSpec; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.PackageManager; + +import androidx.test.core.app.ApplicationProvider; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.util.Collections; +import java.util.List; + +/** This tests AppSearchImpl when it's running with a platform-backed VisibilityStore. */ +public class AppSearchImplPlatformTest { + @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + private MockPackageManager mMockPackageManager = new MockPackageManager(); + private Context mContext; + private AppSearchImpl mAppSearchImpl; + private int mGlobalQuerierUid; + + @Before + public void setUp() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + mContext = + new ContextWrapper(context) { + @Override + public PackageManager getPackageManager() { + return mMockPackageManager.getMockPackageManager(); + } + }; + + // Give ourselves global query permissions + mAppSearchImpl = + AppSearchImpl.create( + mTemporaryFolder.newFolder(), + mContext, + mContext.getUserId(), + mContext.getPackageName()); + mGlobalQuerierUid = + mContext.getPackageManager().getPackageUid(mContext.getPackageName(), /*flags=*/ 0); + } + /** + * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term + * test until we have official support for multiple-apps indexing at once. + */ + @Test + public void testGlobalQueryWithMultiplePackages_noPackageFilters() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package2 schema + List schema2 = + ImmutableList.of(new AppSearchSchema.Builder("schema2").build()); + mAppSearchImpl.setSchema( + "package2", + "database2", + schema2, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package1 document + GenericDocument document1 = + new GenericDocument.Builder<>("uri", "schema1").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package1", "database1", document1); + + // Insert package2 document + GenericDocument document2 = + new GenericDocument.Builder<>("uri", "schema2").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package2", "database2", document2); + + // No query filters specified, global query can retrieve all documents. + SearchSpec searchSpec = + new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build(); + SearchResultPage searchResultPage = + mAppSearchImpl.globalQuery( + "", searchSpec, mContext.getPackageName(), mGlobalQuerierUid); + assertThat(searchResultPage.getResults()).hasSize(2); + + // Document2 will be first since it got indexed later and has a "better", aka more recent + // score. + assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document2); + assertThat(searchResultPage.getResults().get(1).getDocument()).isEqualTo(document1); + } + + /** + * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term + * test until we have official support for multiple-apps indexing at once. + */ + @Test + public void testGlobalQueryWithMultiplePackages_withPackageFilters() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package2 schema + List schema2 = + ImmutableList.of(new AppSearchSchema.Builder("schema2").build()); + mAppSearchImpl.setSchema( + "package2", + "database2", + schema2, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package1 document + GenericDocument document1 = + new GenericDocument.Builder<>("uri", "schema1").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package1", "database1", document1); + + // Insert package2 document + GenericDocument document2 = + new GenericDocument.Builder<>("uri", "schema2").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package2", "database2", document2); + + // "package1" filter specified + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) + .addFilterPackageNames("package1") + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.globalQuery( + "", searchSpec, mContext.getPackageName(), mGlobalQuerierUid); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document1); + + // "package2" filter specified + searchSpec = + new SearchSpec.Builder() + .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) + .addFilterPackageNames("package2") + .build(); + searchResultPage = + mAppSearchImpl.globalQuery( + "", searchSpec, mContext.getPackageName(), mGlobalQuerierUid); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document2); + } + + @Test + public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception { + // Values for a "foo" client + String packageNameFoo = "packageFoo"; + byte[] sha256CertFoo = new byte[] {10}; + int uidFoo = 1; + + // Make sure foo package will pass package manager checks. + mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo); + mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo); + + // Set schema1 + String prefix = AppSearchImpl.createPrefix("package", "database"); + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("schema1").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "schema1", + ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))), + /*forceOverride=*/ false); + + // "schema1" is platform hidden now and package visible to package1 + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "schema1", mGlobalQuerierUid)) + .isFalse(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo)) + .isTrue(); + + // Add a new schema, and include the already-existing "schema1" + mAppSearchImpl.setSchema( + "package", + "database", + ImmutableList.of( + new AppSearchSchema.Builder("schema1").build(), + new AppSearchSchema.Builder("schema2").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "schema1", + ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))), + /*forceOverride=*/ false); + + // Check that "schema1" still has the same visibility settings + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "schema1", mGlobalQuerierUid)) + .isFalse(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo)) + .isTrue(); + + // "schema2" has default visibility settings + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "schema2", mGlobalQuerierUid)) + .isTrue(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller(prefix, prefix + "schema2", uidFoo)) + .isFalse(); + } + + @Test + public void testRemoveSchema_removedFromVisibilityStore() throws Exception { + // Values for a "foo" client + String packageNameFoo = "packageFoo"; + byte[] sha256CertFoo = new byte[] {10}; + int uidFoo = 1; + + // Make sure foo package will pass package manager checks. + mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo); + mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo); + + String prefix = AppSearchImpl.createPrefix("package", "database"); + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("schema1").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "schema1", + ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))), + /*forceOverride=*/ false); + + // "schema1" is platform hidden now and package accessible + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "schema1", mGlobalQuerierUid)) + .isFalse(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo)) + .isTrue(); + + // Remove "schema1" by force overriding + mAppSearchImpl.setSchema( + "package", + "database", + /*schemas=*/ Collections.emptyList(), + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ true); + + // Check that "schema1" is no longer considered platform hidden or package accessible + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "schema1", mGlobalQuerierUid)) + .isTrue(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo)) + .isFalse(); + + // Add "schema1" back, it gets default visibility settings which means it's not platform + // hidden and not package accessible + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("schema1").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "schema1", mGlobalQuerierUid)) + .isTrue(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo)) + .isFalse(); + } + + @Test + public void testSetSchema_defaultPlatformVisible() throws Exception { + String prefix = AppSearchImpl.createPrefix("package", "database"); + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "Schema", mGlobalQuerierUid)) + .isTrue(); + } + + @Test + public void testSetSchema_platformHidden() throws Exception { + String prefix = AppSearchImpl.createPrefix("package", "database"); + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("Schema"), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "Schema", mGlobalQuerierUid)) + .isFalse(); + } + + @Test + public void testSetSchema_defaultNotPackageAccessible() throws Exception { + String prefix = AppSearchImpl.createPrefix("package", "database"); + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller( + prefix, prefix + "Schema", /*callerUid=*/ 42)) + .isFalse(); + } + + @Test + public void testSetSchema_packageAccessible() throws Exception { + // Values for a "foo" client + String packageNameFoo = "packageFoo"; + byte[] sha256CertFoo = new byte[] {10}; + int uidFoo = 1; + + // Make sure foo package will pass package manager checks. + mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo); + mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo); + + String prefix = AppSearchImpl.createPrefix("package", "database"); + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "Schema", + ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))), + /*forceOverride=*/ false); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaSearchableByCaller(prefix, prefix + "Schema", uidFoo)) + .isTrue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/appsearch/MockPackageManager.java b/services/tests/servicestests/src/com/android/server/appsearch/MockPackageManager.java new file mode 100644 index 0000000000000..459fc53dcc08f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appsearch/MockPackageManager.java @@ -0,0 +1,88 @@ +/* + * 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); + } +} diff --git a/services/tests/servicestests/src/com/android/server/appsearch/VisibilityStoreTest.java b/services/tests/servicestests/src/com/android/server/appsearch/VisibilityStoreTest.java new file mode 100644 index 0000000000000..8d35ebe8e2e11 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appsearch/VisibilityStoreTest.java @@ -0,0 +1,324 @@ +/* + * 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 it can access +// AppSearchImpl and VisibilityStore methods without having to make methods public. This should be +// moved into a proper package once AppSearchImpl-VisibilityStore's dependencies are refactored. +package com.android.server.appsearch.external.localstorage; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.appsearch.PackageIdentifier; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.PackageManager; + +import androidx.test.core.app.ApplicationProvider; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.util.Collections; + +public class VisibilityStoreTest { + + @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + private MockPackageManager mMockPackageManager = new MockPackageManager(); + private Context mContext; + private AppSearchImpl mAppSearchImpl; + private VisibilityStore mVisibilityStore; + private int mGlobalQuerierUid; + + @Before + public void setUp() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + mContext = + new ContextWrapper(context) { + @Override + public PackageManager getPackageManager() { + return mMockPackageManager.getMockPackageManager(); + } + }; + + // Give ourselves global query permissions + mAppSearchImpl = + AppSearchImpl.create( + mTemporaryFolder.newFolder(), + mContext, + mContext.getUserId(), + /*globalQuerierPackage=*/ mContext.getPackageName()); + mGlobalQuerierUid = + mContext.getPackageManager().getPackageUid(mContext.getPackageName(), /*flags=*/ 0); + + mVisibilityStore = mAppSearchImpl.getVisibilityStoreLocked(); + } + + /** + * Make sure that we don't conflict with any special characters that AppSearchImpl has reserved. + */ + @Test + public void testValidPackageName() { + assertThat(VisibilityStore.PACKAGE_NAME) + .doesNotContain( + "" + AppSearchImpl.PACKAGE_DELIMITER); // Convert the chars to CharSequences + assertThat(VisibilityStore.PACKAGE_NAME) + .doesNotContain( + "" + + AppSearchImpl + .DATABASE_DELIMITER); // Convert the chars to CharSequences + } + + /** + * Make sure that we don't conflict with any special characters that AppSearchImpl has reserved. + */ + @Test + public void testValidDatabaseName() { + assertThat(VisibilityStore.DATABASE_NAME) + .doesNotContain( + "" + AppSearchImpl.PACKAGE_DELIMITER); // Convert the chars to CharSequences + assertThat(VisibilityStore.DATABASE_NAME) + .doesNotContain( + "" + + AppSearchImpl + .DATABASE_DELIMITER); // Convert the chars to CharSequences + } + + @Test + public void testSetVisibility_platformSurfaceable() throws Exception { + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of( + "prefix/schema1", "prefix/schema2"), + /*schemasPackageAccessible=*/ Collections.emptyMap()); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schema1", mGlobalQuerierUid)) + .isFalse(); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schema2", mGlobalQuerierUid)) + .isFalse(); + + // New .setVisibility() call completely overrides previous visibility settings. So + // "schema2" isn't preserved. + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of( + "prefix/schema1", "prefix/schema3"), + /*schemasPackageAccessible=*/ Collections.emptyMap()); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schema1", mGlobalQuerierUid)) + .isFalse(); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schema2", mGlobalQuerierUid)) + .isTrue(); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schema3", mGlobalQuerierUid)) + .isFalse(); + + // Everything defaults to visible again. + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ Collections.emptyMap()); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schema1", mGlobalQuerierUid)) + .isTrue(); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schema2", mGlobalQuerierUid)) + .isTrue(); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schema3", mGlobalQuerierUid)) + .isTrue(); + } + + @Test + public void testIsSchemaSearchableByCaller_platformQuerierHandlesNameNotFoundException() + throws Exception { + // Initialized the VisibilityStore with this context's package name as the global querier. + mMockPackageManager.mockThrowsNameNotFoundException(mContext.getPackageName()); + + // Create a new VisibilityStore instance since we look up the UID on initialization + AppSearchImpl appSearchImpl = + AppSearchImpl.create( + mTemporaryFolder.newFolder(), + mContext, + mContext.getUserId(), + /*globalQuerierPackage=*/ mContext.getPackageName()); + VisibilityStore visibilityStore = appSearchImpl.getVisibilityStoreLocked(); + + // Use some arbitrary callerUid. If we can't find the global querier's uid though, + // nothing should be platform surfaceable. + assertThat( + visibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaFoo", /*callerUid=*/ 0)) + .isFalse(); + } + + @Test + public void testSetVisibility_packageAccessible() throws Exception { + // Values for a "foo" client + String packageNameFoo = "packageFoo"; + byte[] sha256CertFoo = new byte[] {10}; + int uidFoo = 1; + + // Values for a "bar" client + String packageNameBar = "packageBar"; + byte[] sha256CertBar = new byte[] {100}; + int uidBar = 2; + + // Can't be the same value as uidFoo nor uidBar + int uidNotFooOrBar = 3; + + // By default, a schema isn't package accessible. + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaFoo", uidFoo)) + .isFalse(); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaBar", uidBar)) + .isFalse(); + + // Grant package access + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "prefix/schemaFoo", + ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo)), + "prefix/schemaBar", + ImmutableList.of(new PackageIdentifier(packageNameBar, sha256CertBar)))); + + // Should fail if PackageManager doesn't see that it has the proper certificate + mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo); + mMockPackageManager.mockRemoveSigningCertificate(packageNameFoo, sha256CertFoo); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaFoo", uidFoo)) + .isFalse(); + + // Should fail if PackageManager doesn't think the package belongs to the uid + mMockPackageManager.mockGetPackageUidAsUser( + packageNameFoo, mContext.getUserId(), uidNotFooOrBar); + mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaFoo", uidFoo)) + .isFalse(); + + // But if uid and certificate match, then we should have access + mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo); + mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaFoo", uidFoo)) + .isTrue(); + + mMockPackageManager.mockGetPackageUidAsUser(packageNameBar, mContext.getUserId(), uidBar); + mMockPackageManager.mockAddSigningCertificate(packageNameBar, sha256CertBar); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaBar", uidBar)) + .isTrue(); + + // New .setVisibility() call completely overrides previous visibility settings. So + // "schemaBar" settings aren't preserved. + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "prefix/schemaFoo", + ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo)))); + + mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo); + mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaFoo", uidFoo)) + .isTrue(); + + mMockPackageManager.mockGetPackageUidAsUser(packageNameBar, mContext.getUserId(), uidBar); + mMockPackageManager.mockAddSigningCertificate(packageNameBar, sha256CertBar); + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaBar", uidBar)) + .isFalse(); + } + + @Test + public void testIsSchemaSearchableByCaller_packageAccessibilityHandlesNameNotFoundException() + throws Exception { + // Values for a "foo" client + String packageNameFoo = "packageFoo"; + byte[] sha256CertFoo = new byte[] {10}; + int uidFoo = 1; + + // Pretend we can't find the Foo package. + mMockPackageManager.mockThrowsNameNotFoundException(packageNameFoo); + + // Grant package access + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "prefix/schemaFoo", + ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo)))); + + // If we can't verify the Foo package that has access, assume it doesn't have access. + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + "prefix", "prefix/schemaFoo", uidFoo)) + .isFalse(); + } + + @Test + public void testEmptyPrefix() throws Exception { + // Values for a "foo" client + String packageNameFoo = "packageFoo"; + byte[] sha256CertFoo = new byte[] {10}; + int uidFoo = 1; + + mVisibilityStore.setVisibility( + /*prefix=*/ "", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "schema", + ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo)))); + + assertThat( + mVisibilityStore.isSchemaSearchableByCaller( + /*prefix=*/ "", "schema", mGlobalQuerierUid)) + .isTrue(); + + mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo); + mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo); + assertThat(mVisibilityStore.isSchemaSearchableByCaller(/*prefix=*/ "", "schema", uidFoo)) + .isTrue(); + } +}