• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.telephony;
18 
19 import android.Manifest;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.UserIdInt;
23 import android.app.ActivityManager;
24 import android.app.AppOpsManager;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.location.LocationManager;
28 import android.os.Binder;
29 import android.os.Build;
30 import android.os.Process;
31 import android.os.UserHandle;
32 import android.util.Log;
33 import android.widget.Toast;
34 
35 import com.android.internal.telephony.TelephonyPermissions;
36 import com.android.internal.telephony.flags.Flags;
37 import com.android.internal.telephony.util.TelephonyUtils;
38 
39 /**
40  * Helper for performing location access checks.
41  * @hide
42  */
43 public final class LocationAccessPolicy {
44     private static final String TAG = "LocationAccessPolicy";
45     private static final boolean DBG = false;
46     public static final int MAX_SDK_FOR_ANY_ENFORCEMENT = Build.VERSION_CODES.CUR_DEVELOPMENT;
47 
48     public enum LocationPermissionResult {
49         ALLOWED,
50         /**
51          * Indicates that the denial is due to a transient device state
52          * (e.g. app-ops, location main switch)
53          */
54         DENIED_SOFT,
55         /**
56          * Indicates that the denial is due to a misconfigured app (e.g. missing entry in manifest)
57          */
58         DENIED_HARD,
59     }
60 
61     /** Data structure for location permission query */
62     public static class LocationPermissionQuery {
63         public final String callingPackage;
64         public final String callingFeatureId;
65         public final int callingUid;
66         public final int callingPid;
67         public final int minSdkVersionForCoarse;
68         public final int minSdkVersionForFine;
69         public final boolean logAsInfo;
70         public final String method;
71 
LocationPermissionQuery(String callingPackage, @Nullable String callingFeatureId, int callingUid, int callingPid, int minSdkVersionForCoarse, int minSdkVersionForFine, boolean logAsInfo, String method)72         private LocationPermissionQuery(String callingPackage, @Nullable String callingFeatureId,
73                 int callingUid, int callingPid, int minSdkVersionForCoarse,
74                 int minSdkVersionForFine, boolean logAsInfo, String method) {
75             this.callingPackage = callingPackage;
76             this.callingFeatureId = callingFeatureId;
77             this.callingUid = callingUid;
78             this.callingPid = callingPid;
79             this.minSdkVersionForCoarse = minSdkVersionForCoarse;
80             this.minSdkVersionForFine = minSdkVersionForFine;
81             this.logAsInfo = logAsInfo;
82             this.method = method;
83         }
84 
85         /** Builder for LocationPermissionQuery */
86         public static class Builder {
87             private String mCallingPackage;
88             private String mCallingFeatureId;
89             private int mCallingUid;
90             private int mCallingPid;
91             private int mMinSdkVersionForCoarse = -1;
92             private int mMinSdkVersionForFine = -1;
93             private int mMinSdkVersionForEnforcement = -1;
94             private boolean mLogAsInfo = false;
95             private String mMethod;
96 
97             /**
98              * Mandatory parameter, used for performing permission checks.
99              */
setCallingPackage(String callingPackage)100             public Builder setCallingPackage(String callingPackage) {
101                 mCallingPackage = callingPackage;
102                 return this;
103             }
104 
105             /**
106              * Mandatory parameter, used for performing permission checks.
107              */
setCallingFeatureId(@ullable String callingFeatureId)108             public Builder setCallingFeatureId(@Nullable String callingFeatureId) {
109                 mCallingFeatureId = callingFeatureId;
110                 return this;
111             }
112 
113             /**
114              * Mandatory parameter, used for performing permission checks.
115              */
setCallingUid(int callingUid)116             public Builder setCallingUid(int callingUid) {
117                 mCallingUid = callingUid;
118                 return this;
119             }
120 
121             /**
122              * Mandatory parameter, used for performing permission checks.
123              */
setCallingPid(int callingPid)124             public Builder setCallingPid(int callingPid) {
125                 mCallingPid = callingPid;
126                 return this;
127             }
128 
129             /**
130              * Apps that target at least this sdk version will be checked for coarse location
131              * permission. This method MUST be called before calling {@link #build()}. Otherwise, an
132              * {@link IllegalArgumentException} will be thrown.
133              *
134              * Additionally, if both the argument to this method and
135              * {@link #setMinSdkVersionForFine} are greater than {@link Build.VERSION_CODES#BASE},
136              * you must call {@link #setMinSdkVersionForEnforcement} with the min of the two to
137              * affirm that you do not want any location checks below a certain SDK version.
138              * Otherwise, {@link #build} will throw an {@link IllegalArgumentException}.
139              */
setMinSdkVersionForCoarse( int minSdkVersionForCoarse)140             public Builder setMinSdkVersionForCoarse(
141                     int minSdkVersionForCoarse) {
142                 mMinSdkVersionForCoarse = minSdkVersionForCoarse;
143                 return this;
144             }
145 
146             /**
147              * Apps that target at least this sdk version will be checked for fine location
148              * permission.  This method MUST be called before calling {@link #build()}.
149              * Otherwise, an {@link IllegalArgumentException} will be thrown.
150              *
151              * Additionally, if both the argument to this method and
152              * {@link #setMinSdkVersionForCoarse} are greater than {@link Build.VERSION_CODES#BASE},
153              * you must call {@link #setMinSdkVersionForEnforcement} with the min of the two to
154              * affirm that you do not want any location checks below a certain SDK version.
155              * Otherwise, {@link #build} will throw an {@link IllegalArgumentException}.
156              */
setMinSdkVersionForFine( int minSdkVersionForFine)157             public Builder setMinSdkVersionForFine(
158                     int minSdkVersionForFine) {
159                 mMinSdkVersionForFine = minSdkVersionForFine;
160                 return this;
161             }
162 
163             /**
164              * If both the argument to {@link #setMinSdkVersionForFine} and
165              * {@link #setMinSdkVersionForCoarse} are greater than {@link Build.VERSION_CODES#BASE},
166              * this method must be called with the min of the two to
167              * affirm that you do not want any location checks below a certain SDK version.
168              */
setMinSdkVersionForEnforcement(int minSdkVersionForEnforcement)169             public Builder setMinSdkVersionForEnforcement(int minSdkVersionForEnforcement) {
170                 mMinSdkVersionForEnforcement = minSdkVersionForEnforcement;
171                 return this;
172             }
173 
174             /**
175              * Optional, for logging purposes only.
176              */
setMethod(String method)177             public Builder setMethod(String method) {
178                 mMethod = method;
179                 return this;
180             }
181 
182             /**
183              * If called with {@code true}, log messages will only be printed at the info level.
184              */
setLogAsInfo(boolean logAsInfo)185             public Builder setLogAsInfo(boolean logAsInfo) {
186                 mLogAsInfo = logAsInfo;
187                 return this;
188             }
189 
190             /** build LocationPermissionQuery */
build()191             public LocationPermissionQuery build() {
192                 if (mMinSdkVersionForCoarse < 0 || mMinSdkVersionForFine < 0) {
193                     throw new IllegalArgumentException("Must specify min sdk versions for"
194                             + " enforcement for both coarse and fine permissions");
195                 }
196                 if (mMinSdkVersionForFine > Build.VERSION_CODES.BASE
197                         && mMinSdkVersionForCoarse > Build.VERSION_CODES.BASE) {
198                     if (mMinSdkVersionForEnforcement != Math.min(
199                             mMinSdkVersionForCoarse, mMinSdkVersionForFine)) {
200                         throw new IllegalArgumentException("setMinSdkVersionForEnforcement must be"
201                                 + " called.");
202                     }
203                 }
204 
205                 if (mMinSdkVersionForFine < mMinSdkVersionForCoarse) {
206                     throw new IllegalArgumentException("Since fine location permission includes"
207                             + " access to coarse location, the min sdk level for enforcement of"
208                             + " the fine location permission must not be less than the min sdk"
209                             + " level for enforcement of the coarse location permission.");
210                 }
211 
212                 return new LocationPermissionQuery(mCallingPackage, mCallingFeatureId,
213                         mCallingUid, mCallingPid, mMinSdkVersionForCoarse, mMinSdkVersionForFine,
214                         mLogAsInfo, mMethod);
215             }
216         }
217     }
218 
logError(Context context, LocationPermissionQuery query, String errorMsg)219     private static void logError(Context context, LocationPermissionQuery query, String errorMsg) {
220         if (query.logAsInfo) {
221             Log.i(TAG, errorMsg);
222             return;
223         }
224         Log.e(TAG, errorMsg);
225         try {
226             if (TelephonyUtils.IS_DEBUGGABLE) {
227                 Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show();
228             }
229         } catch (Throwable t) {
230             // whatever, not important
231         }
232     }
233 
appOpsModeToPermissionResult(int appOpsMode)234     private static LocationPermissionResult appOpsModeToPermissionResult(int appOpsMode) {
235         switch (appOpsMode) {
236             case AppOpsManager.MODE_ALLOWED:
237                 return LocationPermissionResult.ALLOWED;
238             case AppOpsManager.MODE_ERRORED:
239                 return LocationPermissionResult.DENIED_HARD;
240             default:
241                 return LocationPermissionResult.DENIED_SOFT;
242         }
243     }
244 
getAppOpsString(String manifestPermission)245     private static String getAppOpsString(String manifestPermission) {
246         switch (manifestPermission) {
247             case Manifest.permission.ACCESS_FINE_LOCATION:
248                 return AppOpsManager.OPSTR_FINE_LOCATION;
249             case Manifest.permission.ACCESS_COARSE_LOCATION:
250                 return AppOpsManager.OPSTR_COARSE_LOCATION;
251             default:
252                 return null;
253         }
254     }
255 
checkAppLocationPermissionHelper(Context context, LocationPermissionQuery query, String permissionToCheck)256     private static LocationPermissionResult checkAppLocationPermissionHelper(Context context,
257             LocationPermissionQuery query, String permissionToCheck) {
258         String locationTypeForLog =
259                 Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck)
260                         ? "fine" : "coarse";
261 
262         // Do the app-ops and the manifest check without any of the allow-overrides first.
263         boolean hasManifestPermission = checkManifestPermission(context, query.callingPid,
264                 query.callingUid, permissionToCheck);
265 
266         if (hasManifestPermission) {
267             // Only check the app op if the app has the permission.
268             int appOpMode = context.getSystemService(AppOpsManager.class)
269                     .noteOpNoThrow(getAppOpsString(permissionToCheck), query.callingUid,
270                             query.callingPackage, query.callingFeatureId, null);
271             if (appOpMode == AppOpsManager.MODE_ALLOWED) {
272                 // If the app did everything right, return without logging.
273                 return LocationPermissionResult.ALLOWED;
274             } else {
275                 // If the app has the manifest permission but not the app-op permission, it means
276                 // that it's aware of the requirement and the user denied permission explicitly.
277                 // If we see this, don't let any of the overrides happen.
278                 Log.i(TAG, query.callingPackage + " is aware of " + locationTypeForLog + " but the"
279                         + " app-ops permission is specifically denied.");
280                 return appOpsModeToPermissionResult(appOpMode);
281             }
282         }
283 
284         int minSdkVersion = Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck)
285                 ? query.minSdkVersionForFine : query.minSdkVersionForCoarse;
286 
287         UserHandle callingUserHandle = UserHandle.getUserHandleForUid(query.callingUid);
288 
289         // If the app fails for some reason, see if it should be allowed to proceed.
290         if (minSdkVersion > MAX_SDK_FOR_ANY_ENFORCEMENT) {
291             String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog
292                     + " because we're not enforcing API " + minSdkVersion + " yet."
293                     + " Please fix this app because it will break in the future. Called from "
294                     + query.method;
295             logError(context, query, errorMsg);
296             return null;
297         } else if (!isAppAtLeastSdkVersion(context, callingUserHandle, query.callingPackage,
298                 minSdkVersion)) {
299             String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog
300                     + " because it doesn't target API " + minSdkVersion + " yet."
301                     + " Please fix this app. Called from " + query.method;
302             logError(context, query, errorMsg);
303             return null;
304         } else {
305             // If we're not allowing it due to the above two conditions, this means that the app
306             // did not declare the permission in their manifest.
307             return LocationPermissionResult.DENIED_HARD;
308         }
309     }
310 
311     /** Check if location permissions have been granted */
checkLocationPermission( Context context, LocationPermissionQuery query)312     public static LocationPermissionResult checkLocationPermission(
313             Context context, LocationPermissionQuery query) {
314         // Always allow the phone process, system server, and network stack to access location.
315         // This avoid breaking legacy code that rely on public-facing APIs to access cell location,
316         // and it doesn't create an info leak risk because the cell location is stored in the phone
317         // process anyway, and the system server already has location access.
318         if (TelephonyPermissions.isSystemOrPhone(query.callingUid)
319                 || UserHandle.isSameApp(query.callingUid, Process.NETWORK_STACK_UID)
320                 || UserHandle.isSameApp(query.callingUid, Process.ROOT_UID)) {
321             return LocationPermissionResult.ALLOWED;
322         }
323 
324         // Check the system-wide requirements. If the location main switch is off and the caller is
325         // not in the allowlist of apps that always have loation access or the app's profile
326         // isn't in the foreground, return a soft denial.
327         if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid,
328                 query.callingPackage)) {
329             return LocationPermissionResult.DENIED_SOFT;
330         }
331 
332         // Do the check for fine, then for coarse.
333         if (query.minSdkVersionForFine < Integer.MAX_VALUE) {
334             LocationPermissionResult resultForFine = checkAppLocationPermissionHelper(
335                     context, query, Manifest.permission.ACCESS_FINE_LOCATION);
336             if (resultForFine != null) {
337                 return resultForFine;
338             }
339         }
340 
341         if (query.minSdkVersionForCoarse < Integer.MAX_VALUE) {
342             LocationPermissionResult resultForCoarse = checkAppLocationPermissionHelper(
343                     context, query, Manifest.permission.ACCESS_COARSE_LOCATION);
344             if (resultForCoarse != null) {
345                 return resultForCoarse;
346             }
347         }
348 
349         // At this point, we're out of location checks to do. If the app bypassed all the previous
350         // ones due to the SDK backwards compatibility schemes, allow it access.
351         return LocationPermissionResult.ALLOWED;
352     }
353 
checkManifestPermission(Context context, int pid, int uid, String permissionToCheck)354     private static boolean checkManifestPermission(Context context, int pid, int uid,
355             String permissionToCheck) {
356         return context.checkPermission(permissionToCheck, pid, uid)
357                 == PackageManager.PERMISSION_GRANTED;
358     }
359 
checkSystemLocationAccess(@onNull Context context, int uid, int pid, @NonNull String callingPackage)360     private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid,
361             @NonNull String callingPackage) {
362         if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier())
363                 && !isLocationBypassAllowed(context, callingPackage)) {
364             if (DBG) Log.w(TAG, "Location disabled, failed, (" + uid + ")");
365             return false;
366         }
367         // If the user or profile is current, permission is granted.
368         // Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission.
369         return isCurrentProfile(context, uid) || checkInteractAcrossUsersFull(context, pid, uid);
370     }
371 
372     /**
373      * @return Whether location is enabled for the given user.
374      */
isLocationModeEnabled(@onNull Context context, @UserIdInt int userId)375     public static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) {
376         LocationManager locationManager = context.getSystemService(LocationManager.class);
377         if (locationManager == null) {
378             Log.w(TAG, "Couldn't get location manager, denying location access");
379             return false;
380         }
381         return locationManager.isLocationEnabledForUser(UserHandle.of(userId));
382     }
383 
isLocationBypassAllowed(@onNull Context context, @NonNull String callingPackage)384     private static boolean isLocationBypassAllowed(@NonNull Context context,
385             @NonNull String callingPackage) {
386         for (String bypassPackage : getLocationBypassPackages(context)) {
387             if (callingPackage.equals(bypassPackage)) {
388                 return true;
389             }
390         }
391         return false;
392     }
393 
394     /**
395      * @return An array of packages that are always allowed to access location.
396      */
getLocationBypassPackages(@onNull Context context)397     public static @NonNull String[] getLocationBypassPackages(@NonNull Context context) {
398         return context.getResources().getStringArray(
399                 com.android.internal.R.array.config_serviceStateLocationAllowedPackages);
400     }
401 
checkInteractAcrossUsersFull( @onNull Context context, int pid, int uid)402     private static boolean checkInteractAcrossUsersFull(
403             @NonNull Context context, int pid, int uid) {
404         return checkManifestPermission(context, pid, uid,
405                 Manifest.permission.INTERACT_ACROSS_USERS_FULL);
406     }
407 
isCurrentProfile(@onNull Context context, int uid)408     private static boolean isCurrentProfile(@NonNull Context context, int uid) {
409         final long token = Binder.clearCallingIdentity();
410         try {
411             if (UserHandle.getUserHandleForUid(uid).getIdentifier()
412                     == ActivityManager.getCurrentUser()) {
413                 return true;
414             }
415             ActivityManager activityManager = context.getSystemService(ActivityManager.class);
416             if (activityManager != null) {
417                 return activityManager.isProfileForeground(
418                         UserHandle.getUserHandleForUid(ActivityManager.getCurrentUser()));
419             } else {
420                 return false;
421             }
422         } finally {
423             Binder.restoreCallingIdentity(token);
424         }
425     }
426 
isAppAtLeastSdkVersion(Context context, @NonNull UserHandle callingUserHandle, String pkgName, int sdkVersion)427     private static boolean isAppAtLeastSdkVersion(Context context,
428             @NonNull UserHandle callingUserHandle, String pkgName, int sdkVersion) {
429         try {
430             if (Flags.hsumPackageManager()) {
431                 if (context.getPackageManager().getApplicationInfoAsUser(
432                         pkgName, 0, callingUserHandle).targetSdkVersion >= sdkVersion) {
433                     return true;
434                 }
435             } else {
436                 if (context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion
437                         >= sdkVersion) {
438                     return true;
439                 }
440             }
441         } catch (PackageManager.NameNotFoundException e) {
442             // In case of exception, assume known app (more strict checking)
443             // Note: This case will never happen since checkPackage is
444             // called to verify validity before checking app's version.
445         }
446         return false;
447     }
448 }
449