Merge "Create an until function to check location permission"

This commit is contained in:
Treehugger Robot
2020-01-16 00:30:39 +00:00
committed by Gerrit Code Review
2 changed files with 488 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
/*
* 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 com.android.internal.util;
import android.Manifest;
import android.annotation.Nullable;
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.UserHandle;
import android.os.UserManager;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
/**
* Utility methods for common functionality using by different networks.
*
* @hide
*/
public class ConnectivityUtil {
private static final String TAG = "ConnectivityUtil";
private final Context mContext;
private final AppOpsManager mAppOps;
private final UserManager mUserManager;
public ConnectivityUtil(Context context) {
mContext = context;
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
}
/**
* API to determine if the caller has fine/coarse location permission (depending on
* config/targetSDK level) and the location mode is enabled for the user. SecurityException is
* thrown if the caller has no permission or the location mode is disabled.
* @param pkgName package name of the application requesting access
* @param featureId The feature in the package
* @param uid The uid of the package
* @param message A message describing why the permission was checked. Only needed if this is
* not inside of a two-way binder call from the data receiver
*/
public void enforceLocationPermission(String pkgName, @Nullable String featureId, int uid,
@Nullable String message)
throws SecurityException {
checkPackage(uid, pkgName);
// Location mode must be enabled
if (!isLocationModeEnabled()) {
// Location mode is disabled, scan results cannot be returned
throw new SecurityException("Location mode is disabled for the device");
}
// LocationAccess by App: caller must have Coarse/Fine Location permission to have access to
// location information.
boolean canAppPackageUseLocation = checkCallersLocationPermission(pkgName, featureId,
uid, /* coarseForTargetSdkLessThanQ */ true, message);
// If neither caller or app has location access, there is no need to check
// any other permissions. Deny access to scan results.
if (!canAppPackageUseLocation) {
throw new SecurityException("UID " + uid + " has no location permission");
}
// If the User or profile is current, permission is granted
// Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission.
if (!isCurrentProfile(uid) && !checkInteractAcrossUsersFull(uid)) {
throw new SecurityException("UID " + uid + " profile not permitted");
}
}
/**
* Checks that calling process has android.Manifest.permission.ACCESS_FINE_LOCATION or
* android.Manifest.permission.ACCESS_COARSE_LOCATION (depending on config/targetSDK level)
* and a corresponding app op is allowed for this package and uid.
*
* @param pkgName PackageName of the application requesting access
* @param featureId The feature in the package
* @param uid The uid of the package
* @param coarseForTargetSdkLessThanQ If true and the targetSDK < Q then will check for COARSE
* else (false or targetSDK >= Q) then will check for FINE
* @param message A message describing why the permission was checked. Only needed if this is
* not inside of a two-way binder call from the data receiver
*/
public boolean checkCallersLocationPermission(String pkgName, @Nullable String featureId,
int uid, boolean coarseForTargetSdkLessThanQ, @Nullable String message) {
boolean isTargetSdkLessThanQ = isTargetSdkLessThan(pkgName, Build.VERSION_CODES.Q, uid);
String permissionType = Manifest.permission.ACCESS_FINE_LOCATION;
if (coarseForTargetSdkLessThanQ && isTargetSdkLessThanQ) {
// Having FINE permission implies having COARSE permission (but not the reverse)
permissionType = Manifest.permission.ACCESS_COARSE_LOCATION;
}
if (getUidPermission(permissionType, uid)
== PackageManager.PERMISSION_DENIED) {
return false;
}
// Always checking FINE - even if will not enforce. This will record the request for FINE
// so that a location request by the app is surfaced to the user.
boolean isFineLocationAllowed = noteAppOpAllowed(
AppOpsManager.OPSTR_FINE_LOCATION, pkgName, featureId, uid, message);
if (isFineLocationAllowed) {
return true;
}
if (coarseForTargetSdkLessThanQ && isTargetSdkLessThanQ) {
return noteAppOpAllowed(AppOpsManager.OPSTR_COARSE_LOCATION, pkgName, featureId, uid,
message);
}
return false;
}
/**
* Retrieves a handle to LocationManager (if not already done) and check if location is enabled.
*/
public boolean isLocationModeEnabled() {
LocationManager locationManager =
(LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
try {
return locationManager.isLocationEnabledForUser(UserHandle.of(
getCurrentUser()));
} catch (Exception e) {
Log.e(TAG, "Failure to get location mode via API, falling back to settings", e);
return false;
}
}
private boolean isTargetSdkLessThan(String packageName, int versionCode, int callingUid) {
long ident = Binder.clearCallingIdentity();
try {
if (mContext.getPackageManager().getApplicationInfoAsUser(
packageName, 0,
UserHandle.getUserHandleForUid(callingUid)).targetSdkVersion
< versionCode) {
return true;
}
} catch (PackageManager.NameNotFoundException e) {
// In case of exception, assume unknown app (more strict checking)
// Note: This case will never happen since checkPackage is
// called to verify validity before checking App's version.
} finally {
Binder.restoreCallingIdentity(ident);
}
return false;
}
private boolean noteAppOpAllowed(String op, String pkgName, @Nullable String featureId,
int uid, @Nullable String message) {
return mAppOps.noteOp(op, uid, pkgName) == AppOpsManager.MODE_ALLOWED;
}
private void checkPackage(int uid, String pkgName) throws SecurityException {
if (pkgName == null) {
throw new SecurityException("Checking UID " + uid + " but Package Name is Null");
}
mAppOps.checkPackage(uid, pkgName);
}
private boolean isCurrentProfile(int uid) {
UserHandle currentUser = UserHandle.of(getCurrentUser());
UserHandle callingUser = UserHandle.getUserHandleForUid(uid);
return currentUser.equals(callingUser)
|| mUserManager.isSameProfileGroup(
currentUser.getIdentifier(), callingUser.getIdentifier());
}
private boolean checkInteractAcrossUsersFull(int uid) {
return getUidPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, uid)
== PackageManager.PERMISSION_GRANTED;
}
@VisibleForTesting
protected int getCurrentUser() {
return ActivityManager.getCurrentUser();
}
private int getUidPermission(String permissionType, int uid) {
// We don't care about pid, pass in -1
return mContext.checkPermission(permissionType, -1, uid);
}
}

