• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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