Merge "Add a platform version of VisibilityStore."

This commit is contained in:
Cassie Wang
2021-01-21 16:33:05 +00:00
committed by Android (Google) Code Review
5 changed files with 908 additions and 104 deletions

View File

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

View File

@@ -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;

View File

@@ -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<AppSearchSchema> 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<AppSearchSchema> 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<AppSearchSchema> 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<AppSearchSchema> 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}