View File

@@ -0,0 +1,286 @@
/*
* 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 com.android.internal.util;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.Manifest;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Binder;
import android.os.Build;
import android.os.UserHandle;
import android.os.UserManager;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.HashMap;
/** Unit tests for {@link ConnectivityUtil}. */
public class ConnectivityUtilTest {
public static final String TAG = "ConnectivityUtilTest";
// Mock objects for testing
@Mock private Context mMockContext;
@Mock private PackageManager mMockPkgMgr;
@Mock private ApplicationInfo mMockApplInfo;
@Mock private AppOpsManager mMockAppOps;
@Mock private UserManager mMockUserManager;
@Mock private LocationManager mLocationManager;
private static final String TEST_PKG_NAME = "com.google.somePackage";
private static final String TEST_FEATURE_ID = "com.google.someFeature";
private static final int MANAGED_PROFILE_UID = 1100000;
private static final int OTHER_USER_UID = 1200000;
private final String mInteractAcrossUsersFullPermission =
"android.permission.INTERACT_ACROSS_USERS_FULL";
private final String mManifestStringCoarse =
Manifest.permission.ACCESS_COARSE_LOCATION;
private final String mManifestStringFine =
Manifest.permission.ACCESS_FINE_LOCATION;
// Test variables
private int mWifiScanAllowApps;
private int mUid;
private int mCoarseLocationPermission;
private int mAllowCoarseLocationApps;
private int mFineLocationPermission;
private int mAllowFineLocationApps;
private int mCurrentUser;
private boolean mIsLocationEnabled;
private boolean mThrowSecurityException;
private Answer<Integer> mReturnPermission;
private HashMap<String, Integer> mPermissionsList = new HashMap<String, Integer>();
private class TestConnectivityUtil extends ConnectivityUtil {
TestConnectivityUtil(Context context) {
super(context);
}
@Override
protected int getCurrentUser() {
return mCurrentUser;
}
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
initTestVars();
}
private void setupMocks() throws Exception {
when(mMockPkgMgr.getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any()))
.thenReturn(mMockApplInfo);
when(mMockContext.getPackageManager()).thenReturn(mMockPkgMgr);
when(mMockAppOps.noteOp(AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME))
.thenReturn(mWifiScanAllowApps);
when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
eq(TEST_PKG_NAME)))
.thenReturn(mAllowCoarseLocationApps);
when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
eq(TEST_PKG_NAME)))
.thenReturn(mAllowFineLocationApps);
if (mThrowSecurityException) {
doThrow(new SecurityException("Package " + TEST_PKG_NAME + " doesn't belong"
+ " to application bound to user " + mUid))
.when(mMockAppOps).checkPackage(mUid, TEST_PKG_NAME);
}
when(mMockContext.getSystemService(Context.APP_OPS_SERVICE))
.thenReturn(mMockAppOps);
when(mMockContext.getSystemService(Context.USER_SERVICE))
.thenReturn(mMockUserManager);
when(mMockContext.getSystemService(Context.LOCATION_SERVICE)).thenReturn(mLocationManager);
}
private void setupTestCase() throws Exception {
setupMocks();
setupMockInterface();
}
private void initTestVars() {
mPermissionsList.clear();
mReturnPermission = createPermissionAnswer();
mWifiScanAllowApps = AppOpsManager.MODE_ERRORED;
mUid = OTHER_USER_UID;
mThrowSecurityException = true;
mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.M;
mIsLocationEnabled = false;
mCurrentUser = UserHandle.USER_SYSTEM;
mCoarseLocationPermission = PackageManager.PERMISSION_DENIED;
mFineLocationPermission = PackageManager.PERMISSION_DENIED;
mAllowCoarseLocationApps = AppOpsManager.MODE_ERRORED;
mAllowFineLocationApps = AppOpsManager.MODE_ERRORED;
}
private void setupMockInterface() {
Binder.restoreCallingIdentity((((long) mUid) << 32) | Binder.getCallingPid());
doAnswer(mReturnPermission).when(mMockContext).checkPermission(
anyString(), anyInt(), anyInt());
when(mMockUserManager.isSameProfileGroup(UserHandle.SYSTEM.getIdentifier(),
UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID).getIdentifier()))
.thenReturn(true);
when(mMockContext.checkPermission(mManifestStringCoarse, -1, mUid))
.thenReturn(mCoarseLocationPermission);
when(mMockContext.checkPermission(mManifestStringFine, -1, mUid))
.thenReturn(mFineLocationPermission);
when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(mIsLocationEnabled);
}
private Answer<Integer> createPermissionAnswer() {
return new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) {
int myUid = (int) invocation.getArguments()[1];
String myPermission = (String) invocation.getArguments()[0];
mPermissionsList.get(myPermission);
if (mPermissionsList.containsKey(myPermission)) {
int uid = mPermissionsList.get(myPermission);
if (myUid == uid) {
return PackageManager.PERMISSION_GRANTED;
}
}
return PackageManager.PERMISSION_DENIED;
}
};
}
@Test
public void testEnforceLocationPermission_HasAllPermissions_BeforeQ() throws Exception {
mIsLocationEnabled = true;
mThrowSecurityException = false;
mCoarseLocationPermission = PackageManager.PERMISSION_GRANTED;
mAllowCoarseLocationApps = AppOpsManager.MODE_ALLOWED;
mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
mUid = mCurrentUser;
setupTestCase();
new TestConnectivityUtil(mMockContext)
.enforceLocationPermission(TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
}
@Test
public void testEnforceLocationPermission_HasAllPermissions_AfterQ() throws Exception {
mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.Q;
mIsLocationEnabled = true;
mThrowSecurityException = false;
mUid = mCurrentUser;
mFineLocationPermission = PackageManager.PERMISSION_GRANTED;
mAllowFineLocationApps = AppOpsManager.MODE_ALLOWED;
mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
setupTestCase();
new TestConnectivityUtil(mMockContext)
.enforceLocationPermission(TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
}
@Test
public void testEnforceLocationPermission_PkgNameAndUidMismatch() throws Exception {
mThrowSecurityException = true;
mIsLocationEnabled = true;
mFineLocationPermission = PackageManager.PERMISSION_GRANTED;
mAllowFineLocationApps = AppOpsManager.MODE_ALLOWED;
mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
setupTestCase();
assertThrows(SecurityException.class,
() -> new TestConnectivityUtil(mMockContext)
.enforceLocationPermission(TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
}
@Test
public void testenforceCanAccessScanResults_UserOrProfileNotCurrent() throws Exception {
mIsLocationEnabled = true;
mThrowSecurityException = false;
mCoarseLocationPermission = PackageManager.PERMISSION_GRANTED;
mAllowCoarseLocationApps = AppOpsManager.MODE_ALLOWED;
mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
setupTestCase();
assertThrows(SecurityException.class,
() -> new TestConnectivityUtil(mMockContext)
.enforceLocationPermission(TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
}
@Test
public void testenforceCanAccessScanResults_NoCoarseLocationPermission() throws Exception {
mThrowSecurityException = false;
mIsLocationEnabled = true;
setupTestCase();
assertThrows(SecurityException.class,
() -> new TestConnectivityUtil(mMockContext)
.enforceLocationPermission(TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
}
@Test
public void testenforceCanAccessScanResults_NoFineLocationPermission() throws Exception {
mThrowSecurityException = false;
mMockApplInfo.targetSdkVersion = Build.VERSION_CODES.Q;
mIsLocationEnabled = true;
mCoarseLocationPermission = PackageManager.PERMISSION_GRANTED;
mAllowFineLocationApps = AppOpsManager.MODE_ERRORED;
mUid = MANAGED_PROFILE_UID;
setupTestCase();
assertThrows(SecurityException.class,
() -> new TestConnectivityUtil(mMockContext)
.enforceLocationPermission(TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
verify(mMockAppOps, never()).noteOp(anyInt(), anyInt(), anyString());
}
@Test
public void testenforceCanAccessScanResults_LocationModeDisabled() throws Exception {
mThrowSecurityException = false;
mUid = MANAGED_PROFILE_UID;
mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
mPermissionsList.put(mInteractAcrossUsersFullPermission, mUid);
mIsLocationEnabled = false;
setupTestCase();
assertThrows(SecurityException.class,
() -> new TestConnectivityUtil(mMockContext)
.enforceLocationPermission(TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
}
private static void assertThrows(Class<? extends Exception> exceptionClass, Runnable r) {
try {
r.run();
Assert.fail("Expected " + exceptionClass + " to be thrown.");
} catch (Exception exception) {
assertTrue(exceptionClass.isInstance(exception));
}
}
}