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
This commit is contained in:
Jeff Davidson
2015-12-09 18:04:50 -08:00
parent b3a88669ef
commit aa65e9c069
2 changed files with 256 additions and 9 deletions

View File

@@ -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);
}
}

View File

@@ -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.HOUR_IN_MILLIS;
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 @@ public class NetworkStatsCollectionTest extends AndroidTestCase {
// 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 @@ public class NetworkStatsCollectionTest extends AndroidTestCase {
// 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 @@ public class NetworkStatsCollectionTest extends AndroidTestCase {
// 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 @@ public class NetworkStatsCollectionTest extends AndroidTestCase {
// 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 @@ public class NetworkStatsCollectionTest extends AndroidTestCase {
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 @@ public class NetworkStatsCollectionTest extends AndroidTestCase {
}
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);
}