Merge "Add separate user consent for Platform VPNs" am: ede31cad1c am: 7846b824e0

Change-Id: If2d856c62b77da4bc8ea99335b1e89170f24fbca
This commit is contained in:
Automerger Merge Worker
2020-02-05 00:24:10 +00:00
12 changed files with 200 additions and 42 deletions

View File

@@ -116,7 +116,7 @@ interface IConnectivityManager
boolean prepareVpn(String oldPackage, String newPackage, int userId);
void setVpnPackageAuthorization(String packageName, int userId, boolean authorized);
void setVpnPackageAuthorization(String packageName, int userId, int vpnType);
ParcelFileDescriptor establishVpn(in VpnConfig config);

View File

@@ -18,6 +18,7 @@ package android.net;
import static com.android.internal.util.Preconditions.checkNotNull;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
@@ -30,6 +31,8 @@ import android.os.RemoteException;
import com.android.internal.net.VpnProfile;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.GeneralSecurityException;
/**
@@ -47,6 +50,18 @@ import java.security.GeneralSecurityException;
* @see Ikev2VpnProfile
*/
public class VpnManager {
/** Type representing a lack of VPN @hide */
public static final int TYPE_VPN_NONE = -1;
/** VPN service type code @hide */
public static final int TYPE_VPN_SERVICE = 1;
/** Platform VPN type code @hide */
public static final int TYPE_VPN_PLATFORM = 2;
/** @hide */
@IntDef(value = {TYPE_VPN_NONE, TYPE_VPN_SERVICE, TYPE_VPN_PLATFORM})
@Retention(RetentionPolicy.SOURCE)
public @interface VpnType {}
@NonNull private final Context mContext;
@NonNull private final IConnectivityManager mService;
@@ -54,7 +69,7 @@ public class VpnManager {
final Intent intent = new Intent();
final ComponentName componentName = ComponentName.unflattenFromString(
Resources.getSystem().getString(
com.android.internal.R.string.config_customVpnConfirmDialogComponent));
com.android.internal.R.string.config_platformVpnConfirmDialogComponent));
intent.setComponent(componentName);
return intent;
}

View File

@@ -234,7 +234,7 @@ public class VpnService extends Service {
if (!cm.prepareVpn(packageName, null, userId)) {
cm.prepareVpn(null, packageName, userId);
}
cm.setVpnPackageAuthorization(packageName, userId, true);
cm.setVpnPackageAuthorization(packageName, userId, VpnManager.TYPE_VPN_SERVICE);
} catch (RemoteException e) {
// ignore
}

View File

@@ -2708,7 +2708,11 @@
<string name="config_customAdbPublicKeyConfirmationSecondaryUserComponent"
>com.android.systemui/com.android.systemui.usb.UsbDebuggingSecondaryUserActivity</string>
<!-- Name of the dialog that is used to request the user's consent to VPN connection -->
<!-- Name of the dialog that is used to request the user's consent for a Platform VPN -->
<string name="config_platformVpnConfirmDialogComponent" translatable="false"
>com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog</string>
<!-- Name of the dialog that is used to request the user's consent for a VpnService VPN -->
<string name="config_customVpnConfirmDialogComponent" translatable="false"
>com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog</string>

View File

@@ -2240,6 +2240,7 @@
<java-symbol type="string" name="config_customAdbPublicKeyConfirmationSecondaryUserComponent" />
<java-symbol type="string" name="config_customVpnConfirmDialogComponent" />
<java-symbol type="string" name="config_customVpnAlwaysOnDisconnectedDialogComponent" />
<java-symbol type="string" name="config_platformVpnConfirmDialogComponent" />
<java-symbol type="string" name="config_carrierAppInstallDialogComponent" />
<java-symbol type="string" name="config_defaultNetworkScorerPackageName" />
<java-symbol type="string" name="config_persistentDataPackageName" />

View File

@@ -34,6 +34,13 @@
</intent-filter>
</activity>
<activity android:name=".PlatformVpnConfirmDialog"
android:theme="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight"
android:noHistory="true"
android:excludeFromRecents="true"
android:exported="true">
</activity>
<activity android:name=".ManageDialog"
android:theme="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight"
android:noHistory="true"

View File

