Merge "Create an until function to check location permission"
am: 948c4086af

Change-Id: I3e04a4b2d769beac85a013dafc777ac60f7080f1
diff --git a/core/java/com/android/internal/util/ConnectivityUtil.java b/core/java/com/android/internal/util/ConnectivityUtil.java
new file mode 100644
index 0000000..b1d4fa0
--- /dev/null
+++ b/core/java/com/android/internal/util/ConnectivityUtil.java
@@ -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);
+    }
+}
diff --git a/core/tests/utiltests/src/com/android/internal/util/ConnectivityUtilTest.java b/core/tests/utiltests/src/com/android/internal/util/ConnectivityUtilTest.java
new file mode 100644
index 0000000..5564712
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/ConnectivityUtilTest.java
@@ -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));
+        }
+    }
+}