Relax permissions around NetworkStatsManager APIs.
Currently, access to network usage history and statistics requires a
signature|privileged permission, an AppOps bit (associated with the
PACKAGE_USAGE_STATS permission), or device/profile ownership. Once
access is granted via one of these mechanisms, it generally applies to
any UID running in the same user as the caller.
This CL expands access as follows:
-Any app can access its own usage history with no extra requirements.
-Carrier-privileged applications can access usage history for the
entire device.
-Device owners can access per-UID breakdowns for usage. Previously
they could access the summary for the whole device, but not the
individual breakdowns.
We simplify the permission model by defining three access levels -
DEFAULT (own app only), USER (all apps in the same user), and DEVICE
(all apps on the device), and propagate these levels throughout.
Finally, this CL fixes an apparent bug in
NetworkStatsSerice#hasAppOpsPermissions - if the AppOp bit was in
MODE_DEFAULT, hasAppOpsPermission would always return false instead of
falling back to the PackageManager permission check.
Bug: 25812859
Bug: 25813856
Change-Id: Ic96e0776e2a4215a400163872acea1ededfaced9
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsAccessTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsAccessTest.java
new file mode 100644
index 0000000..bb8f9d1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsAccessTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2015 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.server.net;
+
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.Manifest.permission;
+import android.app.AppOpsManager;
+import android.app.admin.DeviceAdminInfo;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.telephony.TelephonyManager;
+
+import com.android.server.LocalServices;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class NetworkStatsAccessTest extends TestCase {
+ private static final String TEST_PKG = "com.example.test";
+ private static final int TEST_UID = 12345;
+
+ @Mock private Context mContext;
+ @Mock private DevicePolicyManagerInternal mDpmi;
+ @Mock private TelephonyManager mTm;
+ @Mock private AppOpsManager mAppOps;
+
+ // Hold the real service so we can restore it when tearing down the test.
+ private DevicePolicyManagerInternal mSystemDpmi;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+
+ mSystemDpmi = LocalServices.getService(DevicePolicyManagerInternal.class);
+ LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
+ LocalServices.addService(DevicePolicyManagerInternal.class, mDpmi);
+
+ when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTm);
+ when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOps);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
+ LocalServices.addService(DevicePolicyManagerInternal.class, mSystemDpmi);
+ super.tearDown();
+ }
+
+ public void testCheckAccessLevel_hasCarrierPrivileges() throws Exception {
+ setHasCarrierPrivileges(true);
+ setIsDeviceOwner(false);
+ setIsProfileOwner(false);
+ setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+ setHasReadHistoryPermission(false);
+ assertEquals(NetworkStatsAccess.Level.DEVICE,
+ NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+ }
+
+ public void testCheckAccessLevel_isDeviceOwner() throws Exception {
+ setHasCarrierPrivileges(false);
+ setIsDeviceOwner(true);
+ setIsProfileOwner(false);
+ setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+ setHasReadHistoryPermission(false);
+ assertEquals(NetworkStatsAccess.Level.DEVICE,
+ NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+ }
+
+ public void testCheckAccessLevel_isProfileOwner() throws Exception {
+ setHasCarrierPrivileges(false);
+ setIsDeviceOwner(false);
+ setIsProfileOwner(true);
+ setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+ setHasReadHistoryPermission(false);
+ assertEquals(NetworkStatsAccess.Level.USER,
+ NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+ }
+
+ public void testCheckAccessLevel_hasAppOpsBitAllowed() throws Exception {
+ setHasCarrierPrivileges(false);
+ setIsDeviceOwner(false);
+ setIsProfileOwner(true);
+ setHasAppOpsPermission(AppOpsManager.MODE_ALLOWED, false);
+ setHasReadHistoryPermission(false);
+ assertEquals(NetworkStatsAccess.Level.USER,
+ NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+ }
+
+ public void testCheckAccessLevel_hasAppOpsBitDefault_grantedPermission() throws Exception {
+ setHasCarrierPrivileges(false);
+ setIsDeviceOwner(false);
+ setIsProfileOwner(true);
+ setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, true);
+ setHasReadHistoryPermission(false);
+ assertEquals(NetworkStatsAccess.Level.USER,
+ NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+ }
+
+ public void testCheckAccessLevel_hasReadHistoryPermission() throws Exception {
+ setHasCarrierPrivileges(false);
+ setIsDeviceOwner(false);
+ setIsProfileOwner(true);
+ setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+ setHasReadHistoryPermission(true);
+ assertEquals(NetworkStatsAccess.Level.USER,
+ NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+ }
+
+ public void testCheckAccessLevel_deniedAppOpsBit() throws Exception {
+ setHasCarrierPrivileges(false);
+ setIsDeviceOwner(false);
+ setIsProfileOwner(false);
+ setHasAppOpsPermission(AppOpsManager.MODE_ERRORED, true);
+ setHasReadHistoryPermission(false);
+ assertEquals(NetworkStatsAccess.Level.DEFAULT,
+ NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+ }
+
+ public void testCheckAccessLevel_deniedAppOpsBit_deniedPermission() throws Exception {
+ setHasCarrierPrivileges(false);
+ setIsDeviceOwner(false);
+ setIsProfileOwner(false);
+ setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+ setHasReadHistoryPermission(false);
+ assertEquals(NetworkStatsAccess.Level.DEFAULT,
+ NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+ }
+
+ private void setHasCarrierPrivileges(boolean hasPrivileges) {
+ when(mTm.checkCarrierPrivilegesForPackage(TEST_PKG)).thenReturn(
+ hasPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
+ : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS);
+ }
+
+ private void setIsDeviceOwner(boolean isOwner) {
+ when(mDpmi.isActiveAdminWithPolicy(TEST_UID, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER))
+ .thenReturn(isOwner);
+ }
+
+ private void setIsProfileOwner(boolean isOwner) {
+ when(mDpmi.isActiveAdminWithPolicy(TEST_UID, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER))
+ .thenReturn(isOwner);
+ }
+
+ private void setHasAppOpsPermission(int appOpsMode, boolean hasPermission) {
+ when(mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, TEST_UID, TEST_PKG))
+ .thenReturn(appOpsMode);
+ when(mContext.checkCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS)).thenReturn(
+ hasPermission ? PackageManager.PERMISSION_GRANTED
+ : PackageManager.PERMISSION_DENIED);
+ }
+
+ private void setHasReadHistoryPermission(boolean hasPermission) {
+ when(mContext.checkCallingOrSelfPermission(permission.READ_NETWORK_USAGE_HISTORY))
+ .thenReturn(hasPermission ? PackageManager.PERMISSION_GRANTED
+ : PackageManager.PERMISSION_DENIED);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
index 1a6c289..6026644 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
@@ -16,6 +16,7 @@
package com.android.server.net;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.NetworkStats.SET_DEFAULT;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
@@ -24,9 +25,14 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import android.content.res.Resources;
+import android.net.NetworkIdentity;
import android.net.NetworkStats;
import android.net.NetworkTemplate;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.frameworks.servicestests.R;
@@ -68,7 +74,7 @@
// verify that history read correctly
assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
- 636016770L, 709306L, 88038768L, 518836L);
+ 636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE);
// now export into a unified format
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -77,12 +83,12 @@
// clear structure completely
collection.reset();
assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
- 0L, 0L, 0L, 0L);
+ 0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE);
// and read back into structure, verifying that totals are same
collection.read(new ByteArrayInputStream(bos.toByteArray()));
assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
- 636016770L, 709306L, 88038768L, 518836L);
+ 636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE);
}
public void testReadLegacyUid() throws Exception {
@@ -94,7 +100,7 @@
// verify that history read correctly
assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
- 637076152L, 711413L, 88343717L, 521022L);
+ 637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE);
// now export into a unified format
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -103,12 +109,12 @@
// clear structure completely
collection.reset();
assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
- 0L, 0L, 0L, 0L);
+ 0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE);
// and read back into structure, verifying that totals are same
collection.read(new ByteArrayInputStream(bos.toByteArray()));
assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
- 637076152L, 711413L, 88343717L, 521022L);
+ 637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE);
}
public void testReadLegacyUidTags() throws Exception {
@@ -151,6 +157,66 @@
assertEquals(2 * HOUR_IN_MILLIS, collection.getEndMillis());
}
+ public void testAccessLevels() throws Exception {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+ final NetworkIdentitySet identSet = new NetworkIdentitySet();
+ identSet.add(new NetworkIdentity(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN,
+ TEST_IMSI, null, false));
+
+ int myUid = Process.myUid();
+ int otherUidInSameUser = Process.myUid() + 1;
+ int uidInDifferentUser = Process.myUid() + UserHandle.PER_USER_RANGE;
+
+ // Record one entry for the current UID.
+ entry.rxBytes = 32;
+ collection.recordData(identSet, myUid, SET_DEFAULT, TAG_NONE, 0, 60 * MINUTE_IN_MILLIS,
+ entry);
+
+ // Record one entry for another UID in this user.
+ entry.rxBytes = 64;
+ collection.recordData(identSet, otherUidInSameUser, SET_DEFAULT, TAG_NONE, 0,
+ 60 * MINUTE_IN_MILLIS, entry);
+
+ // Record one entry for the system UID.
+ entry.rxBytes = 128;
+ collection.recordData(identSet, Process.SYSTEM_UID, SET_DEFAULT, TAG_NONE, 0,
+ 60 * MINUTE_IN_MILLIS, entry);
+
+ // Record one entry for a UID in a different user.
+ entry.rxBytes = 256;
+ collection.recordData(identSet, uidInDifferentUser, SET_DEFAULT, TAG_NONE, 0,
+ 60 * MINUTE_IN_MILLIS, entry);
+
+ // Verify the set of relevant UIDs for each access level.
+ MoreAsserts.assertEquals(new int[] { myUid },
+ collection.getRelevantUids(NetworkStatsAccess.Level.DEFAULT));
+ MoreAsserts.assertEquals(new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser },
+ collection.getRelevantUids(NetworkStatsAccess.Level.USER));
+ MoreAsserts.assertEquals(
+ new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser, uidInDifferentUser },
+ collection.getRelevantUids(NetworkStatsAccess.Level.DEVICE));
+
+ // Verify security check in getHistory.
+ assertNotNull(collection.getHistory(buildTemplateMobileAll(TEST_IMSI), myUid, SET_DEFAULT,
+ TAG_NONE, 0, NetworkStatsAccess.Level.DEFAULT));
+ try {
+ collection.getHistory(buildTemplateMobileAll(TEST_IMSI), otherUidInSameUser,
+ SET_DEFAULT, TAG_NONE, 0, NetworkStatsAccess.Level.DEFAULT);
+ fail("Should have thrown SecurityException for accessing different UID");
+ } catch (SecurityException e) {
+ // expected
+ }
+
+ // Verify appropriate aggregation in getSummary.
+ assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32, 0, 0, 0,
+ NetworkStatsAccess.Level.DEFAULT);
+ assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128, 0, 0, 0,
+ NetworkStatsAccess.Level.USER);
+ assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128 + 256, 0, 0,
+ 0, NetworkStatsAccess.Level.DEVICE);
+ }
+
/**
* Copy a {@link Resources#openRawResource(int)} into {@link File} for
* testing purposes.
@@ -170,16 +236,19 @@
}
private static void assertSummaryTotal(NetworkStatsCollection collection,
- NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+ NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets,
+ @NetworkStatsAccess.Level int accessLevel) {
final NetworkStats.Entry entry = collection.getSummary(
- template, Long.MIN_VALUE, Long.MAX_VALUE).getTotal(null);
+ template, Long.MIN_VALUE, Long.MAX_VALUE, accessLevel)
+ .getTotal(null);
assertEntry(entry, rxBytes, rxPackets, txBytes, txPackets);
}
private static void assertSummaryTotalIncludingTags(NetworkStatsCollection collection,
NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
final NetworkStats.Entry entry = collection.getSummary(
- template, Long.MIN_VALUE, Long.MAX_VALUE).getTotalIncludingTags(null);
+ template, Long.MIN_VALUE, Long.MAX_VALUE, NetworkStatsAccess.Level.DEVICE)
+ .getTotalIncludingTags(null);
assertEntry(entry, rxBytes, rxPackets, txBytes, txPackets);
}