Allow read-only sharing of Uri in voicemail content provider.
Since we perform our own permission check, uri permission check
done by the content resolver is not sufficient. Our implementation of
the content provider should also explicitly check for uri level permission.
This is needed to allow sharing of voicemail uri from the contacts app.
In a follow up change we will allow sharing of URIs only to those apps that has
ALL permission. Right now the provider definition does not allow sharing
of uri.
Bug: 4961053
Change-Id: I5af53ee76ea10fa5f45c8cdcb95c773cc7ad138e
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java
index c55c11c..d6ddd92 100644
--- a/src/com/android/providers/contacts/VoicemailContentProvider.java
+++ b/src/com/android/providers/contacts/VoicemailContentProvider.java
@@ -26,6 +26,8 @@
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
@@ -91,7 +93,7 @@
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
- UriData uriData = checkPermissionsAndCreateUriData(uri);
+ UriData uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
selectionBuilder.addClause(getPackageRestrictionClause());
return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(),
@@ -117,7 +119,12 @@
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
- UriData uriData = checkPermissionsAndCreateUriData(uri);
+ UriData uriData = null;
+ if (mode.equals("r")) {
+ uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
+ } else {
+ uriData = checkPermissionsAndCreateUriData(uri);
+ }
// openFileHelper() relies on "_data" column to be populated with the file path.
return getTableDelegate(uriData).openFile(uriData, mode);
}
@@ -261,6 +268,20 @@
* Performs necessary voicemail permission checks common to all operations and returns
* the structured representation, {@link UriData}, of the supplied uri.
*/
+ private UriData checkPermissionsAndCreateUriDataForReadOperation(Uri uri) {
+ // If the caller has been explicitly granted read permission to this URI then no need to
+ // check further.
+ if (context().checkCallingUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ == PackageManager.PERMISSION_GRANTED) {
+ return UriData.createUriData(uri);
+ }
+ return checkPermissionsAndCreateUriData(uri);
+ }
+
+ /**
+ * Performs necessary voicemail permission checks common to all operations and returns
+ * the structured representation, {@link UriData}, of the supplied uri.
+ */
private UriData checkPermissionsAndCreateUriData(Uri uri) {
mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
UriData uriData = UriData.createUriData(uri);
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 865c956..29d8eed 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -56,8 +56,6 @@
import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver;
import android.test.mock.MockContext;
-import android.test.mock.MockResources;
-import android.util.TypedValue;
import java.io.File;
import java.io.IOException;
@@ -88,6 +86,7 @@
private Account[] mAccounts = new Account[0];
private Set<String> mGrantedPermissions = Sets.newHashSet();
+ private final Set<Uri> mGrantedUriPermissions = Sets.newHashSet();
private CountryDetector mMockCountryDetector = new CountryDetector(null){
@Override
@@ -145,7 +144,7 @@
Class<? extends ContentProvider> providerClass, String authority) throws Exception {
resolver = new MockContentResolver();
context = new RestrictionMockContext(overallContext, packageName, resolver,
- mGrantedPermissions);
+ mGrantedPermissions, mGrantedUriPermissions);
this.packageName = packageName;
RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(context,
@@ -196,6 +195,14 @@
mGrantedPermissions.removeAll(Arrays.asList(permissions));
}
+ public void addUriPermissions(Uri... uris) {
+ mGrantedUriPermissions.addAll(Arrays.asList(uris));
+ }
+
+ public void removeUriPermissions(Uri... uris) {
+ mGrantedUriPermissions.removeAll(Arrays.asList(uris));
+ }
+
/**
* Mock {@link Context} that reports specific well-known values for testing
* data protection. The creator can override the owner package name, and
@@ -213,16 +220,19 @@
private final ContentResolver mResolver;
private final Resources mRes;
private final Set<String> mGrantedPermissions;
+ private final Set<Uri> mGrantedUriPermissions;
/**
* Create a {@link Context} under the given package name.
*/
public RestrictionMockContext(Context overallContext, String reportedPackageName,
- ContentResolver resolver, Set<String> grantedPermissions) {
+ ContentResolver resolver, Set<String> grantedPermissions,
+ Set<Uri> grantedUriPermissions) {
mOverallContext = overallContext;
mReportedPackageName = reportedPackageName;
mResolver = resolver;
mGrantedPermissions = grantedPermissions;
+ mGrantedUriPermissions = grantedUriPermissions;
mPackageManager = new ContactsMockPackageManager();
mPackageManager.addPackage(1000, PACKAGE_GREY);
@@ -281,6 +291,20 @@
}
@Override
+ public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
+ return checkCallingUriPermission(uri, modeFlags);
+ }
+
+ @Override
+ public int checkCallingUriPermission(Uri uri, int modeFlags) {
+ if (mGrantedUriPermissions.contains(uri)) {
+ return PackageManager.PERMISSION_GRANTED;
+ } else {
+ return PackageManager.PERMISSION_DENIED;
+ }
+ }
+
+ @Override
public int checkCallingOrSelfPermission(String permission) {
return checkCallingPermission(permission);
}
diff --git a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
index 119d8b3..702b55b 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -16,10 +16,13 @@
package com.android.providers.contacts;
+import com.android.common.io.MoreCloseables;
+
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
+import android.os.ParcelFileDescriptor;
import android.provider.CallLog.Calls;
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Status;
@@ -27,6 +30,7 @@
import android.test.MoreAsserts;
import java.io.FileNotFoundException;
+import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
@@ -225,6 +229,107 @@
.build();
}
+ public void testUriPermissions() {
+ setUpForFullPermission();
+ final Uri uri1 = insertVoicemail();
+ final Uri uri2 = insertVoicemail();
+ // Give away all permissions before querying. Access to both uris should be denied.
+ setUpForNoPermission();
+ checkHasNoAccessToUri(uri1);
+ checkHasNoAccessToUri(uri2);
+
+ // Just grant permission to uri1. uri1 should pass but uri2 should still fail.
+ mActor.addUriPermissions(uri1);
+ checkHasReadOnlyAccessToUri(uri1);
+ checkHasNoAccessToUri(uri2);
+
+ // Cleanup.
+ mActor.removeUriPermissions(uri1);
+ }
+
+ private void checkHasNoAccessToUri(final Uri uri) {
+ checkHasNoReadAccessToUri(uri);
+ checkHasNoWriteAccessToUri(uri);
+ }
+
+ private void checkHasReadOnlyAccessToUri(final Uri uri) {
+ checkHasReadAccessToUri(uri);
+ checkHasNoWriteAccessToUri(uri);
+ }
+
+ private void checkHasReadAccessToUri(final Uri uri) {
+ Cursor cursor = null;
+ try {
+ cursor = mResolver.query(uri, null, null ,null, null);
+ assertEquals(1, cursor.getCount());
+ try {
+ ParcelFileDescriptor fd = mResolver.openFileDescriptor(uri, "r");
+ assertNotNull(fd);
+ fd.close();
+ } catch (FileNotFoundException e) {
+ fail(e.getMessage());
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ } finally {
+ MoreCloseables.closeQuietly(cursor);
+ }
+ }
+
+ private void checkHasNoReadAccessToUri(final Uri uri) {
+ EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+ @Override
+ public void run() {
+ mResolver.query(uri, null, null ,null, null);
+ }
+ });
+ EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mResolver.openFileDescriptor(uri, "r");
+ } catch (FileNotFoundException e) {
+ fail(e.getMessage());
+ }
+ }
+ });
+ }
+
+ private void checkHasNoWriteAccessToUri(final Uri uri) {
+ EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+ @Override
+ public void run() {
+ mResolver.update(uri, getTestVoicemailValues(), null, null);
+ }
+ });
+ EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+ @Override
+ public void run() {
+ mResolver.delete(uri, null, null);
+ }
+ });
+ EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mResolver.openFileDescriptor(uri, "w");
+ } catch (FileNotFoundException e) {
+ fail(e.getMessage());
+ }
+ }
+ });
+ EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mResolver.openFileDescriptor(uri, "rw");
+ } catch (FileNotFoundException e) {
+ fail(e.getMessage());
+ }
+ }
+ });
+ }
+
// Test to ensure that all operations fail when no voicemail permission is granted.
public void testNoPermissions() {
setUpForNoPermission();