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