• 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             }
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