blob: ef6c9630d841b73bb72ebb2d5699f47d835a91f2 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.media;
import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO;
import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE;
import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_PLAYLIST;
import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_SUBTITLE;
import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO;
import static com.android.providers.media.AccessChecker.getWhereForConstrainedAccess;
import static com.android.providers.media.AccessChecker.getWhereForMediaTypeMatch;
import static com.android.providers.media.AccessChecker.getWhereForOwnerPackageMatch;
import static com.android.providers.media.AccessChecker.getWhereForUserIdMatch;
import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess;
import static com.android.providers.media.AccessChecker.hasAccessToCollection;
import static com.android.providers.media.AccessChecker.hasUserSelectedAccess;
import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri;
import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA;
import static com.android.providers.media.LocalUriMatcher.DOWNLOADS;
import static com.android.providers.media.LocalUriMatcher.DOWNLOADS_ID;
import static com.android.providers.media.LocalUriMatcher.FILES;
import static com.android.providers.media.LocalUriMatcher.FILES_ID;
import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA;
import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID;
import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS;
import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID;
import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA;
import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID;
import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS;
import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.os.Bundle;
import android.system.Os;
import android.text.TextUtils;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
public class AccessCheckerTest {
@Test
public void testHasAccessToCollection_forRead_noPerms() {
LocalCallingIdentity hasNoPerms = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), 0);
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA,
DOWNLOADS,
FILES)) {
// App with no permissions only has access to owned files
assertWithMessage("Expected no global read access to collection " + collection)
.that(hasAccessToCollection(hasNoPerms, collection, false))
.isFalse();
}
}
@Test
public void testHasAccessToCollection_forRead_hasReadMediaPerms() {
LocalCallingIdentity hasReadMedia = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), getReadMediaPermission());
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA)) {
// App with read media permissions only has full read access to media collection
assertWithMessage("Expected full read access to " + collection)
.that(hasAccessToCollection(hasReadMedia, collection, false))
.isTrue();
}
for (int collection : Arrays.asList(
DOWNLOADS,
FILES)) {
// App with read media permissions doesn't have full read access to non-media collection
assertWithMessage("Expected no full read access to " + collection)
.that(hasAccessToCollection(hasReadMedia, collection, false))
.isFalse();
}
}
@Test
public void testHasAccessToCollection_forRead_hasLegacyRead() {
LocalCallingIdentity hasLegacyRead = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), getLegacyReadPermission());
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA,
DOWNLOADS,
FILES)) {
// Legacy app with READ_EXTERNAL_STORAGE permission has read access to all files
assertWithMessage("Expected global read access to collection " + collection)
.that(hasAccessToCollection(hasLegacyRead, collection, false))
.isTrue();
}
}
@Test
public void testHasAccessToCollection_forWrite_noPerms() {
LocalCallingIdentity hasNoPerms = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), 0);
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA,
DOWNLOADS,
FILES)) {
// App with no permissions only has access to owned files
assertWithMessage("Expected no global write access to collection " + collection)
.that(hasAccessToCollection(hasNoPerms, collection, true))
.isFalse();
}
}
@Test
public void testHasAccessToCollection_forWrite_hasWriteMediaPerms() {
LocalCallingIdentity hasWriteMedia = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), getWriteMediaPermission());
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA)) {
// App with write media permissions only has full write access to media collection
assertWithMessage("Expected global write access to " + collection)
.that(hasAccessToCollection(hasWriteMedia, collection, true))
.isTrue();
}
for (int collection : Arrays.asList(
DOWNLOADS,
FILES)) {
// App with write media permissions doesn't have full write access to non-media
// collection
assertWithMessage("Expected no full write access to " + collection)
.that(hasAccessToCollection(hasWriteMedia, collection, true))
.isFalse();
}
}
@Test
public void testHasAccessToCollection_forWrite_hasLegacyWrite() {
LocalCallingIdentity hasLegacyWrite = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(),
getLegacyWritePermission());
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA)) {
// Legacy app with WRITE_EXTERNAL_STORAGE permission has write access to media
// files
assertWithMessage("Expected global write access to collection " + collection)
.that(hasAccessToCollection(hasLegacyWrite, collection, true))
.isTrue();
}
for (int collection : Arrays.asList(
DOWNLOADS,
FILES)) {
// Legacy app with WRITE_EXTERNAL_STORAGE permission has write access to only primary
// storage.
assertWithMessage("Expected no full write access to " + collection)
.that(hasAccessToCollection(hasLegacyWrite, collection, true))
.isFalse();
}
}
@Test
public void testHasUserSelectedAccess_noPerms() {
LocalCallingIdentity hasNoPerms = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), 0);
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA,
DOWNLOADS,
FILES)) {
// App with no permissions doesn't have access to user selected items.
assertWithMessage("Expected no user selected read access to collection " + collection)
.that(hasUserSelectedAccess(hasNoPerms, collection, false)).isFalse();
}
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA,
DOWNLOADS,
FILES)) {
// App with no permissions doesn't have access to user selected items.
assertWithMessage("Expected no user selected write access to collection " + collection)
.that(hasUserSelectedAccess(hasNoPerms, collection, true)).isFalse();
}
}
@Test
public void testHasUserSelectedAccess_userSelectedPerms() {
LocalCallingIdentity userSelectPerms = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(),
getUserSelectedPermission());
for (int collection : Arrays.asList(
VIDEO_MEDIA,
IMAGES_MEDIA,
DOWNLOADS,
FILES)) {
// App with user selected permission grant has read access to user selected visual media
// files
assertWithMessage("Expected no user selected read access to collection " + collection)
.that(hasUserSelectedAccess(userSelectPerms, collection, false)).isTrue();
}
// App with user selected permission grant doesn't have access to audio files.
assertWithMessage("Expected no user selected read access to audio collection")
.that(hasUserSelectedAccess(userSelectPerms, AUDIO_MEDIA, false)).isFalse();
for (int collection : Arrays.asList(
AUDIO_MEDIA,
VIDEO_MEDIA,
IMAGES_MEDIA,
DOWNLOADS,
FILES)) {
// App with user selected permission grant doesn't have write access
assertWithMessage("Expected no user selected write access to collection " + collection)
.that(hasUserSelectedAccess(userSelectPerms, collection, true)).isFalse();
}
}
@Test
public void testGetWhereForUserSelectedAccess() {
LocalCallingIdentity callingIdentity = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(),
getUserSelectedPermission());
for (int collection : Arrays.asList(
VIDEO_MEDIA,
VIDEO_MEDIA_ID,
IMAGES_MEDIA,
IMAGES_MEDIA_ID,
DOWNLOADS,
DOWNLOADS_ID,
FILES,
FILES_ID)) {
assertWithMessage("Expected user selected where clause for collection " + collection)
.that(getWhereForUserSelectedAccess(callingIdentity, collection))
.isEqualTo("_id IN (SELECT file_id from media_grants WHERE "
+ getWhereForOwnerPackageMatch(callingIdentity)
+ " AND "
+ getWhereForUserIdMatch(callingIdentity)
+ ")");
}
for (int collection : Arrays.asList(
IMAGES_THUMBNAILS,
IMAGES_THUMBNAILS_ID)) {
assertWithMessage("Expected user selected where clause for collection " + collection)
.that(getWhereForUserSelectedAccess(callingIdentity, collection))
.isEqualTo("image_id IN (SELECT file_id from media_grants WHERE "
+ getWhereForOwnerPackageMatch(callingIdentity)
+ " AND "
+ getWhereForUserIdMatch(callingIdentity)
+ ")");
}
for (int collection : Arrays.asList(
VIDEO_THUMBNAILS,
VIDEO_THUMBNAILS_ID)) {
assertWithMessage("Expected user selected where clause for collection " + collection)
.that(getWhereForUserSelectedAccess(callingIdentity, collection))
.isEqualTo("video_id IN (SELECT file_id from media_grants WHERE "
+ getWhereForOwnerPackageMatch(callingIdentity)
+ " AND "
+ getWhereForUserIdMatch(callingIdentity)
+ ")");
}
assertThrows(UnsupportedOperationException.class,
() -> getWhereForUserSelectedAccess(callingIdentity, AUDIO_MEDIA));
}
@Test
public void testGetWhereForConstrainedAccess_forRead_noPerms() {
LocalCallingIdentity hasNoPerms = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), 0);
// App with no permissions only has access to owned files
assertWithMessage("Expected owned access SQL for Audio collection")
.that(getWhereForConstrainedAccess(hasNoPerms, AUDIO_MEDIA, false, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms)
+ " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1");
assertWithMessage("Expected owned access SQL for Video collection")
.that(getWhereForConstrainedAccess(hasNoPerms, VIDEO_MEDIA, false, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms));
assertWithMessage("Expected owned access SQL for Images collection")
.that(getWhereForConstrainedAccess(hasNoPerms, IMAGES_MEDIA, false, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms));
// App with no permissions only has access to owned files
assertWithMessage("Expected owned access SQL for Downloads collection")
.that(getWhereForConstrainedAccess(hasNoPerms, DOWNLOADS, false, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms));
assertWithMessage("Expected owned access SQL for FILES collection")
.that(getWhereForConstrainedAccess(hasNoPerms, FILES, false, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasNoPerms));
}
@Test
public void testGetWhereForConstrainedAccess_forRead_hasReadMediaPerms() {
LocalCallingIdentity hasReadMedia = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), getReadMediaPermission());
// App with READ_EXTERNAL_STORAGE or READ_MEDIA_* permission has access to media files. In
// this case, we already tested AccessCheckHelper#hasAccessToCollection returns true
// App with READ_EXTERNAL_STORAGE or READ_MEDIA_* permission has access to only owned
// non-media files or media files.
assertWithMessage("Expected owned access SQL for Downloads collection")
.that(getWhereForConstrainedAccess(hasReadMedia, DOWNLOADS, false, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasReadMedia));
assertWithMessage("Expected owned access SQL for FILES collection")
.that(getWhereForConstrainedAccess(hasReadMedia, FILES, false, Bundle.EMPTY))
.isEqualTo(
getWhereForOwnerPackageMatch(hasReadMedia) + " OR " + getFilesAccessSql());
}
@Test
public void testGetWhereForConstrainedAccess_forWrite_noPerms() {
LocalCallingIdentity noPerms = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), 0);
// App with no permissions only has access to owned files.
assertWithMessage("Expected owned access SQL for Audio collection")
.that(getWhereForConstrainedAccess(noPerms, AUDIO_MEDIA, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(noPerms)
+ " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1");
assertWithMessage("Expected owned access SQL for Video collection")
.that(getWhereForConstrainedAccess(noPerms, VIDEO_MEDIA, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(noPerms));
assertWithMessage("Expected owned access SQL for Images collection")
.that(getWhereForConstrainedAccess(noPerms, IMAGES_MEDIA, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(noPerms));
// App with no permissions only has access to owned files
assertWithMessage("Expected owned access SQL for Downloads collection")
.that(getWhereForConstrainedAccess(noPerms, DOWNLOADS, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(noPerms));
assertWithMessage("Expected owned access SQL for FILES collection")
.that(getWhereForConstrainedAccess(noPerms, FILES, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(noPerms));
}
@Test
public void testGetWhereForConstrainedAccess_forWrite_hasWriteMediaPerms() {
LocalCallingIdentity hasReadPerms = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(), getWriteMediaPermission());
// App with write permission to media files has access write access to media files. In
// this case, we already tested AccessCheckHelper#hasAccessToCollection returns true
// App with write permission to media files has access write access to media files and owned
// files.
assertWithMessage("Expected owned access SQL for Downloads collection")
.that(getWhereForConstrainedAccess(hasReadPerms, DOWNLOADS, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasReadPerms));
assertWithMessage("Expected owned access SQL for FILES collection")
.that(getWhereForConstrainedAccess(hasReadPerms, FILES, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasReadPerms) + " OR "
+ getFilesAccessSql());
}
@Test
public void testIsRedactionNeededForPickerUri_returnsFalse_withNoRedactPerms() {
LocalCallingIdentity callingIdentityWithRedactionNotNeededPermission =
LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(),
~LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED);
assertFalse("App with write perms should get non redacted data",
isRedactionNeededForPickerUri(callingIdentityWithRedactionNotNeededPermission));
}
@Test
public void testIsRedactionNeededForPickerUri_returnsTrue_withRedactPerms() {
LocalCallingIdentity callingIdentityWithRedactionNeededPermission =
LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(),
LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED);
assertTrue("App with no perms should get redacted data",
isRedactionNeededForPickerUri(callingIdentityWithRedactionNeededPermission));
}
@Test
public void testGetWhereForConstrainedAccess_forWrite_hasLegacyWrite() {
LocalCallingIdentity hasLegacyWrite = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(),
getLegacyWritePermission());
// Legacy app with WRITE_EXTERNAL_STORAGE permission has access to media files. In
// this case, we already tested AccessCheckHelper#hasAccessToCollection returns true
// Legacy app with WRITE_EXTERNAL_STORAGE permission has access to non-media files as well.
// However, they don't have global write access to secondary volume.
assertWithMessage("Expected where clause SQL for Downloads collection to be")
.that(getWhereForConstrainedAccess(hasLegacyWrite, DOWNLOADS, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasLegacyWrite) + " OR "
+ AccessChecker.getWhereForExternalPrimaryMatch());
assertWithMessage("Expected where clause SQL for FILES collection to be")
.that(getWhereForConstrainedAccess(hasLegacyWrite, FILES, true, Bundle.EMPTY))
.isEqualTo(getWhereForOwnerPackageMatch(hasLegacyWrite) + " OR "
+ AccessChecker.getWhereForExternalPrimaryMatch() + " OR "
+ getFilesAccessSql());
}
private String getFilesAccessSql() {
final ArrayList<String> options = new ArrayList<>();
options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_AUDIO));
options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_PLAYLIST));
options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_SUBTITLE));
options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_VIDEO));
options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_SUBTITLE));
options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_IMAGE));
return TextUtils.join(" OR ", options);
}
private static int getReadMediaPermission() {
return LocalCallingIdentity.PERMISSION_READ_AUDIO
| LocalCallingIdentity.PERMISSION_READ_VIDEO
| LocalCallingIdentity.PERMISSION_READ_IMAGES;
}
private static int getWriteMediaPermission() {
return LocalCallingIdentity.PERMISSION_WRITE_AUDIO
| LocalCallingIdentity.PERMISSION_WRITE_VIDEO
| LocalCallingIdentity.PERMISSION_WRITE_IMAGES;
}
private static int getLegacyReadPermission() {
return LocalCallingIdentity.PERMISSION_READ_AUDIO
| LocalCallingIdentity.PERMISSION_READ_VIDEO
| LocalCallingIdentity.PERMISSION_READ_IMAGES
| LocalCallingIdentity.PERMISSION_IS_LEGACY_READ;
}
private static int getUserSelectedPermission() {
return LocalCallingIdentity.PERMISSION_READ_MEDIA_VISUAL_USER_SELECTED;
}
private static int getLegacyWritePermission() {
return LocalCallingIdentity.PERMISSION_WRITE_AUDIO
| LocalCallingIdentity.PERMISSION_WRITE_VIDEO
| LocalCallingIdentity.PERMISSION_WRITE_IMAGES
| LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
}
}