1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media; 18 19 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO; 20 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE; 21 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_PLAYLIST; 22 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_SUBTITLE; 23 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO; 24 import static android.provider.MediaStore.MediaColumns.OWNER_PACKAGE_NAME; 25 26 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART; 27 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_ID; 28 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS; 29 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS_ID; 30 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS; 31 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID; 32 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID_ALBUMS; 33 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES; 34 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ALL_MEMBERS; 35 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID; 36 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID_MEMBERS; 37 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA; 38 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID; 39 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES; 40 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES_ID; 41 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS; 42 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID; 43 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS; 44 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS_ID; 45 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS; 46 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS_ID; 47 import static com.android.providers.media.LocalUriMatcher.FILES; 48 import static com.android.providers.media.LocalUriMatcher.FILES_ID; 49 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA; 50 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID; 51 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS; 52 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID; 53 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA; 54 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID; 55 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS; 56 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID; 57 import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN; 58 import static com.android.providers.media.MediaProvider.INCLUDED_DEFAULT_DIRECTORIES; 59 import static com.android.providers.media.util.DatabaseUtils.bindSelection; 60 61 import android.os.Bundle; 62 import android.provider.MediaStore; 63 import android.provider.MediaStore.Files.FileColumns; 64 import android.provider.MediaStore.MediaColumns; 65 import android.text.TextUtils; 66 67 import androidx.annotation.NonNull; 68 import androidx.annotation.Nullable; 69 import androidx.annotation.VisibleForTesting; 70 71 import java.util.ArrayList; 72 73 /** 74 * Class responsible for performing all access checks (read/write access states for calling package) 75 * and generating relevant SQL statements 76 */ 77 public class AccessChecker { 78 private static final String NO_ACCESS_SQL = "0"; 79 80 /** 81 * Returns {@code true} if given {@code callingIdentity} has full access to the given collection 82 * 83 * @param callingIdentity {@link LocalCallingIdentity} of the caller to verify permission state 84 * @param uriType the collection info for which the requested access is, 85 * e.g., Images -> {@link MediaProvider}#IMAGES_MEDIA. 86 * @param forWrite type of the access requested. Read / write access to the file / collection. 87 */ hasAccessToCollection(LocalCallingIdentity callingIdentity, int uriType, boolean forWrite)88 public static boolean hasAccessToCollection(LocalCallingIdentity callingIdentity, int uriType, 89 boolean forWrite) { 90 switch (uriType) { 91 case AUDIO_MEDIA_ID: 92 case AUDIO_MEDIA: 93 case AUDIO_PLAYLISTS_ID: 94 case AUDIO_PLAYLISTS: 95 case AUDIO_ARTISTS_ID: 96 case AUDIO_ARTISTS: 97 case AUDIO_ARTISTS_ID_ALBUMS: 98 case AUDIO_ALBUMS_ID: 99 case AUDIO_ALBUMS: 100 case AUDIO_ALBUMART_ID: 101 case AUDIO_ALBUMART: 102 case AUDIO_GENRES_ID: 103 case AUDIO_GENRES: 104 case AUDIO_MEDIA_ID_GENRES_ID: 105 case AUDIO_MEDIA_ID_GENRES: 106 case AUDIO_GENRES_ID_MEMBERS: 107 case AUDIO_GENRES_ALL_MEMBERS: 108 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 109 case AUDIO_PLAYLISTS_ID_MEMBERS: { 110 return callingIdentity.checkCallingPermissionAudio(forWrite); 111 } 112 case IMAGES_MEDIA: 113 case IMAGES_MEDIA_ID: 114 case IMAGES_THUMBNAILS_ID: 115 case IMAGES_THUMBNAILS: { 116 return callingIdentity.checkCallingPermissionImages(forWrite); 117 } 118 case VIDEO_MEDIA_ID: 119 case VIDEO_MEDIA: 120 case VIDEO_THUMBNAILS_ID: 121 case VIDEO_THUMBNAILS: { 122 return callingIdentity.checkCallingPermissionVideo(forWrite); 123 } 124 case DOWNLOADS_ID: 125 case DOWNLOADS: 126 case FILES_ID: 127 case FILES: { 128 // Allow apps with legacy read access to read all files. 129 return !forWrite 130 && callingIdentity.isCallingPackageLegacyRead(); 131 } 132 default: { 133 throw new UnsupportedOperationException( 134 "Unknown or unsupported type: " + uriType); 135 } 136 } 137 } 138 139 /** 140 * Returns {@code true} if the request is for read access to a collection that contains 141 * visual media files and app has READ_MEDIA_VISUAL_USER_SELECTED permission. 142 * 143 * @param callingIdentity {@link LocalCallingIdentity} of the caller to verify permission state 144 * @param uriType the collection info for which the requested access is, 145 * e.g., Images -> {@link MediaProvider}#IMAGES_MEDIA. 146 * @param forWrite type of the access requested. Read / write access to the file / collection. 147 */ hasUserSelectedAccess(@onNull LocalCallingIdentity callingIdentity, int uriType, boolean forWrite)148 public static boolean hasUserSelectedAccess(@NonNull LocalCallingIdentity callingIdentity, 149 int uriType, boolean forWrite) { 150 if (forWrite) { 151 // Apps only get read access via media_grants. For write access on user selected items, 152 // app needs to get uri grants. 153 return false; 154 } 155 156 switch (uriType) { 157 case IMAGES_MEDIA: 158 case IMAGES_MEDIA_ID: 159 case IMAGES_THUMBNAILS_ID: 160 case IMAGES_THUMBNAILS: 161 case VIDEO_MEDIA_ID: 162 case VIDEO_MEDIA: 163 case VIDEO_THUMBNAILS_ID: 164 case VIDEO_THUMBNAILS: 165 case DOWNLOADS_ID: 166 case DOWNLOADS: 167 case FILES_ID: 168 case FILES: { 169 return callingIdentity.checkCallingPermissionUserSelected(); 170 } 171 default: return false; 172 } 173 } 174 175 /** 176 * Returns where clause for access on user selected permission. 177 * 178 * <p><strong>NOTE:</strong> This method assumes that app has necessary permissions and returns 179 * the where clause without checking any permission state of the app. 180 */ 181 @NonNull getWhereForUserSelectedAccess( @onNull LocalCallingIdentity callingIdentity, int uriType)182 public static String getWhereForUserSelectedAccess( 183 @NonNull LocalCallingIdentity callingIdentity, int uriType) { 184 switch (uriType) { 185 case IMAGES_MEDIA: 186 case IMAGES_MEDIA_ID: 187 case VIDEO_MEDIA_ID: 188 case VIDEO_MEDIA: 189 case DOWNLOADS_ID: 190 case DOWNLOADS: 191 case FILES_ID: 192 case FILES: { 193 return getWhereForUserSelectedMatch(callingIdentity, MediaColumns._ID); 194 } 195 case IMAGES_THUMBNAILS_ID: 196 case IMAGES_THUMBNAILS: { 197 return getWhereForUserSelectedMatch(callingIdentity, "image_id"); 198 } 199 case VIDEO_THUMBNAILS_ID: 200 case VIDEO_THUMBNAILS: { 201 return getWhereForUserSelectedMatch(callingIdentity, "video_id"); 202 } 203 default: 204 throw new UnsupportedOperationException( 205 "Unknown or unsupported type: " + uriType); 206 } 207 } 208 209 /** 210 * Returns where clause for access on user selected permission with filtering for latest 211 * selection only. 212 * 213 * <p><strong>NOTE:</strong> This method assumes that app has necessary permissions and returns 214 * the where clause without checking any permission state of the app. 215 */ 216 @NonNull getWhereForLatestSelection( @onNull LocalCallingIdentity callingIdentity, int uriType)217 public static String getWhereForLatestSelection( 218 @NonNull LocalCallingIdentity callingIdentity, int uriType) { 219 switch (uriType) { 220 case IMAGES_MEDIA: 221 case IMAGES_MEDIA_ID: 222 case VIDEO_MEDIA_ID: 223 case VIDEO_MEDIA: 224 case DOWNLOADS_ID: 225 case DOWNLOADS: 226 case FILES_ID: 227 case FILES: { 228 return getWhereClauseForLatestUserSelection(callingIdentity, MediaColumns._ID); 229 } 230 case IMAGES_THUMBNAILS_ID: 231 case IMAGES_THUMBNAILS: { 232 return getWhereClauseForLatestUserSelection(callingIdentity, "image_id"); 233 } 234 case VIDEO_THUMBNAILS_ID: 235 case VIDEO_THUMBNAILS: { 236 return getWhereClauseForLatestUserSelection(callingIdentity, "video_id"); 237 } 238 default: 239 throw new UnsupportedOperationException( 240 "Unknown or unsupported type: " + uriType); 241 } 242 } 243 244 /** 245 * Returns where clause for constrained access. 246 * 247 * Where clause is generated based on the given collection type{@code uriType} and access 248 * permissions of the app. Generated where clause may include one or more combinations of 249 * below checks - 250 * * Match {@link MediaColumns#OWNER_PACKAGE_NAME} with calling package's package name. 251 * * Match ringtone or alarm or notification files to allow legacy use-cases 252 * * Match media files if app has corresponding read / write permissions on media files 253 * * Match files in primary storage if app has legacy write permissions 254 * * Match default directories in case of use-cases like System gallery 255 * 256 * This method assumes global access permission checks and full access checks for the collection 257 * is already checked. The method returns where clause assuming app doesn't have global access 258 * permission to the given collection type. 259 * 260 * @param callingIdentity {@link LocalCallingIdentity} of the caller to verify permission state 261 * @param uriType the collection info for which the requested access is, 262 * e.g., Images -> {@link MediaProvider}#IMAGES_MEDIA. 263 * @param forWrite type of the access requested. Read / write access to the file / collection. 264 * @param extras bundle containing {@link MediaProvider#INCLUDED_DEFAULT_DIRECTORIES} info if 265 * there is any. 266 */ 267 @NonNull getWhereForConstrainedAccess( @onNull LocalCallingIdentity callingIdentity, int uriType, boolean forWrite, @NonNull Bundle extras)268 public static String getWhereForConstrainedAccess( 269 @NonNull LocalCallingIdentity callingIdentity, int uriType, 270 boolean forWrite, @NonNull Bundle extras) { 271 switch (uriType) { 272 case AUDIO_MEDIA_ID: 273 case AUDIO_MEDIA: { 274 // Apps without Audio permission can only see their own 275 // media, but we also let them see ringtone-style media to 276 // support legacy use-cases. 277 return getWhereForOwnerPackageMatch(callingIdentity) 278 + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"; 279 } 280 case AUDIO_PLAYLISTS_ID: 281 case AUDIO_PLAYLISTS: 282 case IMAGES_MEDIA: 283 case IMAGES_MEDIA_ID: 284 case VIDEO_MEDIA_ID: 285 case VIDEO_MEDIA: { 286 return getWhereForOwnerPackageMatch(callingIdentity); 287 } 288 case AUDIO_ARTISTS_ID: 289 case AUDIO_ARTISTS: 290 case AUDIO_ARTISTS_ID_ALBUMS: 291 case AUDIO_ALBUMS_ID: 292 case AUDIO_ALBUMS: 293 case AUDIO_ALBUMART_ID: 294 case AUDIO_ALBUMART: 295 case AUDIO_GENRES_ID: 296 case AUDIO_GENRES: 297 case AUDIO_MEDIA_ID_GENRES_ID: 298 case AUDIO_MEDIA_ID_GENRES: 299 case AUDIO_GENRES_ID_MEMBERS: 300 case AUDIO_GENRES_ALL_MEMBERS: 301 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 302 case AUDIO_PLAYLISTS_ID_MEMBERS: { 303 // We don't have a great way to filter parsed metadata by 304 // owner, so callers need to hold READ_MEDIA_AUDIO 305 return NO_ACCESS_SQL; 306 } 307 case IMAGES_THUMBNAILS_ID: 308 case IMAGES_THUMBNAILS: { 309 return "image_id IN (SELECT _id FROM images WHERE " 310 + getWhereForOwnerPackageMatch(callingIdentity) + ")"; 311 } 312 case VIDEO_THUMBNAILS_ID: 313 case VIDEO_THUMBNAILS: { 314 return "video_id IN (SELECT _id FROM video WHERE " 315 + getWhereForOwnerPackageMatch(callingIdentity) + ")"; 316 } 317 case DOWNLOADS_ID: 318 case DOWNLOADS: { 319 final ArrayList<String> options = new ArrayList<>(); 320 // Allow access to owned files 321 options.add(getWhereForOwnerPackageMatch(callingIdentity)); 322 323 if (shouldAllowLegacyWrite(callingIdentity, forWrite)) { 324 // b/130766639: We're willing to let apps interact with well-defined MediaStore 325 // collections on secondary storage devices, but we continue to hold 326 // firm that any other legacy access to secondary storage devices must 327 // be read-only. 328 options.add(getWhereForExternalPrimaryMatch()); 329 } 330 331 return TextUtils.join(" OR ", options); 332 } 333 case FILES_ID: 334 case FILES: { 335 final ArrayList<String> options = new ArrayList<>(); 336 // Allow access to owned files 337 options.add(getWhereForOwnerPackageMatch(callingIdentity)); 338 339 if (shouldAllowLegacyWrite(callingIdentity, forWrite)) { 340 // b/130766639: We're willing to let apps interact with well-defined MediaStore 341 // collections on secondary storage devices, but we continue to hold 342 // firm that any other legacy access to secondary storage devices must 343 // be read-only. 344 options.add(getWhereForExternalPrimaryMatch()); 345 } 346 347 // Allow access to media files if the app has corresponding read/write media 348 // permission 349 if (hasAccessToCollection(callingIdentity, AUDIO_MEDIA, forWrite)) { 350 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_AUDIO)); 351 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_PLAYLIST)); 352 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_SUBTITLE)); 353 } 354 if (hasAccessToCollection(callingIdentity, VIDEO_MEDIA, forWrite)) { 355 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_VIDEO)); 356 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_SUBTITLE)); 357 } 358 if (hasAccessToCollection(callingIdentity, IMAGES_MEDIA, forWrite)) { 359 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_IMAGE)); 360 } 361 362 // Allow access to file in directories. This si particularly used only for 363 // SystemGallery use-case 364 final String defaultDirectorySql = getWhereForDefaultDirectoryMatch(extras); 365 if (defaultDirectorySql != null) { 366 options.add(defaultDirectorySql); 367 } 368 369 return TextUtils.join(" OR ", options); 370 } 371 default: 372 throw new UnsupportedOperationException( 373 "Unknown or unsupported type: " + uriType); 374 } 375 } 376 shouldAllowLegacyWrite(LocalCallingIdentity callingIdentity, boolean forWrite)377 private static boolean shouldAllowLegacyWrite(LocalCallingIdentity callingIdentity, 378 boolean forWrite) { 379 return forWrite && callingIdentity.isCallingPackageLegacyWrite(); 380 } 381 382 /** 383 * Returns where clause to match {@link MediaColumns#OWNER_PACKAGE_NAME} with package names of 384 * the given {@code callingIdentity} 385 */ getWhereForOwnerPackageMatch(LocalCallingIdentity callingIdentity)386 public static String getWhereForOwnerPackageMatch(LocalCallingIdentity callingIdentity) { 387 return OWNER_PACKAGE_NAME + " IN " + callingIdentity.getSharedPackagesAsString(); 388 } 389 390 /** 391 * Generates the where clause for a user_id media grant match. 392 * 393 * @param callingIdentity - the current caller. 394 * @return where clause to match {@link MediaGrants#PACKAGE_USER_ID_COLUMN} with user id of the 395 * given {@code callingIdentity} 396 */ getWhereForUserIdMatch(LocalCallingIdentity callingIdentity)397 public static String getWhereForUserIdMatch(LocalCallingIdentity callingIdentity) { 398 return PACKAGE_USER_ID_COLUMN + "=" + callingIdentity.uid / MediaStore.PER_USER_RANGE; 399 } 400 401 /** 402 * Returns true if redaction is needed for openFile calls on picker uri by checking calling 403 * package permission 404 * 405 * @param callingIdentity - the current caller 406 */ isRedactionNeededForPickerUri(LocalCallingIdentity callingIdentity)407 public static boolean isRedactionNeededForPickerUri(LocalCallingIdentity callingIdentity) { 408 return callingIdentity.hasPermission(LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED); 409 } 410 411 @VisibleForTesting getWhereForMediaTypeMatch(int mediaType)412 static String getWhereForMediaTypeMatch(int mediaType) { 413 return bindSelection("media_type=?", mediaType); 414 } 415 416 @VisibleForTesting getWhereForExternalPrimaryMatch()417 static String getWhereForExternalPrimaryMatch() { 418 return bindSelection("volume_name=?", MediaStore.VOLUME_EXTERNAL_PRIMARY); 419 } 420 getWhereForUserSelectedMatch( @onNull LocalCallingIdentity callingIdentity, String id)421 private static String getWhereForUserSelectedMatch( 422 @NonNull LocalCallingIdentity callingIdentity, String id) { 423 424 return String.format( 425 "%s IN (SELECT file_id from media_grants WHERE %s AND %s)", 426 id, 427 getWhereForOwnerPackageMatch(callingIdentity), 428 getWhereForUserIdMatch(callingIdentity)); 429 } 430 getWhereClauseForLatestUserSelection( @onNull LocalCallingIdentity callingIdentity, String id)431 private static String getWhereClauseForLatestUserSelection( 432 @NonNull LocalCallingIdentity callingIdentity, String id) { 433 return String.format("%s IN (SELECT file_id from media_grants WHERE generation_granted = " 434 + "(SELECT MAX(generation_granted) from media_grants WHERE %s AND %s))", 435 id, 436 getWhereForOwnerPackageMatch(callingIdentity), 437 getWhereForUserIdMatch(callingIdentity)); 438 } 439 440 /** 441 * @see MediaProvider#INCLUDED_DEFAULT_DIRECTORIES 442 */ 443 @Nullable getWhereForDefaultDirectoryMatch(@onNull Bundle extras)444 private static String getWhereForDefaultDirectoryMatch(@NonNull Bundle extras) { 445 final ArrayList<String> includedDefaultDirs = extras.getStringArrayList( 446 INCLUDED_DEFAULT_DIRECTORIES); 447 final ArrayList<String> options = new ArrayList<>(); 448 if (includedDefaultDirs != null) { 449 for (String defaultDir : includedDefaultDirs) { 450 options.add(FileColumns.RELATIVE_PATH + " LIKE '" + defaultDir + "/%'"); 451 } 452 } 453 454 if (options.size() > 0) { 455 return TextUtils.join(" OR ", options); 456 } 457 return null; 458 } 459 } 460