Loading core/java/com/android/internal/util/ConnectivityUtil.java 0 → 100644 +202 −0 Original line number Diff line number Diff line /* * 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); } } core/tests/utiltests/src/com/android/internal/util/ConnectivityUtilTest.java 0 → 100644 +286 −0 Original line number Diff line number Diff line /* * 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)); } } } Loading
core/java/com/android/internal/util/ConnectivityUtil.java 0 → 100644 +202 −0 Original line number Diff line number Diff line /* * 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); } }
core/tests/utiltests/src/com/android/internal/util/ConnectivityUtilTest.java 0 → 100644 +286 −0 Original line number Diff line number Diff line /* * 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)); } } }