Enforce carrier privileges when setting/clearing VCN configs

This change ensures that only carrier-privileged apps can modify VCN
configs.

Since carrier privilege is checked per-subId, we iterate through all
subIds in the group, and check if any of them grant the calling app
carrier privileges.

Bug: 165670724
Test: New tests added, passing.
Change-Id: Iac032136d9c1975e6b95a2d2ad9b811ce45c9a53
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 165b6a1..e9f17ff 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -25,13 +25,22 @@
 import android.net.NetworkRequest;
 import android.net.vcn.IVcnManagementService;
 import android.net.vcn.VcnConfig;
+import android.os.Binder;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.ParcelUuid;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * VcnManagementService manages Virtual Carrier Network profiles and lifecycles.
  *
@@ -130,6 +139,18 @@
             }
             return mHandlerThread.getLooper();
         }
+
+        /**
+         * Retrieves the caller's UID
+         *
+         * <p>This call MUST be made before calling {@link Binder#clearCallingIdentity}, otherwise
+         * this will not work properly.
+         *
+         * @return
+         */
+        public int getBinderCallingUid() {
+            return Binder.getCallingUid();
+        }
     }
 
     /** Notifies the VcnManagementService that external dependencies can be set up. */
@@ -140,6 +161,50 @@
                 .registerNetworkProvider(mNetworkProvider);
     }
 
+    private void enforcePrimaryUser() {
+        final int uid = mDeps.getBinderCallingUid();
+        if (uid == Process.SYSTEM_UID) {
+            throw new IllegalStateException(
+                    "Calling identity was System Server. Was Binder calling identity cleared?");
+        }
+
+        if (!UserHandle.getUserHandleForUid(uid).isSystem()) {
+            throw new SecurityException(
+                    "VcnManagementService can only be used by callers running as the primary user");
+        }
+    }
+
+    private void enforceCallingUserAndCarrierPrivilege(ParcelUuid subscriptionGroup) {
+        // Only apps running in the primary (system) user are allowed to configure the VCN. This is
+        // in line with Telephony's behavior with regards to binding to a Carrier App provided
+        // CarrierConfigService.
+        enforcePrimaryUser();
+
+        // TODO (b/172619301): Check based on events propagated from CarrierPrivilegesTracker
+        final SubscriptionManager subMgr = mContext.getSystemService(SubscriptionManager.class);
+        final List<SubscriptionInfo> subscriptionInfos = new ArrayList<>();
+        Binder.withCleanCallingIdentity(
+                () -> {
+                    subscriptionInfos.addAll(subMgr.getSubscriptionsInGroup(subscriptionGroup));
+                });
+
+        final TelephonyManager telMgr = mContext.getSystemService(TelephonyManager.class);
+        for (SubscriptionInfo info : subscriptionInfos) {
+            // Check subscription is active first; much cheaper/faster check, and an app (currently)
+            // cannot be carrier privileged for inactive subscriptions.
+            if (subMgr.isValidSlotIndex(info.getSimSlotIndex())
+                    && telMgr.hasCarrierPrivileges(info.getSubscriptionId())) {
+                // TODO (b/173717728): Allow configuration for inactive, but manageable
+                // subscriptions.
+                // TODO (b/173718661): Check for whole subscription groups at a time.
+                return;
+            }
+        }
+
+        throw new SecurityException(
+                "Carrier privilege required for subscription group to set VCN Config");
+    }
+
     /**
      * Sets a VCN config for a given subscription group.
      *
@@ -150,6 +215,10 @@
         requireNonNull(subscriptionGroup, "subscriptionGroup was null");
         requireNonNull(config, "config was null");
 
+        enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
+
+        // TODO: Clear Binder calling identity
+
         // TODO: Store VCN configuration, trigger startup as necessary
     }
 
@@ -162,6 +231,10 @@
     public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) {
         requireNonNull(subscriptionGroup, "subscriptionGroup was null");
 
+        enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
+
+        // TODO: Clear Binder calling identity
+
         // TODO: Clear VCN configuration, trigger teardown as necessary
     }
 
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index c91fdbf..633cf64 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -16,14 +16,23 @@
 
 package com.android.server;
 
+import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.vcn.VcnConfig;
+import android.os.ParcelUuid;
+import android.os.Process;
+import android.os.UserHandle;
 import android.os.test.TestLooper;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -31,27 +40,73 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Collections;
+import java.util.UUID;
+
 /** Tests for {@link VcnManagementService}. */
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class VcnManagementServiceTest {
+    private static final ParcelUuid TEST_UUID_1 = new ParcelUuid(new UUID(0, 0));
+    private static final SubscriptionInfo TEST_SUBSCRIPTION_INFO =
+            new SubscriptionInfo(
+                    1 /* id */,
+                    "" /* iccId */,
+                    0 /* simSlotIndex */,
+                    "Carrier" /* displayName */,
+                    "Carrier" /* carrierName */,
+                    0 /* nameSource */,
+                    255 /* iconTint */,
+                    "12345" /* number */,
+                    0 /* roaming */,
+                    null /* icon */,
+                    "0" /* mcc */,
+                    "0" /* mnc */,
+                    "0" /* countryIso */,
+                    false /* isEmbedded */,
+                    null /* nativeAccessRules */,
+                    null /* cardString */,
+                    false /* isOpportunistic */,
+                    TEST_UUID_1.toString() /* groupUUID */,
+                    0 /* carrierId */,
+                    0 /* profileClass */);
+
     private final Context mMockContext = mock(Context.class);
     private final VcnManagementService.Dependencies mMockDeps =
             mock(VcnManagementService.Dependencies.class);
     private final TestLooper mTestLooper = new TestLooper();
     private final ConnectivityManager mConnMgr = mock(ConnectivityManager.class);
+    private final TelephonyManager mTelMgr = mock(TelephonyManager.class);
+    private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class);
     private final VcnManagementService mVcnMgmtSvc;
 
