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 constrained access. 211 * 212 * Where clause is generated based on the given collection type{@code uriType} and access 213 * permissions of the app. Generated where clause may include one or more combinations of 214 * below checks - 215 * * Match {@link MediaColumns#OWNER_PACKAGE_NAME} with calling package's package name. 216 * * Match ringtone or alarm or notification files to allow legacy use-cases 217 * * Match media files if app has corresponding read / write permissions on media files 218 * * Match files in primary storage if app has legacy write permissions 219 * * Match default directories in case of use-cases like System gallery 220 * 221 * This method assumes global access permission checks and full access checks for the collection 222 * is already checked. The method returns where clause assuming app doesn't have global access 223 * permission to the given collection type. 224 * 225 * @param callingIdentity {@link LocalCallingIdentity} of the caller to verify permission state 226 * @param uriType the collection info for which the requested access is, 227 * e.g., Images -> {@link MediaProvider}#IMAGES_MEDIA. 228 * @param forWrite type of the access requested. Read / write access to the file / collection. 229 * @param extras bundle containing {@link MediaProvider#INCLUDED_DEFAULT_DIRECTORIES} info if 230 * there is any. 231 */ 232 @NonNull getWhereForConstrainedAccess( @onNull LocalCallingIdentity callingIdentity, int uriType, boolean forWrite, @NonNull Bundle extras)233 public static String getWhereForConstrainedAccess( 234 @NonNull LocalCallingIdentity callingIdentity, int uriType, 235 boolean forWrite, @NonNull Bundle extras) { 236 switch (uriType) { 237 case AUDIO_MEDIA_ID: 238 case AUDIO_MEDIA: { 239 // Apps without Audio permission can only see their own 240 // media, but we also let them see ringtone-style media to 241 // support legacy use-cases. 242 return getWhereForOwnerPackageMatch(callingIdentity) 243 + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"; 244 } 245 case AUDIO_PLAYLISTS_ID: 246 case AUDIO_PLAYLISTS: 247 case IMAGES_MEDIA: 248 case IMAGES_MEDIA_ID: 249 case VIDEO_MEDIA_ID: 250 case VIDEO_MEDIA: { 251 return getWhereForOwnerPackageMatch(callingIdentity); 252 } 253 case AUDIO_ARTISTS_ID: 254 case AUDIO_ARTISTS: 255 case AUDIO_ARTISTS_ID_ALBUMS: 256 case AUDIO_ALBUMS_ID: 257 case AUDIO_ALBUMS: 258 case AUDIO_ALBUMART_ID: 259 case AUDIO_ALBUMART: 260 case AUDIO_GENRES_ID: 261 case AUDIO_GENRES: 262 case AUDIO_MEDIA_ID_GENRES_ID: 263 case AUDIO_MEDIA_ID_GENRES: 264 case AUDIO_GENRES_ID_MEMBERS: 265 case AUDIO_GENRES_ALL_MEMBERS: 266 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 267 case AUDIO_PLAYLISTS_ID_MEMBERS: { 268 // We don't have a great way to filter parsed metadata by 269 // owner, so callers need to hold READ_MEDIA_AUDIO 270 return NO_ACCESS_SQL; 271 } 272 case IMAGES_THUMBNAILS_ID: 273 case IMAGES_THUMBNAILS: { 274 return "image_id IN (SELECT _id FROM images WHERE " 275 + getWhereForOwnerPackageMatch(callingIdentity) + ")"; 276 } 277 case VIDEO_THUMBNAILS_ID: 278 case VIDEO_THUMBNAILS: { 279 return "video_id IN (SELECT _id FROM video WHERE " 280 + getWhereForOwnerPackageMatch(callingIdentity) + ")"; 281 } 282 case DOWNLOADS_ID: 283 case DOWNLOADS: { 284 final ArrayList<String> options = new ArrayList<>(); 285 // Allow access to owned files 286 options.add(getWhereForOwnerPackageMatch(callingIdentity)); 287 288 if (shouldAllowLegacyWrite(callingIdentity, forWrite)) { 289 // b/130766639: We're willing to let apps interact with well-defined MediaStore 290 // collections on secondary storage devices, but we continue to hold 291 // firm that any other legacy access to secondary storage devices must 292 // be read-only. 293 options.add(getWhereForExternalPrimaryMatch()); 294 } 295 296 return TextUtils.join(" OR ", options); 297 } 298 case FILES_ID: 299 case FILES: { 300 final ArrayList<String> options = new ArrayList<>(); 301 // Allow access to owned files 302 options.add(getWhereForOwnerPackageMatch(callingIdentity)); 303 304 if (shouldAllowLegacyWrite(callingIdentity, forWrite)) { 305 // b/130766639: We're willing to let apps interact with well-defined MediaStore 306 // collections on secondary storage devices, but we continue to hold 307 // firm that any other legacy access to secondary storage devices must 308 // be read-only. 309 options.add(getWhereForExternalPrimaryMatch()); 310 } 311 312 // Allow access to media files if the app has corresponding read/write media 313 // permission 314 if (hasAccessToCollection(callingIdentity, AUDIO_MEDIA, forWrite)) { 315 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_AUDIO)); 316 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_PLAYLIST)); 317 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_SUBTITLE)); 318 } 319 if (hasAccessToCollection(callingIdentity, VIDEO_MEDIA, forWrite)) { 320 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_VIDEO)); 321 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_SUBTITLE)); 322 } 323 if (hasAccessToCollection(callingIdentity, IMAGES_MEDIA, forWrite)) { 324 options.add(getWhereForMediaTypeMatch(MEDIA_TYPE_IMAGE)); 325 } 326 327 // Allow access to file in directories. This si particularly used only for 328 // SystemGallery use-case 329 final String defaultDirectorySql = getWhereForDefaultDirectoryMatch(extras); 330 if (defaultDirectorySql != null) { 331 options.add(defaultDirectorySql); 332 } 333 334 return TextUtils.join(" OR ", options); 335 } 336 default: 337 throw new UnsupportedOperationException( 338 "Unknown or unsupported type: " + uriType); 339 } 340 } 341 shouldAllowLegacyWrite(LocalCallingIdentity callingIdentity, boolean forWrite)342 private static boolean shouldAllowLegacyWrite(LocalCallingIdentity callingIdentity, 343 boolean forWrite) { 344 return forWrite && callingIdentity.isCallingPackageLegacyWrite(); 345 } 346 347 /** 348 * Returns where clause to match {@link MediaColumns#OWNER_PACKAGE_NAME} with package names of 349 * the given {@code callingIdentity} 350 */ getWhereForOwnerPackageMatch(LocalCallingIdentity callingIdentity)351 public static String getWhereForOwnerPackageMatch(LocalCallingIdentity callingIdentity) { 352 return OWNER_PACKAGE_NAME + " IN " + callingIdentity.getSharedPackagesAsString(); 353 } 354 355 /** 356 * Generates the where clause for a user_id media grant match. 357 * 358 * @param callingIdentity - the current caller. 359 * @return where clause to match {@link MediaGrants#PACKAGE_USER_ID_COLUMN} with user id of the 360 * given {@code callingIdentity} 361 */ getWhereForUserIdMatch(LocalCallingIdentity callingIdentity)362 public static String getWhereForUserIdMatch(LocalCallingIdentity callingIdentity) { 363 return PACKAGE_USER_ID_COLUMN + "=" + callingIdentity.uid / MediaStore.PER_USER_RANGE; 364 } 365 366 @VisibleForTesting getWhereForMediaTypeMatch(int mediaType)367 static String getWhereForMediaTypeMatch(int mediaType) { 368 return bindSelection("media_type=?", mediaType); 369 } 370 371 @VisibleForTesting getWhereForExternalPrimaryMatch()372 static String getWhereForExternalPrimaryMatch() { 373 return bindSelection("volume_name=?", MediaStore.VOLUME_EXTERNAL_PRIMARY); 374 } 375 getWhereForUserSelectedMatch( @onNull LocalCallingIdentity callingIdentity, String id)376 private static String getWhereForUserSelectedMatch( 377 @NonNull LocalCallingIdentity callingIdentity, String id) { 378 379 return String.format( 380 "%s IN (SELECT file_id from media_grants WHERE %s AND %s)", 381 id, 382 getWhereForOwnerPackageMatch(callingIdentity), 383 getWhereForUserIdMatch(callingIdentity)); 384 } 385 386 /** 387 * @see MediaProvider#INCLUDED_DEFAULT_DIRECTORIES 388 */ 389 @Nullable getWhereForDefaultDirectoryMatch(@onNull Bundle extras)390 private static String getWhereForDefaultDirectoryMatch(@NonNull Bundle extras) { 391 final ArrayList<String> includedDefaultDirs = extras.getStringArrayList( 392 INCLUDED_DEFAULT_DIRECTORIES); 393 final ArrayList<String> options = new ArrayList<>(); 394 if (includedDefaultDirs != null) { 395 for (String defaultDir : includedDefaultDirs) { 396 options.add(FileColumns.RELATIVE_PATH + " LIKE '" + defaultDir + "/%'"); 397 } 398 } 399 400 if (options.size() > 0) { 401 return TextUtils.join(" OR ", options); 402 } 403 return null; 404 } 405 } 406