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