-    public VcnManagementServiceTest() {
-        doReturn(Context.CONNECTIVITY_SERVICE)
-                .when(mMockContext)
-                .getSystemServiceName(ConnectivityManager.class);
-        doReturn(mConnMgr).when(mMockContext).getSystemService(Context.CONNECTIVITY_SERVICE);
+    public VcnManagementServiceTest() throws Exception {
+        setupSystemService(mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
+        setupSystemService(mTelMgr, Context.TELEPHONY_SERVICE, TelephonyManager.class);
+        setupSystemService(
+                mSubMgr, Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class);
 
         doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper();
+        doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid();
+
+        setupMockedCarrierPrivilege(true);
         mVcnMgmtSvc = new VcnManagementService(mMockContext, mMockDeps);
     }
 
+    private void setupSystemService(Object service, String name, Class<?> serviceClass) {
+        doReturn(name).when(mMockContext).getSystemServiceName(serviceClass);
+        doReturn(service).when(mMockContext).getSystemService(name);
+    }
+
+    private void setupMockedCarrierPrivilege(boolean isPrivileged) {
+        doReturn(Collections.singletonList(TEST_SUBSCRIPTION_INFO))
+                .when(mSubMgr)
+                .getSubscriptionsInGroup(any());
+        doReturn(isPrivileged)
+                .when(mTelMgr)
+                .hasCarrierPrivileges(eq(TEST_SUBSCRIPTION_INFO.getSubscriptionId()));
+    }
+
     @Test
     public void testSystemReady() throws Exception {
         mVcnMgmtSvc.systemReady();
@@ -59,4 +114,74 @@
         verify(mConnMgr)
                 .registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class));
     }
+
+    @Test
+    public void testSetVcnConfigRequiresNonSystemServer() throws Exception {
+        doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
+
+        try {
+            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+            fail("Expected IllegalStateException exception for system server");
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    @Test
+    public void testSetVcnConfigRequiresSystemUser() throws Exception {
+        doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, Process.FIRST_APPLICATION_UID))
+                .when(mMockDeps)
+                .getBinderCallingUid();
+
+        try {
+            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+            fail("Expected security exception for non system user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testSetVcnConfigRequiresCarrierPrivileges() throws Exception {
+        setupMockedCarrierPrivilege(false);
+
+        try {
+            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+            fail("Expected security exception for missing carrier privileges");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testClearVcnConfigRequiresNonSystemServer() throws Exception {
+        doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
+
+        try {
+            mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
+            fail("Expected IllegalStateException exception for system server");
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    @Test
+    public void testClearVcnConfigRequiresSystemUser() throws Exception {
+        doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, Process.FIRST_APPLICATION_UID))
+                .when(mMockDeps)
+                .getBinderCallingUid();
+
+        try {
+            mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
+            fail("Expected security exception for non system user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testClearVcnConfigRequiresCarrierPrivileges() throws Exception {
+        setupMockedCarrierPrivilege(false);
+
+        try {
+            mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
+            fail("Expected security exception for missing carrier privileges");
+        } catch (SecurityException expected) {
+        }
+    }
 }