blob: eb744f619f2e5e034997c534c2c2532ec5f02d48 [file] [log] [blame]
/*
* Copyright (C) 2017 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.UserIdInt;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.location.LocationManager;
import android.os.Binder;
import android.os.Build;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.widget.Toast;
import java.util.List;
/**
* 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 master switch)
*/
DENIED_SOFT,
/**
* Indicates that the denial is due to a misconfigured app (e.g. missing entry in manifest)
*/
DENIED_HARD,
}
public static class LocationPermissionQuery {
public final String callingPackage;
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, int callingUid, int callingPid,
int minSdkVersionForCoarse, int minSdkVersionForFine, boolean logAsInfo,
String method) {
this.callingPackage = callingPackage;
this.callingUid = callingUid;
this.callingPid = callingPid;
this.minSdkVersionForCoarse = minSdkVersionForCoarse;
this.minSdkVersionForFine = minSdkVersionForFine;
this.logAsInfo = logAsInfo;
this.method = method;
}
public static class Builder {
private String mCallingPackage;
private int mCallingUid;
private int mCallingPid;
private int mMinSdkVersionForCoarse = Integer.MAX_VALUE;
private int mMinSdkVersionForFine = Integer.MAX_VALUE;
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 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. Defaults to INT_MAX (which means don't check)
*/
public Builder setMinSdkVersionForCoarse(
int minSdkVersionForCoarse) {
mMinSdkVersionForCoarse = minSdkVersionForCoarse;
return this;
}
/**
* Apps that target at least this sdk version will be checked for fine location
* permission. Defaults to INT_MAX (which means don't check)
*/
public Builder setMinSdkVersionForFine(
int minSdkVersionForFine) {
mMinSdkVersionForFine = minSdkVersionForFine;
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;
}
public LocationPermissionQuery build() {
return new LocationPermissionQuery(mCallingPackage, 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 (Build.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 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(AppOpsManager.permissionToOpCode(permissionToCheck),
query.callingUid, query.callingPackage);
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;
}
}
public static LocationPermissionResult checkLocationPermission(
Context context, LocationPermissionQuery query) {
// Always allow the phone process and system server 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.ROOT_UID) {
return LocationPermissionResult.ALLOWED;
}
// Check the system-wide requirements. If the location master switch is off or
// the app's profile isn't in foreground, return a soft denial.
if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid)) {
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 grandfathering 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) {
if (!isLocationModeEnabled(context, UserHandle.getUserId(uid))) {
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, uid, pid);
}
private 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 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) {
long token = Binder.clearCallingIdentity();
try {
final int currentUser = ActivityManager.getCurrentUser();
final int callingUserId = UserHandle.getUserId(uid);
if (callingUserId == currentUser) {
return true;
} else {
List<UserInfo> userProfiles = context.getSystemService(
UserManager.class).getProfiles(currentUser);
for (UserInfo user : userProfiles) {
if (user.id == callingUserId) {
return true;
}
}
}
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;
}
}