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