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 /* forDataDelivery */ true); 112 } 113 case IMAGES_MEDIA: 114 case IMAGES_MEDIA_ID: 115 case IMAGES_THUMBNAILS_ID: 116 case IMAGES_THUMBNAILS: { 117 return callingIdentity.checkCallingPermissionImages(forWrite, 118 /* forDataDelivery */ true); 119 } 120 case VIDEO_MEDIA_ID: 121 case VIDEO_MEDIA: 122 case VIDEO_THUMBNAILS_ID: 123 case VIDEO_THUMBNAILS: { 124 return callingIdentity.checkCallingPermissionVideo(forWrite, 125 /* forDataDelivery */ true); 126 } 127 case DOWNLOADS_ID: 128 case DOWNLOADS: 129 case FILES_ID: 130 case FILES: { 131 // Allow apps with legacy read access to read all files. 132 return !forWrite 133 && callingIdentity.isCallingPackageLegacyRead(); 134 } 135 default: { 136 throw new UnsupportedOperationException( 137 "Unknown or unsupported type: " + uriType); 138 } 139 } 140 } 141 142 /** 143 * Returns {@code true} if the request is for read access to a collection that contains 144 * visual media files and app has READ_MEDIA_VISUAL_USER_SELECTED permission. 145 * 146 * @param callingIdentity {@link LocalCallingIdentity} of the caller to verify permission state 147 * @param uriType the collection info for which the requested access is, 148 * e.g., Images -> {@link MediaProvider}#IMAGES_MEDIA. 149 * @param forWrite type of the access requested. Read / write access to the file / collection. 150 */ hasUserSelectedAccess(@onNull LocalCallingIdentity callingIdentity, int uriType, boolean forWrite)151 public static boolean hasUserSelectedAccess(@NonNull LocalCallingIdentity callingIdentity, 152 int uriType, boolean forWrite) { 153 if (forWrite) { 154 // Apps only get read access via media_grants. For write access on user selected items, 155 // app needs to get uri grants. 156 return false; 157 } 158 159 switch (uriType) { 160 case IMAGES_MEDIA: 161 case IMAGES_MEDIA_ID: 162 case IMAGES_THUMBNAILS_ID: 163 case IMAGES_THUMBNAILS: 164 case VIDEO_MEDIA_ID: 165 case VIDEO_MEDIA: 166 case VIDEO_THUMBNAILS_ID: 167 case VIDEO_THUMBNAILS: 168 case DOWNLOADS_ID: 169 case DOWNLOADS: 170 case FILES_ID: 171 case FILES: { 172 return callingIdentity.checkCallingPermissionUserSelected( 173 /* forDataDelivery */ true); 174 } 175 default: return false; 176 } 177 } 178 179 /** 180 * Returns where clause for access on user selected permission. 181 * 182 * <p><strong>NOTE:</strong> This method assumes that app has necessary permissions and returns 183 * the where clause without checking any permission state of the app. 184 */ 185 @NonNull getWhereForUserSelectedAccess( @onNull LocalCallingIdentity callingIdentity, int uriType)186 public static String getWhereForUserSelectedAccess( 187 @NonNull LocalCallingIdentity callingIdentity, int uriType) { 188 switch (uriType) { 189 case IMAGES_MEDIA: 190 case IMAGES_MEDIA_ID: 191 case VIDEO_MEDIA_ID: 192 case VIDEO_MEDIA: 193 case DOWNLOADS_ID: 194 case DOWNLOADS: 195 case FILES_ID: 196 case FILES: { 197 return getWhereForUserSelectedMatch(callingIdentity, MediaColumns._ID); 198 } 199 case IMAGES_THUMBNAILS_ID: 200 case IMAGES_THUMBNAILS: { 201 return getWhereForUserSelectedMatch(callingIdentity, "image_id"); 202 } 203 case VIDEO_THUMBNAILS_ID: 204 case VIDEO_THUMBNAILS: { 205 return getWhereForUserSelectedMatch(callingIdentity, "video_id"); 206 } 207 default: 208 throw new UnsupportedOperationException( 209 "Unknown or unsupported type: " + uriType); 210 } 211 } 212 213 /** 214 * Returns where clause for access on user selected permission with filtering for latest 215 * selection only. 216 * 217 * <p><strong>NOTE:</strong> This method assumes that app has necessary permissions and returns 218 * the where clause without checking any permission state of the app. 219 */ 220 @NonNull getWhereForLatestSelection( @onNull LocalCallingIdentity callingIdentity, int uriType)221 public static String getWhereForLatestSelection( 222 @NonNull LocalCallingIdentity callingIdentity, int uriType) { 223 switch (uriType) { 224 case IMAGES_MEDIA: 225 case IMAGES_MEDIA_ID: 226 case VIDEO_MEDIA_ID: 227 case VIDEO_MEDIA: 228 case DOWNLOADS_ID: 229 case DOWNLOADS: 230 case FILES_ID: 231 case FILES: { 232 return getWhereClauseForLatestUserSelection(callingIdentity, MediaColumns._ID); 233 } 234 case IMAGES_THUMBNAILS_ID: 235 case IMAGES_THUMBNAILS: { 236 return getWhereClauseForLatestUserSelection(callingIdentity, "image_id"); 237 } 238 case VIDEO_THUMBNAILS_ID: 239 case VIDEO_THUMBNAILS: { 240 return getWhereClauseForLatestUserSelection(callingIdentity, "video_id"); 241 } 242 default: 243 throw new UnsupportedOperationException( 244 "Unknown or unsupported type: " + uriType); 245 } 246 } 247 248 /** 249 * Returns where clause for constrained access. 250 * 251 * Where clause is generated based on the given collection type{@code uriType} and access 252 * permissions of the app. Generated where clause may include one or more combinations of 253 * below checks - 254 * * Match {@link MediaColumns#OWNER_PACKAGE_NAME} with calling package's package name. 255 * * Match ringtone or alarm or notification files to allow legacy use-cases 256 * * Match media files if app has corresponding read / write permissions on media files 257 * * Match files in primary storage if app has legacy write permissions 258 * * Match default directories in case of use-cases like System gallery 259 * 260 * This method assumes global access permission checks and full access checks for the collection 261 * is already checked. The method returns where clause assuming app doesn't have global access 262 * permission to the given collection type. 263 * 264 * @param callingIdentity {@link LocalCallingIdentity} of the caller to verify permission state 265 * @param uriType the collection info for which the requested access is, 266 * e.g., Images -> {@link MediaProvider}#IMAGES_MEDIA. 267 * @param forWrite type of the access requested. Read / write access to the file / collection. 268 * @param extras bundle containing {@link MediaProvider#INCLUDED_DEFAULT_DIRECTORIES} info if 269 * there is any. 270 */ 271 @NonNull getWhereForConstrainedAccess( @onNull LocalCallingIdentity callingIdentity, int uriType, boolean forWrite, @NonNull Bundle extras)272 public static String getWhereForConstrainedAccess( 273 @NonNull LocalCallingIdentity callingIdentity, int uriType, 274 boolean forWrite, @NonNull Bundle extras) { 275 switch (uriType) { 276 case AUDIO_MEDIA_ID: 277 case AUDIO_MEDIA: { 278 // Apps without Audio permission can only see their own 279 // media, but we also let them see ringtone-style media to 280 // support legacy use-cases. 281 return getWhereForOwnerPackageMatch(callingIdentity) 282 + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"; 283 } 284 case AUDIO_PLAYLISTS_ID: 285 case AUDIO_PLAYLISTS: 286 case IMAGES_MEDIA: 287 case IMAGES_MEDIA_ID: 288 case VIDEO_MEDIA_ID: 289 case VIDEO_MEDIA: { 290 return getWhereForOwnerPackageMatch(callingIdentity); 291 } 292 case AUDIO_ARTISTS_ID: 293 case AUDIO_ARTISTS: 294 case AUDIO_ARTISTS_ID_ALBUMS: 295 case AUDIO_ALBUMS_ID: 296 case AUDIO_ALBUMS: 297 case AUDIO_ALBUMART_ID: 298 case AUDIO_ALBUMART: 299 case AUDIO_GENRES_ID: 300 case AUDIO_GENRES: 301 case AUDIO_MEDIA_ID_GENRES_ID: 302 case AUDIO_MEDIA_ID_GENRES: 303 case AUDIO_GENRES_ID_MEMBERS: 304 case AUDIO_GENRES_ALL_MEMBERS: 305 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 306 case AUDIO_PLAYLISTS_ID_MEMBERS: { 307 // We don't have a great way to filter parsed metadata by 308 // owner, so callers need to hold READ_MEDIA_AUDIO 309 return NO_ACCESS_SQL; 310 } 311 case IMAGES_THUMBNAILS_ID: 312 case IMAGES_THUMBNAILS: { 313 return "image_id IN (SELECT _id FROM images WHERE " 314 + getWhereForOwnerPackageMatch(callingIdentity) + ")"; 315 } 316 case VIDEO_THUMBNAILS_ID: 317 case VIDEO_THUMBNAILS: { 318 return "video_id IN (SELECT _id FROM video WHERE " 319 + getWhereForOwnerPackageMatch(callingIdentity) + ")"; 320 } 321 case DOWNLOADS_ID: 322 case DOWNLOADS: { 323 final ArrayList<String> options = new ArrayList<>(); 324 // Allow access to owned files 325 options.add(getWhereForOwnerPackageMatch(callingIdentity)); 326 327 if (shouldAllowLegacyWrite(callingIdentity, forWrite)) { 328 // b/130766639: We're willing to let apps interact with well-defined MediaStore 329 // collections on secondary storage devices, but we continue to hold 330 // firm that any other legacy access to secondary storage devices must 331 // be read-only. 332 options.add(getWhereForExternalPrimaryMatch()); 333 } 334 335 return TextUtils.join(" OR ", options); 336 } 337 case FILES_ID: 338 case FILES: { 339 final ArrayList<String> options = new ArrayList<>(); 340 // Allow access to owned files 341 options.add(getWhereForOwnerPackageMatch(callingIdentity)); 342 343 if (shouldAllowLegacyWrite(callingIdentity, forWrite)) { 344 // b/130766639: We're willing to let apps interact with well-defined MediaStore 345 // collections on secondary storage devices, but we continue to hold 346 // firm that any other legacy access to secondary storage devices must 347 // be read-only. 348 options.add(getWhereForExternalPrimaryMatch()); 349 } 350 351 // Allow access to media files if the app has corresponding read/write media 352 // permission 353 if (hasAccessToCollection(callingIdentity, AUDIO_MEDIA, forWrite)) { 354 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_AUDIO)); 355 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_PLAYLIST)); 356 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_SUBTITLE)); 357 } 358 if (hasAccessToCollection(callingIdentity, VIDEO_MEDIA, forWrite)) { 359 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_VIDEO)); 360 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_SUBTITLE)); 361 } 362 if (hasAccessToCollection(callingIdentity, IMAGES_MEDIA, forWrite)) { 363 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_IMAGE)); 364 } 365 366 // Allow access to file in directories. This si particularly used only for 367 // SystemGallery use-case 368 final String defaultDirectorySql = getWhereForDefaultDirectoryMatch(extras); 369 if (defaultDirectorySql != null) { 370 options.add(defaultDirectorySql); 371 } 372 373 return TextUtils.join(" OR ", options); 374 } 375 default: 376 throw new UnsupportedOperationException( 377 "Unknown or unsupported type: " + uriType); 378 } 379 } 380 shouldAllowLegacyWrite(LocalCallingIdentity callingIdentity, boolean forWrite)381 private static boolean shouldAllowLegacyWrite(LocalCallingIdentity callingIdentity, 382 boolean forWrite) { 383 return forWrite && callingIdentity.isCallingPackageLegacyWrite(); 384 } 385 386 /** 387 * Returns where clause to match {@link MediaColumns#OWNER_PACKAGE_NAME} with package names of 388 * the given {@code callingIdentity} 389 */ getWhereForOwnerPackageMatch(LocalCallingIdentity callingIdentity)390 public static String getWhereForOwnerPackageMatch(LocalCallingIdentity callingIdentity) { 391 return OWNER_PACKAGE_NAME + " IN " + callingIdentity.getSharedPackagesAsString(); 392 } 393 394 /** 395 * Generates the where clause for a user_id media grant match. 396 * 397 * @param callingIdentity - the current caller. 398 * @return where clause to match {@link MediaGrants#PACKAGE_USER_ID_COLUMN} with user id of the 399 * given {@code callingIdentity} 400 */ getWhereForUserIdMatch(LocalCallingIdentity callingIdentity)401 public static String getWhereForUserIdMatch(LocalCallingIdentity callingIdentity) { 402 return PACKAGE_USER_ID_COLUMN + "=" + callingIdentity.uid / MediaStore.PER_USER_RANGE; 403 } 404 405 /** 406 * Returns true if redaction is needed for openFile calls on picker uri by checking calling 407 * package permission 408 * 409 * @param callingIdentity - the current caller 410 */ isRedactionNeededForPickerUri(LocalCallingIdentity callingIdentity)411 public static boolean isRedactionNeededForPickerUri(LocalCallingIdentity callingIdentity) { 412 return callingIdentity.hasPermission(LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED); 413 } 414 415 @VisibleForTesting getWhereForMediaTypeMatch(int mediaType)416 static String getWhereForMediaTypeMatch(int mediaType) { 417 return bindSelection("media_type=?", mediaType); 418 } 419 420 @VisibleForTesting getWhereForExternalPrimaryMatch()421 static String getWhereForExternalPrimaryMatch() { 422 return bindSelection("volume_name=?", MediaStore.VOLUME_EXTERNAL_PRIMARY); 423 } 424 getWhereForUserSelectedMatch( @onNull LocalCallingIdentity callingIdentity, String id)425 private static String getWhereForUserSelectedMatch( 426 @NonNull LocalCallingIdentity callingIdentity, String id) { 427 428 return String.format( 429 "%s IN (SELECT file_id from media_grants WHERE %s AND %s)", 430 id, 431 getWhereForOwnerPackageMatch(callingIdentity), 432 getWhereForUserIdMatch(callingIdentity)); 433 } 434 getWhereClauseForLatestUserSelection( @onNull LocalCallingIdentity callingIdentity, String id)435 private static String getWhereClauseForLatestUserSelection( 436 @NonNull LocalCallingIdentity callingIdentity, String id) { 437 return String.format("%s IN (SELECT file_id from media_grants WHERE generation_granted = " 438 + "(SELECT MAX(generation_granted) from media_grants WHERE %s AND %s))", 439 id, 440 getWhereForOwnerPackageMatch(callingIdentity), 441 getWhereForUserIdMatch(callingIdentity)); 442 } 443 444 /** 445 * @see MediaProvider#INCLUDED_DEFAULT_DIRECTORIES 446 */ 447 @Nullable getWhereForDefaultDirectoryMatch(@onNull Bundle extras)448 private static String getWhereForDefaultDirectoryMatch(@NonNull Bundle extras) { 449 final ArrayList<String> includedDefaultDirs = extras.getStringArrayList( 450 INCLUDED_DEFAULT_DIRECTORIES); 451 final ArrayList<String> options = new ArrayList<>(); 452 if (includedDefaultDirs != null) { 453 for (String defaultDir : includedDefaultDirs) { 454 options.add(FileColumns.RELATIVE_PATH + " LIKE '" + defaultDir + "/%'"); 455 } 456 } 457 458 if (options.size() > 0) { 459 return TextUtils.join(" OR ", options); 460 } 461 return null; 462 } 463 } 464