@@ -23,6 +23,7 @@ import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.IConnectivityManager;
import android.net.VpnManager;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -43,10 +44,20 @@ public class ConfirmDialog extends AlertActivity
implements DialogInterface.OnClickListener, ImageGetter {
private static final String TAG = "VpnConfirm";
@VpnManager.VpnType private final int mVpnType;
private String mPackage;
private IConnectivityManager mService;
public ConfirmDialog() {
this(VpnManager.TYPE_VPN_SERVICE);
}
public ConfirmDialog(@VpnManager.VpnType int vpnType) {
mVpnType = vpnType;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -138,7 +149,7 @@ public class ConfirmDialog extends AlertActivity
if (mService.prepareVpn(null, mPackage, UserHandle.myUserId())) {
// Authorize this app to initiate VPN connections in the future without user
// intervention.
mService.setVpnPackageAuthorization(mPackage, UserHandle.myUserId(), true);
mService.setVpnPackageAuthorization(mPackage, UserHandle.myUserId(), mVpnType);
setResult(RESULT_OK);
}
} catch (Exception e) {

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2019 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.vpndialogs;
import android.net.VpnManager;
/**
* PlatformVpnConfirmDialog is a minimal subclass for requesting user consent for platform VPN
* profiles.
*/
public class PlatformVpnConfirmDialog extends ConfirmDialog {
public PlatformVpnConfirmDialog() {
super(VpnManager.TYPE_VPN_PLATFORM);
}
}

View File

@@ -112,6 +112,7 @@ import android.net.SocketKeepalive;
import android.net.TetheringManager;
import android.net.UidRange;
import android.net.Uri;
import android.net.VpnManager;
import android.net.VpnService;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.NetworkEvent;
@@ -4310,7 +4311,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
throwIfLockdownEnabled();
Vpn vpn = mVpns.get(userId);
if (vpn != null) {
return vpn.prepare(oldPackage, newPackage, false);
return vpn.prepare(oldPackage, newPackage, VpnManager.TYPE_VPN_SERVICE);
} else {
return false;
}
@@ -4318,26 +4319,29 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
/**
* Set whether the VPN package has the ability to launch VPNs without user intervention.
* This method is used by system-privileged apps.
* VPN permissions are checked in the {@link Vpn} class. If the caller is not {@code userId},
* {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission is required.
* Set whether the VPN package has the ability to launch VPNs without user intervention. This
* method is used by system-privileged apps. VPN permissions are checked in the {@link Vpn}
* class. If the caller is not {@code userId}, {@link
* android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission is required.
*
* @param packageName The package for which authorization state should change.
* @param userId User for whom {@code packageName} is installed.
* @param authorized {@code true} if this app should be able to start a VPN connection without
* explicit user approval, {@code false} if not.
*
* explicit user approval, {@code false} if not.
* @param vpnType The {@link VpnManager.VpnType} constant representing what class of VPN
* permissions should be granted. When unauthorizing an app, {@link
* VpnManager.TYPE_VPN_NONE} should be used.
* @hide
*/
@Override
public void setVpnPackageAuthorization(String packageName, int userId, boolean authorized) {
public void setVpnPackageAuthorization(
String packageName, int userId, @VpnManager.VpnType int vpnType) {
enforceCrossUserPermission(userId);
synchronized (mVpns) {
Vpn vpn = mVpns.get(userId);
if (vpn != null) {
vpn.setPackageAuthorization(packageName, authorized);
vpn.setPackageAuthorization(packageName, vpnType);
}
}
}
@@ -7239,7 +7243,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
final String alwaysOnPackage = getAlwaysOnVpnPackage(userId);
if (alwaysOnPackage != null) {
setAlwaysOnVpnPackage(userId, null, false, null);
setVpnPackageAuthorization(alwaysOnPackage, userId, false);
setVpnPackageAuthorization(alwaysOnPackage, userId, VpnManager.TYPE_VPN_NONE);
}
// Turn Always-on VPN off
@@ -7262,7 +7266,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
} else {
// Prevent this app (packagename = vpnConfig.user) from initiating
// VPN connections in the future without user intervention.
setVpnPackageAuthorization(vpnConfig.user, userId, false);
setVpnPackageAuthorization(
vpnConfig.user, userId, VpnManager.TYPE_VPN_NONE);
prepareVpn(null, VpnConfig.LEGACY_VPN, userId);
}

View File

@@ -62,6 +62,7 @@ import android.net.NetworkInfo.DetailedState;
import android.net.NetworkProvider;
import android.net.RouteInfo;
import android.net.UidRange;
import android.net.VpnManager;
import android.net.VpnService;
import android.os.Binder;
import android.os.Build.VERSION_CODES;
@@ -519,8 +520,11 @@ public class Vpn {
}
if (packageName != null) {
// Pre-authorize new always-on VPN package.
if (!setPackageAuthorization(packageName, true)) {
// TODO: Give the minimum permission possible; if there is a Platform VPN profile, only
// grant ACTIVATE_PLATFORM_VPN.
// Pre-authorize new always-on VPN package. Grant the full ACTIVATE_VPN appop, allowing
// both VpnService and Platform VPNs.
if (!setPackageAuthorization(packageName, VpnManager.TYPE_VPN_SERVICE)) {
return false;
}
mAlwaysOn = true;
@@ -691,13 +695,12 @@ public class Vpn {
*
* @param oldPackage The package name of the old VPN application
* @param newPackage The package name of the new VPN application
* @param isPlatformVpn Whether the package being prepared is using a platform VPN profile.
* Preparing a platform VPN profile requires only the lesser ACTIVATE_PLATFORM_VPN appop.
* @param vpnType The type of VPN being prepared. One of {@link VpnManager.VpnType} Preparing a
* platform VPN profile requires only the lesser ACTIVATE_PLATFORM_VPN appop.
* @return true if the operation succeeded.
*/
// TODO: Use an Intdef'd type to represent what kind of VPN the caller is preparing.
public synchronized boolean prepare(
String oldPackage, String newPackage, boolean isPlatformVpn) {
String oldPackage, String newPackage, @VpnManager.VpnType int vpnType) {
if (oldPackage != null) {
// Stop an existing always-on VPN from being dethroned by other apps.
if (mAlwaysOn && !isCurrentPreparedPackage(oldPackage)) {
@@ -709,13 +712,13 @@ public class Vpn {
// The package doesn't match. We return false (to obtain user consent) unless the
// user has already consented to that VPN package.
if (!oldPackage.equals(VpnConfig.LEGACY_VPN)
&& isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) {
&& isVpnPreConsented(mContext, oldPackage, vpnType)) {
prepareInternal(oldPackage);
return true;
}
return false;
} else if (!oldPackage.equals(VpnConfig.LEGACY_VPN)
&& !isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) {
&& !isVpnPreConsented(mContext, oldPackage, vpnType)) {
// Currently prepared VPN is revoked, so unprepare it and return false.
prepareInternal(VpnConfig.LEGACY_VPN);
return false;
@@ -798,25 +801,49 @@ public class Vpn {
}
}
/**
* Set whether a package has the ability to launch VPNs without user intervention.
*/
public boolean setPackageAuthorization(String packageName, boolean authorized) {
/** Set whether a package has the ability to launch VPNs without user intervention. */
public boolean setPackageAuthorization(String packageName, @VpnManager.VpnType int vpnType) {
// Check if the caller is authorized.
enforceControlPermissionOrInternalCaller();
int uid = getAppUid(packageName, mUserHandle);
final int uid = getAppUid(packageName, mUserHandle);
if (uid == -1 || VpnConfig.LEGACY_VPN.equals(packageName)) {
// Authorization for nonexistent packages (or fake ones) can't be updated.
return false;
}
long token = Binder.clearCallingIdentity();
final long token = Binder.clearCallingIdentity();
try {
AppOpsManager appOps =
final int[] toChange;
// Clear all AppOps if the app is being unauthorized.
switch (vpnType) {
case VpnManager.TYPE_VPN_NONE:
toChange = new int[] {
AppOpsManager.OP_ACTIVATE_VPN, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN
};
break;
case VpnManager.TYPE_VPN_PLATFORM:
toChange = new int[] {AppOpsManager.OP_ACTIVATE_PLATFORM_VPN};
break;
case VpnManager.TYPE_VPN_SERVICE:
toChange = new int[] {AppOpsManager.OP_ACTIVATE_VPN};
break;
default:
Log.wtf(TAG, "Unrecognized VPN type while granting authorization");
return false;
}
final AppOpsManager appOpMgr =
(AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
appOps.setMode(AppOpsManager.OP_ACTIVATE_VPN, uid, packageName,
authorized ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
for (final int appOp : toChange) {
appOpMgr.setMode(
appOp,
uid,
packageName,
vpnType == VpnManager.TYPE_VPN_NONE
? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED);
}
return true;
} catch (Exception e) {
Log.wtf(TAG, "Failed to set app ops for package " + packageName + ", uid " + uid, e);
@@ -826,11 +853,15 @@ public class Vpn {
return false;
}
private static boolean isVpnPreConsented(
Context context, String packageName, boolean isPlatformVpn) {
return isPlatformVpn
? isVpnProfilePreConsented(context, packageName)
: isVpnServicePreConsented(context, packageName);
private static boolean isVpnPreConsented(Context context, String packageName, int vpnType) {
switch (vpnType) {
case VpnManager.TYPE_VPN_SERVICE:
return isVpnServicePreConsented(context, packageName);
case VpnManager.TYPE_VPN_PLATFORM:
return isVpnProfilePreConsented(context, packageName);
default:
return false;
}
}
private static boolean doesPackageHaveAppop(Context context, String packageName, int appop) {
@@ -2373,7 +2404,7 @@ public class Vpn {
checkNotNull(keyStore, "KeyStore missing");
// Prepare VPN for startup
if (!prepare(packageName, null /* newPackage */, true /* isPlatformVpn */)) {
if (!prepare(packageName, null /* newPackage */, VpnManager.TYPE_VPN_PLATFORM)) {
throw new SecurityException("User consent not granted for package " + packageName);
}

View File

@@ -16,6 +16,7 @@
package android.net;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.any;
@@ -24,6 +25,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.Intent;
import android.test.mock.MockContext;
import androidx.test.filters.SmallTest;
@@ -78,7 +81,13 @@ public class VpnManagerTest {
when(mMockCs.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))).thenReturn(false);
// Expect intent to be returned, as consent has not already been granted.
assertNotNull(mVpnManager.provisionVpnProfile(profile));
final Intent intent = mVpnManager.provisionVpnProfile(profile);
assertNotNull(intent);
final ComponentName expectedComponentName =
ComponentName.unflattenFromString(
"com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog");
assertEquals(expectedComponentName, intent.getComponent());
verify(mMockCs).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
}

View File

@@ -63,6 +63,7 @@ import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo.DetailedState;
import android.net.UidRange;
import android.net.VpnManager;
import android.net.VpnService;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
@@ -471,12 +472,12 @@ public class VpnTest {
order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser));
// When a new VPN package is set the rules should change to cover that package.
vpn.prepare(null, PKGS[0], false /* isPlatformVpn */);
vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE);
order.verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(entireUser));
order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(exceptPkg0));
// When that VPN package is unset, everything should be undone again in reverse.
vpn.prepare(null, VpnConfig.LEGACY_VPN, false /* isPlatformVpn */);
vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE);
order.verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(exceptPkg0));
order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser));
}
@@ -817,6 +818,51 @@ public class VpnTest {
eq(TEST_VPN_PKG));
}
@Test
public void testSetPackageAuthorizationVpnService() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks();
assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE));
verify(mAppOps)
.setMode(
eq(AppOpsManager.OP_ACTIVATE_VPN),
eq(Process.myUid()),
eq(TEST_VPN_PKG),
eq(AppOpsManager.MODE_ALLOWED));
}
@Test
public void testSetPackageAuthorizationPlatformVpn() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks();
assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM));
verify(mAppOps)
.setMode(
eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN),
eq(Process.myUid()),
eq(TEST_VPN_PKG),
eq(AppOpsManager.MODE_ALLOWED));
}
@Test
public void testSetPackageAuthorizationRevokeAuthorization() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks();
assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE));
verify(mAppOps)
.setMode(
eq(AppOpsManager.OP_ACTIVATE_VPN),
eq(Process.myUid()),
eq(TEST_VPN_PKG),
eq(AppOpsManager.MODE_IGNORED));
verify(mAppOps)
.setMode(
eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN),
eq(Process.myUid()),
eq(TEST_VPN_PKG),
eq(AppOpsManager.MODE_IGNORED));
}
/**
* Mock some methods of vpn object.
*/