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.util.TelephonyUtils;
36 
37 /**
38  * Helper for performing location access checks.
39  * @hide
40  */
41 public final class LocationAccessPolicy {
42     private static final String TAG = "LocationAccessPolicy";
43     private static final boolean DBG = false;
44     public static final int MAX_SDK_FOR_ANY_ENFORCEMENT = Build.VERSION_CODES.CUR_DEVELOPMENT;
45 
46     public enum LocationPermissionResult {
47         ALLOWED,
48         /**
49          * Indicates that the denial is due to a transient device state
50          * (e.g. app-ops, location master switch)
51          */
52         DENIED_SOFT,
53         /**
54          * Indicates that the denial is due to a misconfigured app (e.g. missing entry in manifest)
55          */
56         DENIED_HARD,
57     }
58 
59     /** Data structure for location permission query */
60     public static class LocationPermissionQuery {
61         public final String callingPackage;
62         public final String callingFeatureId;
63         public final int callingUid;
64         public final int callingPid;
65         public final int minSdkVersionForCoarse;
66         public final int minSdkVersionForFine;
67         public final boolean logAsInfo;
68         public final String method;
69 
LocationPermissionQuery(String callingPackage, @Nullable String callingFeatureId, int callingUid, int callingPid, int minSdkVersionForCoarse, int minSdkVersionForFine, boolean logAsInfo, String method)70         private LocationPermissionQuery(String callingPackage, @Nullable String callingFeatureId,
71                 int callingUid, int callingPid, int minSdkVersionForCoarse,
72                 int minSdkVersionForFine, boolean logAsInfo, String method) {
73             this.callingPackage = callingPackage;
74             this.callingFeatureId = callingFeatureId;
75             this.callingUid = callingUid;
76             this.callingPid = callingPid;
77             this.minSdkVersionForCoarse = minSdkVersionForCoarse;
78             this.minSdkVersionForFine = minSdkVersionForFine;
79             this.logAsInfo = logAsInfo;
80             this.method = method;
81         }
82 
83         /** Builder for LocationPermissionQuery */
84         public static class Builder {
85             private String mCallingPackage;
86             private String mCallingFeatureId;
87             private int mCallingUid;
88             private int mCallingPid;
89             private int mMinSdkVersionForCoarse = Integer.MAX_VALUE;
90             private int mMinSdkVersionForFine = Integer.MAX_VALUE;
91             private boolean mLogAsInfo = false;
92             private String mMethod;
93 
94             /**
95              * Mandatory parameter, used for performing permission checks.
96              */
setCallingPackage(String callingPackage)97             public Builder setCallingPackage(String callingPackage) {
98                 mCallingPackage = callingPackage;
99                 return this;
100             }
101 
102             /**
103              * Mandatory parameter, used for performing permission checks.
104              */
setCallingFeatureId(@ullable String callingFeatureId)105             public Builder setCallingFeatureId(@Nullable String callingFeatureId) {
106                 mCallingFeatureId = callingFeatureId;
107                 return this;
108             }
109 
110             /**
111              * Mandatory parameter, used for performing permission checks.
112              */
setCallingUid(int callingUid)113             public Builder setCallingUid(int callingUid) {
114                 mCallingUid = callingUid;
115                 return this;
116             }
117 
118             /**
119              * Mandatory parameter, used for performing permission checks.
120              */
setCallingPid(int callingPid)121             public Builder setCallingPid(int callingPid) {
122                 mCallingPid = callingPid;
123                 return this;
124             }
125 
126             /**
127              * Apps that target at least this sdk version will be checked for coarse location
128              * permission. Defaults to INT_MAX (which means don't check)
129              */
setMinSdkVersionForCoarse( int minSdkVersionForCoarse)130             public Builder setMinSdkVersionForCoarse(
131                     int minSdkVersionForCoarse) {
132                 mMinSdkVersionForCoarse = minSdkVersionForCoarse;
133                 return this;
134             }
135 
136             /**
137              * Apps that target at least this sdk version will be checked for fine location
138              * permission. Defaults to INT_MAX (which means don't check)
139              */
setMinSdkVersionForFine( int minSdkVersionForFine)140             public Builder setMinSdkVersionForFine(
141                     int minSdkVersionForFine) {
142                 mMinSdkVersionForFine = minSdkVersionForFine;
143                 return this;
144             }
145 
146             /**
147              * Optional, for logging purposes only.
148              */
setMethod(String method)149             public Builder setMethod(String method) {
150                 mMethod = method;
151                 return this;
152             }
153 
154             /**
155              * If called with {@code true}, log messages will only be printed at the info level.
156              */
setLogAsInfo(boolean logAsInfo)157             public Builder setLogAsInfo(boolean logAsInfo) {
158                 mLogAsInfo = logAsInfo;
159                 return this;
160             }
161 
162             /** build LocationPermissionQuery */
build()163             public LocationPermissionQuery build() {
164                 return new LocationPermissionQuery(mCallingPackage, mCallingFeatureId,
165                         mCallingUid, mCallingPid, mMinSdkVersionForCoarse, mMinSdkVersionForFine,
166                         mLogAsInfo, mMethod);
167             }
168         }
169     }
170 
logError(Context context, LocationPermissionQuery query, String errorMsg)171     private static void logError(Context context, LocationPermissionQuery query, String errorMsg) {
172         if (query.logAsInfo) {
173             Log.i(TAG, errorMsg);
174             return;
175         }
176         Log.e(TAG, errorMsg);
177         try {
178             if (TelephonyUtils.IS_DEBUGGABLE) {
179                 Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show();
180             }
181         } catch (Throwable t) {
182             // whatever, not important
183         }
184     }
185 
appOpsModeToPermissionResult(int appOpsMode)186     private static LocationPermissionResult appOpsModeToPermissionResult(int appOpsMode) {
187         switch (appOpsMode) {
188             case AppOpsManager.MODE_ALLOWED:
189                 return LocationPermissionResult.ALLOWED;
190             case AppOpsManager.MODE_ERRORED:
191                 return LocationPermissionResult.DENIED_HARD;
192             default:
193                 return LocationPermissionResult.DENIED_SOFT;
194         }
195     }
196 
getAppOpsString(String manifestPermission)197     private static String getAppOpsString(String manifestPermission) {
198         switch (manifestPermission) {
199             case Manifest.permission.ACCESS_FINE_LOCATION:
200                 return AppOpsManager.OPSTR_FINE_LOCATION;
201             case Manifest.permission.ACCESS_COARSE_LOCATION:
202                 return AppOpsManager.OPSTR_COARSE_LOCATION;
203             default:
204                 return null;
205         }
206     }
207 
checkAppLocationPermissionHelper(Context context, LocationPermissionQuery query, String permissionToCheck)208     private static LocationPermissionResult checkAppLocationPermissionHelper(Context context,
209             LocationPermissionQuery query, String permissionToCheck) {
210         String locationTypeForLog =
211                 Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck)
212                         ? "fine" : "coarse";
213 
214         // Do the app-ops and the manifest check without any of the allow-overrides first.
215         boolean hasManifestPermission = checkManifestPermission(context, query.callingPid,
216                 query.callingUid, permissionToCheck);
217 
218         if (hasManifestPermission) {
219             // Only check the app op if the app has the permission.
220             int appOpMode = context.getSystemService(AppOpsManager.class)
221                     .noteOpNoThrow(getAppOpsString(permissionToCheck), query.callingUid,
222                             query.callingPackage, query.callingFeatureId, null);
223             if (appOpMode == AppOpsManager.MODE_ALLOWED) {
224                 // If the app did everything right, return without logging.
225                 return LocationPermissionResult.ALLOWED;
226             } else {
227                 // If the app has the manifest permission but not the app-op permission, it means
228                 // that it's aware of the requirement and the user denied permission explicitly.
229                 // If we see this, don't let any of the overrides happen.
230                 Log.i(TAG, query.callingPackage + " is aware of " + locationTypeForLog + " but the"
231                         + " app-ops permission is specifically denied.");
232                 return appOpsModeToPermissionResult(appOpMode);
233             }
234         }
235 
236         int minSdkVersion = Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck)
237                 ? query.minSdkVersionForFine : query.minSdkVersionForCoarse;
238 
239         // If the app fails for some reason, see if it should be allowed to proceed.
240         if (minSdkVersion > MAX_SDK_FOR_ANY_ENFORCEMENT) {
241             String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog
242                     + " because we're not enforcing API " + minSdkVersion + " yet."
243                     + " Please fix this app because it will break in the future. Called from "
244                     + query.method;
245             logError(context, query, errorMsg);
246             return null;
247         } else if (!isAppAtLeastSdkVersion(context, query.callingPackage, minSdkVersion)) {
248             String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog
249                     + " because it doesn't target API " + minSdkVersion + " yet."
250                     + " Please fix this app. Called from " + query.method;
251             logError(context, query, errorMsg);
252             return null;
253         } else {
254             // If we're not allowing it due to the above two conditions, this means that the app
255             // did not declare the permission in their manifest.
256             return LocationPermissionResult.DENIED_HARD;
257         }
258     }
259 
260     /** Check if location permissions have been granted */
checkLocationPermission( Context context, LocationPermissionQuery query)261     public static LocationPermissionResult checkLocationPermission(
262             Context context, LocationPermissionQuery query) {
263         // Always allow the phone process, system server, and network stack to access location.
264         // This avoid breaking legacy code that rely on public-facing APIs to access cell location,
265         // and it doesn't create an info leak risk because the cell location is stored in the phone
266         // process anyway, and the system server already has location access.
267         if (query.callingUid == Process.PHONE_UID || query.callingUid == Process.SYSTEM_UID
268                 || query.callingUid == Process.NETWORK_STACK_UID
269                 || query.callingUid == Process.ROOT_UID) {
270             return LocationPermissionResult.ALLOWED;
271         }
272 
273         // Check the system-wide requirements. If the location master switch is off or
274         // the app's profile isn't in foreground, return a soft denial.
275         if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid)) {
276             return LocationPermissionResult.DENIED_SOFT;
277         }
278 
279         // Do the check for fine, then for coarse.
280         if (query.minSdkVersionForFine < Integer.MAX_VALUE) {
281             LocationPermissionResult resultForFine = checkAppLocationPermissionHelper(
282                     context, query, Manifest.permission.ACCESS_FINE_LOCATION);
283             if (resultForFine != null) {
284                 return resultForFine;
285             }
286         }
287 
288         if (query.minSdkVersionForCoarse < Integer.MAX_VALUE) {
289             LocationPermissionResult resultForCoarse = checkAppLocationPermissionHelper(
290                     context, query, Manifest.permission.ACCESS_COARSE_LOCATION);
291             if (resultForCoarse != null) {
292                 return resultForCoarse;
293             }
294         }
295 
296         // At this point, we're out of location checks to do. If the app bypassed all the previous
297         // ones due to the SDK grandfathering schemes, allow it access.
298         return LocationPermissionResult.ALLOWED;
299     }
300 
301 
checkManifestPermission(Context context, int pid, int uid, String permissionToCheck)302     private static boolean checkManifestPermission(Context context, int pid, int uid,
303             String permissionToCheck) {
304         return context.checkPermission(permissionToCheck, pid, uid)
305                 == PackageManager.PERMISSION_GRANTED;
306     }
307 
checkSystemLocationAccess(@onNull Context context, int uid, int pid)308     private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid) {
309         if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier())) {
310             if (DBG) Log.w(TAG, "Location disabled, failed, (" + uid + ")");
311             return false;
312         }
313         // If the user or profile is current, permission is granted.
314         // Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission.
315         return isCurrentProfile(context, uid) || checkInteractAcrossUsersFull(context, pid, uid);
316     }
317 
isLocationModeEnabled(@onNull Context context, @UserIdInt int userId)318     private static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) {
319         LocationManager locationManager = context.getSystemService(LocationManager.class);
320         if (locationManager == null) {
321             Log.w(TAG, "Couldn't get location manager, denying location access");
322             return false;
323         }
324         return locationManager.isLocationEnabledForUser(UserHandle.of(userId));
325     }
326 
checkInteractAcrossUsersFull( @onNull Context context, int pid, int uid)327     private static boolean checkInteractAcrossUsersFull(
328             @NonNull Context context, int pid, int uid) {
329         return checkManifestPermission(context, pid, uid,
330                 Manifest.permission.INTERACT_ACROSS_USERS_FULL);
331     }
332 
isCurrentProfile(@onNull Context context, int uid)333     private static boolean isCurrentProfile(@NonNull Context context, int uid) {
334         long token = Binder.clearCallingIdentity();
335         try {
336             if (UserHandle.getUserHandleForUid(uid).getIdentifier()
337                     == ActivityManager.getCurrentUser()) {
338                 return true;
339             }
340             ActivityManager activityManager = context.getSystemService(ActivityManager.class);
341             if (activityManager != null) {
342                 return activityManager.isProfileForeground(
343                         UserHandle.getUserHandleForUid(ActivityManager.getCurrentUser()));
344             } else {
345                 return false;
346             }
347         } finally {
348             Binder.restoreCallingIdentity(token);
349         }
350     }
351 
isAppAtLeastSdkVersion(Context context, String pkgName, int sdkVersion)352     private static boolean isAppAtLeastSdkVersion(Context context, String pkgName, int sdkVersion) {
353         try {
354             if (context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion
355                     >= sdkVersion) {
356                 return true;
357             }
358         } catch (PackageManager.NameNotFoundException e) {
359             // In case of exception, assume known app (more strict checking)
360             // Note: This case will never happen since checkPackage is
361             // called to verify validity before checking app's version.
362         }
363         return false;
364     }
365 }
366