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