/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.telephony; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.PackageManager; import android.location.LocationManager; import android.os.Binder; import android.os.Build; import android.os.Process; import android.os.UserHandle; import android.util.Log; import android.widget.Toast; import com.android.internal.telephony.util.TelephonyUtils; /** * Helper for performing location access checks. * @hide */ public final class LocationAccessPolicy { private static final String TAG = "LocationAccessPolicy"; private static final boolean DBG = false; public static final int MAX_SDK_FOR_ANY_ENFORCEMENT = Build.VERSION_CODES.CUR_DEVELOPMENT; public enum LocationPermissionResult { ALLOWED, /** * Indicates that the denial is due to a transient device state * (e.g. app-ops, location main switch) */ DENIED_SOFT, /** * Indicates that the denial is due to a misconfigured app (e.g. missing entry in manifest) */ DENIED_HARD, } /** Data structure for location permission query */ public static class LocationPermissionQuery { public final String callingPackage; public final String callingFeatureId; public final int callingUid; public final int callingPid; public final int minSdkVersionForCoarse; public final int minSdkVersionForFine; public final boolean logAsInfo; public final String method; private LocationPermissionQuery(String callingPackage, @Nullable String callingFeatureId, int callingUid, int callingPid, int minSdkVersionForCoarse, int minSdkVersionForFine, boolean logAsInfo, String method) { this.callingPackage = callingPackage; this.callingFeatureId = callingFeatureId; this.callingUid = callingUid; this.callingPid = callingPid; this.minSdkVersionForCoarse = minSdkVersionForCoarse; this.minSdkVersionForFine = minSdkVersionForFine; this.logAsInfo = logAsInfo; this.method = method; } /** Builder for LocationPermissionQuery */ public static class Builder { private String mCallingPackage; private String mCallingFeatureId; private int mCallingUid; private int mCallingPid; private int mMinSdkVersionForCoarse = -1; private int mMinSdkVersionForFine = -1; private int mMinSdkVersionForEnforcement = -1; private boolean mLogAsInfo = false; private String mMethod; /** * Mandatory parameter, used for performing permission checks. */ public Builder setCallingPackage(String callingPackage) { mCallingPackage = callingPackage; return this; } /** * Mandatory parameter, used for performing permission checks. */ public Builder setCallingFeatureId(@Nullable String callingFeatureId) { mCallingFeatureId = callingFeatureId; return this; } /** * Mandatory parameter, used for performing permission checks. */ public Builder setCallingUid(int callingUid) { mCallingUid = callingUid; return this; } /** * Mandatory parameter, used for performing permission checks. */ public Builder setCallingPid(int callingPid) { mCallingPid = callingPid; return this; } /** * Apps that target at least this sdk version will be checked for coarse location * permission. This method MUST be called before calling {@link #build()}. Otherwise, an * {@link IllegalArgumentException} will be thrown. * * Additionally, if both the argument to this method and * {@link #setMinSdkVersionForFine} are greater than {@link Build.VERSION_CODES#BASE}, * you must call {@link #setMinSdkVersionForEnforcement} with the min of the two to * affirm that you do not want any location checks below a certain SDK version. * Otherwise, {@link #build} will throw an {@link IllegalArgumentException}. */ public Builder setMinSdkVersionForCoarse( int minSdkVersionForCoarse) { mMinSdkVersionForCoarse = minSdkVersionForCoarse; return this; } /** * Apps that target at least this sdk version will be checked for fine location * permission. This method MUST be called before calling {@link #build()}. * Otherwise, an {@link IllegalArgumentException} will be thrown. * * Additionally, if both the argument to this method and * {@link #setMinSdkVersionForCoarse} are greater than {@link Build.VERSION_CODES#BASE}, * you must call {@link #setMinSdkVersionForEnforcement} with the min of the two to * affirm that you do not want any location checks below a certain SDK version. * Otherwise, {@link #build} will throw an {@link IllegalArgumentException}. */ public Builder setMinSdkVersionForFine( int minSdkVersionForFine) { mMinSdkVersionForFine = minSdkVersionForFine; return this; } /** * If both the argument to {@link #setMinSdkVersionForFine} and * {@link #setMinSdkVersionForCoarse} are greater than {@link Build.VERSION_CODES#BASE}, * this method must be called with the min of the two to * affirm that you do not want any location checks below a certain SDK version. */ public Builder setMinSdkVersionForEnforcement(int minSdkVersionForEnforcement) { mMinSdkVersionForEnforcement = minSdkVersionForEnforcement; return this; } /** * Optional, for logging purposes only. */ public Builder setMethod(String method) { mMethod = method; return this; } /** * If called with {@code true}, log messages will only be printed at the info level. */ public Builder setLogAsInfo(boolean logAsInfo) { mLogAsInfo = logAsInfo; return this; } /** build LocationPermissionQuery */ public LocationPermissionQuery build() { if (mMinSdkVersionForCoarse < 0 || mMinSdkVersionForFine < 0) { throw new IllegalArgumentException("Must specify min sdk versions for" + " enforcement for both coarse and fine permissions"); } if (mMinSdkVersionForFine > Build.VERSION_CODES.BASE && mMinSdkVersionForCoarse > Build.VERSION_CODES.BASE) { if (mMinSdkVersionForEnforcement != Math.min( mMinSdkVersionForCoarse, mMinSdkVersionForFine)) { throw new IllegalArgumentException("setMinSdkVersionForEnforcement must be" + " called."); } } if (mMinSdkVersionForFine < mMinSdkVersionForCoarse) { throw new IllegalArgumentException("Since fine location permission includes" + " access to coarse location, the min sdk level for enforcement of" + " the fine location permission must not be less than the min sdk" + " level for enforcement of the coarse location permission."); } return new LocationPermissionQuery(mCallingPackage, mCallingFeatureId, mCallingUid, mCallingPid, mMinSdkVersionForCoarse, mMinSdkVersionForFine, mLogAsInfo, mMethod); } } } private static void logError(Context context, LocationPermissionQuery query, String errorMsg) { if (query.logAsInfo) { Log.i(TAG, errorMsg); return; } Log.e(TAG, errorMsg); try { if (TelephonyUtils.IS_DEBUGGABLE) { Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show(); } } catch (Throwable t) { // whatever, not important } } private static LocationPermissionResult appOpsModeToPermissionResult(int appOpsMode) { switch (appOpsMode) { case AppOpsManager.MODE_ALLOWED: return LocationPermissionResult.ALLOWED; case AppOpsManager.MODE_ERRORED: return LocationPermissionResult.DENIED_HARD; default: return LocationPermissionResult.DENIED_SOFT; } } private static String getAppOpsString(String manifestPermission) { switch (manifestPermission) { case Manifest.permission.ACCESS_FINE_LOCATION: return AppOpsManager.OPSTR_FINE_LOCATION; case Manifest.permission.ACCESS_COARSE_LOCATION: return AppOpsManager.OPSTR_COARSE_LOCATION; default: return null; } } private static LocationPermissionResult checkAppLocationPermissionHelper(Context context, LocationPermissionQuery query, String permissionToCheck) { String locationTypeForLog = Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck) ? "fine" : "coarse"; // Do the app-ops and the manifest check without any of the allow-overrides first. boolean hasManifestPermission = checkManifestPermission(context, query.callingPid, query.callingUid, permissionToCheck); if (hasManifestPermission) { // Only check the app op if the app has the permission. int appOpMode = context.getSystemService(AppOpsManager.class) .noteOpNoThrow(getAppOpsString(permissionToCheck), query.callingUid, query.callingPackage, query.callingFeatureId, null); if (appOpMode == AppOpsManager.MODE_ALLOWED) { // If the app did everything right, return without logging. return LocationPermissionResult.ALLOWED; } else { // If the app has the manifest permission but not the app-op permission, it means // that it's aware of the requirement and the user denied permission explicitly. // If we see this, don't let any of the overrides happen. Log.i(TAG, query.callingPackage + " is aware of " + locationTypeForLog + " but the" + " app-ops permission is specifically denied."); return appOpsModeToPermissionResult(appOpMode); } } int minSdkVersion = Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck) ? query.minSdkVersionForFine : query.minSdkVersionForCoarse; // If the app fails for some reason, see if it should be allowed to proceed. if (minSdkVersion > MAX_SDK_FOR_ANY_ENFORCEMENT) { String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog + " because we're not enforcing API " + minSdkVersion + " yet." + " Please fix this app because it will break in the future. Called from " + query.method; logError(context, query, errorMsg); return null; } else if (!isAppAtLeastSdkVersion(context, query.callingPackage, minSdkVersion)) { String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog + " because it doesn't target API " + minSdkVersion + " yet." + " Please fix this app. Called from " + query.method; logError(context, query, errorMsg); return null; } else { // If we're not allowing it due to the above two conditions, this means that the app // did not declare the permission in their manifest. return LocationPermissionResult.DENIED_HARD; } } /** Check if location permissions have been granted */ public static LocationPermissionResult checkLocationPermission( Context context, LocationPermissionQuery query) { // Always allow the phone process, system server, and network stack to access location. // This avoid breaking legacy code that rely on public-facing APIs to access cell location, // and it doesn't create an info leak risk because the cell location is stored in the phone // process anyway, and the system server already has location access. if (query.callingUid == Process.PHONE_UID || query.callingUid == Process.SYSTEM_UID || query.callingUid == Process.NETWORK_STACK_UID || query.callingUid == Process.ROOT_UID) { return LocationPermissionResult.ALLOWED; } // Check the system-wide requirements. If the location main switch is off and the caller is // not in the allowlist of apps that always have loation access or the app's profile // isn't in the foreground, return a soft denial. if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid, query.callingPackage)) { return LocationPermissionResult.DENIED_SOFT; } // Do the check for fine, then for coarse. if (query.minSdkVersionForFine < Integer.MAX_VALUE) { LocationPermissionResult resultForFine = checkAppLocationPermissionHelper( context, query, Manifest.permission.ACCESS_FINE_LOCATION); if (resultForFine != null) { return resultForFine; } } if (query.minSdkVersionForCoarse < Integer.MAX_VALUE) { LocationPermissionResult resultForCoarse = checkAppLocationPermissionHelper( context, query, Manifest.permission.ACCESS_COARSE_LOCATION); if (resultForCoarse != null) { return resultForCoarse; } } // At this point, we're out of location checks to do. If the app bypassed all the previous // ones due to the SDK backwards compatibility schemes, allow it access. return LocationPermissionResult.ALLOWED; } private static boolean checkManifestPermission(Context context, int pid, int uid, String permissionToCheck) { return context.checkPermission(permissionToCheck, pid, uid) == PackageManager.PERMISSION_GRANTED; } private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid, @NonNull String callingPackage) { if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier()) && !isLocationBypassAllowed(context, callingPackage)) { if (DBG) Log.w(TAG, "Location disabled, failed, (" + uid + ")"); return false; } // If the user or profile is current, permission is granted. // Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission. return isCurrentProfile(context, uid) || checkInteractAcrossUsersFull(context, pid, uid); } /** * @return Whether location is enabled for the given user. */ public static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) { LocationManager locationManager = context.getSystemService(LocationManager.class); if (locationManager == null) { Log.w(TAG, "Couldn't get location manager, denying location access"); return false; } return locationManager.isLocationEnabledForUser(UserHandle.of(userId)); } private static boolean isLocationBypassAllowed(@NonNull Context context, @NonNull String callingPackage) { for (String bypassPackage : getLocationBypassPackages(context)) { if (callingPackage.equals(bypassPackage)) { return true; } } return false; } /** * @return An array of packages that are always allowed to access location. */ public static @NonNull String[] getLocationBypassPackages(@NonNull Context context) { return context.getResources().getStringArray( com.android.internal.R.array.config_serviceStateLocationAllowedPackages); } private static boolean checkInteractAcrossUsersFull( @NonNull Context context, int pid, int uid) { return checkManifestPermission(context, pid, uid, Manifest.permission.INTERACT_ACROSS_USERS_FULL); } private static boolean isCurrentProfile(@NonNull Context context, int uid) { final long token = Binder.clearCallingIdentity(); try { if (UserHandle.getUserHandleForUid(uid).getIdentifier() == ActivityManager.getCurrentUser()) { return true; } ActivityManager activityManager = context.getSystemService(ActivityManager.class); if (activityManager != null) { return activityManager.isProfileForeground( UserHandle.getUserHandleForUid(ActivityManager.getCurrentUser())); } else { return false; } } finally { Binder.restoreCallingIdentity(token); } } private static boolean isAppAtLeastSdkVersion(Context context, String pkgName, int sdkVersion) { try { if (context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion >= sdkVersion) { return true; } } catch (PackageManager.NameNotFoundException e) { // In case of exception, assume known app (more strict checking) // Note: This case will never happen since checkPackage is // called to verify validity before checking app's version. } return false; } }