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