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 }