Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2c68dadb authored by Robin Lee's avatar Robin Lee
Browse files

Better call emulation for DevicePolicyManagerTest

Let's have several separate contexts instead of doing strange things
with package names in one monolithic context representing every single
user on the device at once, sometimes multiple times in the same call.

Syntax looks like:

        runAsCaller(callerContext, dpms, (dpm) -> {
            assertSomething(dpm.doSomething(caller, param));
        });

When a caller calls into DevicePolicyManager here's what happens:

PRE

- a new DevicePolicyManager is created with the caller context
- service context callingIdentity is saved
- the callingUid, callingPid, and callingPermissions are added to the service context

TEST

- client-side test code interacts with DevicePolicyManager using the caller context
- server-side coder under test runs as DevicePolicyManagerService using the service context

POST

- service context callingIdentity is restored to what it was before the test.

This should be easier to reason about.

Test: runtest -x frameworks/base/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
Test: cts-tradefed run cts-dev --module CtsDevicePolicyManagerTestCases
Change-Id: I148e3f298b0a958639ce261e9cf91f6eb49fae4d
parent c4a6d29a
Loading
Loading
Loading
Loading
+133 −99
Original line number Diff line number Diff line
@@ -157,7 +157,6 @@ public class DevicePolicyManagerTest extends DpmTestBase {
        super.setUp();

        mContext = getContext();

        when(mContext.packageManager.hasSystemFeature(eq(PackageManager.FEATURE_DEVICE_ADMIN)))
                .thenReturn(true);

@@ -174,6 +173,12 @@ public class DevicePolicyManagerTest extends DpmTestBase {
        setUpUserManager();
    }

    @Override
    protected void tearDown() throws Exception {
        flushTasks();
        super.tearDown();
    }

    private void initializeDpms() {
        // Need clearCallingIdentity() to pass permission checks.
        final long ident = mContext.binder.clearCallingIdentity();
@@ -1246,7 +1251,7 @@ public class DevicePolicyManagerTest extends DpmTestBase {
        mContext.applicationInfo = new ApplicationInfo();
        mContext.callerPermissions.add(permission.MANAGE_USERS);
        mContext.packageName = "com.android.frameworks.servicestests";
        mContext.userContexts.put(user, mContext);
        mContext.addPackageContext(user, mContext);
        when(mContext.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE);

        StringParceledListSlice oneCert = asSlice(new String[] {"1"});
@@ -3966,121 +3971,168 @@ public class DevicePolicyManagerTest extends DpmTestBase {
    }

    public void testGetOwnerInstalledCaCertsForDeviceOwner() throws Exception {
        mContext.packageName = mRealTestContext.getPackageName();
        setDeviceOwner();

        mContext.packageName = admin1.getPackageName();
        mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
        verifyCanGetOwnerInstalledCaCerts(admin1);
        final DpmMockContext caller = new DpmMockContext(mRealTestContext, "test-caller");
        caller.packageName = admin1.getPackageName();
        caller.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;

        verifyCanGetOwnerInstalledCaCerts(admin1, caller);
    }

    public void testGetOwnerInstalledCaCertsForProfileOwner() throws Exception {
        mContext.packageName = mRealTestContext.getPackageName();
        setAsProfileOwner(admin1);

        mContext.packageName = admin1.getPackageName();
        verifyCanGetOwnerInstalledCaCerts(admin1);
        verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(admin1);
        final DpmMockContext caller = new DpmMockContext(mRealTestContext, "test-caller");
        caller.packageName = admin1.getPackageName();
        caller.binder.callingUid = DpmMockContext.CALLER_UID;

        verifyCanGetOwnerInstalledCaCerts(admin1, caller);
        verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(admin1, caller);
    }

    public void testGetOwnerInstalledCaCertsForDelegate() throws Exception {
        mContext.packageName = mRealTestContext.getPackageName();
        setAsProfileOwner(admin1);

        final String delegate = "com.example.delegate";
        final int delegateUid = setupPackageInPackageManager(delegate, 20988);
        dpm.setCertInstallerPackage(admin1, delegate);

        mContext.packageName = delegate;
        mContext.binder.callingUid = delegateUid;
        verifyCanGetOwnerInstalledCaCerts(null);
        verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(null);
        final DpmMockContext caller = new DpmMockContext(mRealTestContext, "test-caller");
        caller.packageName = delegate;
        caller.binder.callingUid = delegateUid;

        verifyCanGetOwnerInstalledCaCerts(null, caller);
        verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(null, caller);
    }

    private void verifyCanGetOwnerInstalledCaCerts(ComponentName caller) throws Exception {
        final UserHandle user = UserHandle.getUserHandleForUid(mContext.binder.callingUid);
        final int ownerUid = user.equals(UserHandle.SYSTEM) ?
                DpmMockContext.CALLER_SYSTEM_USER_UID : DpmMockContext.CALLER_UID;
    private void verifyCanGetOwnerInstalledCaCerts(
            final ComponentName caller, final DpmMockContext callerContext) throws Exception {
        final String alias = "cert";
        final byte[] caCert = TEST_CA.getBytes();

        mContext.applicationInfo = new ApplicationInfo();
        mContext.userContexts.put(user, mContext);
        when(mContext.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE);
        // device admin (used for posting the tls notification)
        final DpmMockContext admin1Context;
        if (admin1.getPackageName().equals(callerContext.getPackageName())) {
            admin1Context = callerContext;
        } else {
            admin1Context = new DpmMockContext(mRealTestContext, "test-admin");
            admin1Context.packageName = admin1.getPackageName();
            admin1Context.applicationInfo = new ApplicationInfo();
        }
        when(admin1Context.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE);

        // caller: device admin or delegated certificate installer
        callerContext.applicationInfo = new ApplicationInfo();
        final UserHandle callerUser = callerContext.binder.getCallingUserHandle();

        // system_server
        final DpmMockContext serviceContext = mContext;
        serviceContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
        serviceContext.addPackageContext(callerUser, admin1Context);
        serviceContext.addPackageContext(callerUser, callerContext);

        // Install a CA cert.
        final String alias = "cert";
        final byte[] caCert = TEST_CA.getBytes();
        runAsCaller(callerContext, dpms, (dpm) -> {
            when(mContext.keyChainConnection.getService().installCaCertificate(caCert))
                        .thenReturn(alias);
            assertTrue(dpm.installCaCert(caller, caCert));
            when(mContext.keyChainConnection.getService().getUserCaAliases())
                    .thenReturn(asSlice(new String[] {alias}));
        mContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED));

        });

        serviceContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED)
                .putExtra(Intent.EXTRA_USER_HANDLE, callerUser.getIdentifier()));
        flushTasks();

        final List<String> ownerInstalledCaCerts = new ArrayList<>();

        // Device Owner / Profile Owner can find out which CA certs were installed by itself.
        final String packageName = mContext.packageName;
        mContext.packageName = admin1.getPackageName();
        final long callerIdentity = mContext.binder.clearCallingIdentity();
        mContext.binder.callingUid = ownerUid;
        List<String> ownerInstalledCaCerts = dpm.getOwnerInstalledCaCerts(user);
        assertNotNull(ownerInstalledCaCerts);
        assertEquals(1, ownerInstalledCaCerts.size());
        assertTrue(ownerInstalledCaCerts.contains(alias));
        runAsCaller(admin1Context, dpms, (dpm) -> {
            final List<String> installedCaCerts = dpm.getOwnerInstalledCaCerts(callerUser);
            assertEquals(Arrays.asList(alias), installedCaCerts);
            ownerInstalledCaCerts.addAll(installedCaCerts);
        });

        // Restarting the DPMS should not lose information.
        initializeDpms();
        assertEquals(ownerInstalledCaCerts, dpm.getOwnerInstalledCaCerts(user));
        runAsCaller(admin1Context, dpms, (dpm) -> {
            assertEquals(ownerInstalledCaCerts, dpm.getOwnerInstalledCaCerts(callerUser));
        });

        // System can find out which CA certs were installed by the Device Owner / Profile Owner.
        mContext.packageName = "com.android.frameworks.servicestests";
        mContext.binder.clearCallingIdentity();
        assertEquals(ownerInstalledCaCerts, dpm.getOwnerInstalledCaCerts(user));
        runAsCaller(serviceContext, dpms, (dpm) -> {
            assertEquals(ownerInstalledCaCerts, dpm.getOwnerInstalledCaCerts(callerUser));

            // Remove the CA cert.
        mContext.packageName = packageName;
        mContext.binder.restoreCallingIdentity(callerIdentity);
            reset(mContext.keyChainConnection.getService());
        mContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED));
        });

        serviceContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED)
                .putExtra(Intent.EXTRA_USER_HANDLE, callerUser.getIdentifier()));
        flushTasks();

        // Verify that the CA cert is no longer reported as installed by the Device Owner / Profile
        // Owner.
        mContext.packageName = admin1.getPackageName();
        mContext.binder.callingUid = ownerUid;
        ownerInstalledCaCerts = dpm.getOwnerInstalledCaCerts(user);
        assertNotNull(ownerInstalledCaCerts);
        assertTrue(ownerInstalledCaCerts.isEmpty());
        runAsCaller(admin1Context, dpms, (dpm) -> {
            MoreAsserts.assertEmpty(dpm.getOwnerInstalledCaCerts(callerUser));
        });
    }

        mContext.packageName = packageName;
        mContext.binder.restoreCallingIdentity(callerIdentity);
    private void verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(
            final ComponentName callerName, final DpmMockContext callerContext) throws Exception {
        final String alias = "cert";
        final byte[] caCert = TEST_CA.getBytes();

        // device admin (used for posting the tls notification)
        final DpmMockContext admin1Context;
        if (admin1.getPackageName().equals(callerContext.getPackageName())) {
            admin1Context = callerContext;
        } else {
            admin1Context = new DpmMockContext(mRealTestContext, "test-admin");
            admin1Context.packageName = admin1.getPackageName();
            admin1Context.applicationInfo = new ApplicationInfo();
        }
        when(admin1Context.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE);

    private void verifyCantGetOwnerInstalledCaCertsProfileOwnerRemoval(ComponentName caller)
            throws Exception {
        final UserHandle user = UserHandle.of(DpmMockContext.CALLER_USER_HANDLE);
        // caller: device admin or delegated certificate installer
        callerContext.applicationInfo = new ApplicationInfo();
        final UserHandle callerUser = callerContext.binder.getCallingUserHandle();

        mContext.applicationInfo = new ApplicationInfo();
        mContext.userContexts.put(user, mContext);
        when(mContext.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE);
        // system_server
        final DpmMockContext serviceContext = mContext;
        serviceContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
        serviceContext.addPackageContext(callerUser, admin1Context);
        serviceContext.addPackageContext(callerUser, callerContext);

        // Install a CA cert.
        final String alias = "cert";
        final byte[] caCert = TEST_CA.getBytes();
        // Install a CA cert as caller
        runAsCaller(callerContext, dpms, (dpm) -> {
            when(mContext.keyChainConnection.getService().installCaCertificate(caCert))
                    .thenReturn(alias);
        assertTrue(dpm.installCaCert(caller, caCert));
        when(mContext.keyChainConnection.getService().getUserCaAliases())
            assertTrue(dpm.installCaCert(callerName, caCert));
        });

        // Fake the CA cert as having been installed
        when(serviceContext.keyChainConnection.getService().getUserCaAliases())
                .thenReturn(asSlice(new String[] {alias}));
        mContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED));
        serviceContext.injectBroadcast(new Intent(KeyChain.ACTION_TRUST_STORE_CHANGED)
                .putExtra(Intent.EXTRA_USER_HANDLE, callerUser.getIdentifier()));
        flushTasks();

        // Removing the Profile Owner should clear the information which CA certs were installed
        // by it.
        mContext.packageName = admin1.getPackageName();
        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
        // Removing the Profile Owner should clear the information on which CA certs were installed
        runAsCaller(admin1Context, dpms, (dpm) -> {
            dpm.clearProfileOwner(admin1);
        mContext.packageName = "com.android.frameworks.servicestests";
        mContext.binder.clearCallingIdentity();
        final List<String> ownerInstalledCaCerts = dpm.getOwnerInstalledCaCerts(user);
        });

        runAsCaller(serviceContext, dpms, (dpm) -> {
            final List<String> ownerInstalledCaCerts = dpm.getOwnerInstalledCaCerts(callerUser);
            assertNotNull(ownerInstalledCaCerts);
            assertTrue(ownerInstalledCaCerts.isEmpty());
        });
    }

    private void setUserSetupCompleteForUser(boolean isUserSetupComplete, int userhandle) {
@@ -4147,29 +4199,11 @@ public class DevicePolicyManagerTest extends DpmTestBase {
    }

    private void flushTasks() throws Exception {
        Boolean tasksFlushed[] = new Boolean[] {false};
        final Runnable tasksFlushedNotifier = () -> {
            synchronized (tasksFlushed) {
                tasksFlushed[0] = true;
                tasksFlushed.notify();
            }
        };
        dpms.mHandler.runWithScissors(() -> {}, 0 /*now*/);
        dpms.mBackgroundHandler.runWithScissors(() -> {}, 0 /*now*/);

        // Flush main thread handler.
        dpms.mHandler.post(tasksFlushedNotifier);
        synchronized (tasksFlushed) {
            if (!tasksFlushed[0]) {
                tasksFlushed.wait();
            }
        }

        // Flush background thread handler.
        tasksFlushed[0] = false;
        dpms.mBackgroundHandler.post(tasksFlushedNotifier);
        synchronized (tasksFlushed) {
            if (!tasksFlushed[0]) {
                tasksFlushed.wait();
            }
        }
        // We can't let exceptions happen on the background thread. Throw them here if they happen
        // so they still cause the test to fail despite being suppressed.
        mContext.rethrowBackgroundBroadcastExceptions();
    }
}
+57 −14
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import android.telephony.TelephonyManager;
import android.test.mock.MockContentResolver;
import android.test.mock.MockContext;
import android.util.ArrayMap;
import android.util.Pair;
import android.view.IWindowManager;

import com.android.internal.widget.LockPatternUtils;
@@ -61,6 +62,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
@@ -111,6 +113,7 @@ public class DpmMockContext extends MockContext {
    public static class MockBinder {
        public int callingUid = CALLER_UID;
        public int callingPid = CALLER_PID;
        public final Map<Integer, List<String>> callingPermissions = new ArrayMap<>();

        public long clearCallingIdentity() {
            final long token = (((long) callingUid) << 32) | (callingPid);
@@ -303,14 +306,19 @@ public class DpmMockContext extends MockContext {
    /** Note this is a partial mock, not a real mock. */
    public final PackageManager packageManager;

    /** TODO: Migrate everything to use {@link #permissions} to avoid confusion. */
    @Deprecated
    public final List<String> callerPermissions = new ArrayList<>();

    /** Less confusing alias for {@link #callerPermissions}. */
    public final List<String> permissions = callerPermissions;

    private final ArrayList<UserInfo> mUserInfos = new ArrayList<>();

    public final BuildMock buildMock = new BuildMock();

    /** Optional mapping of other user contexts for {@link #createPackageContextAsUser} to return */
    public final Map<UserHandle, Context> userContexts = new ArrayMap<>();
    public final Map<Pair<UserHandle, String>, Context> userPackageContexts = new ArrayMap<>();

    public String packageName = null;

@@ -324,6 +332,9 @@ public class DpmMockContext extends MockContext {
        public final IntentFilter filter;
        public final Handler scheduler;

        // Exceptions thrown in a background thread kill the whole test. Save them instead.
        public final AtomicReference<Exception> backgroundException = new AtomicReference<>();

        public BroadcastReceiverRegistration(BroadcastReceiver receiver, IntentFilter filter,
                Handler scheduler) {
            this.receiver = receiver;
@@ -337,20 +348,30 @@ public class DpmMockContext extends MockContext {
                    0 /* type */, false /* ordered */, false /* sticky */, null /* token */, userId,
                    0 /* flags */);
            if (filter.match(null, intent, false, "DpmMockContext") > 0) {
                if (scheduler != null) {
                    scheduler.post(() -> {
                final Runnable send = () -> {
                    receiver.setPendingResult(result);
                    receiver.onReceive(DpmMockContext.this, intent);
                };
                if (scheduler != null) {
                    scheduler.post(() -> {
                        try {
                            send.run();
                        } catch (Exception e) {
                            backgroundException.compareAndSet(null, e);
                        }
                    });
                } else {
                    receiver.setPendingResult(result);
                    receiver.onReceive(DpmMockContext.this, intent);
                    send.run();
                }
            }
        }
    }
    private List<BroadcastReceiverRegistration> mBroadcastReceivers = new ArrayList<>();

    public DpmMockContext(Context realTestContext, String name) {
        this(realTestContext, new File(realTestContext.getCacheDir(), name));
    }

    public DpmMockContext(Context context, File dataDir) {
        realTestContext = context;

@@ -511,13 +532,29 @@ public class DpmMockContext extends MockContext {
                .thenReturn(isRunning);
    }

    public void injectBroadcast(Intent intent) {
    public void injectBroadcast(final Intent intent) {
        final int userId = UserHandle.getUserId(binder.getCallingUid());
        for (final BroadcastReceiverRegistration receiver : mBroadcastReceivers) {
            receiver.sendBroadcastIfApplicable(userId, intent);
        }
    }

    public void rethrowBackgroundBroadcastExceptions() throws Exception {
        for (final BroadcastReceiverRegistration receiver : mBroadcastReceivers) {
            final Exception e = receiver.backgroundException.getAndSet(null);
            if (e != null) {
                throw e;
            }
        }
    }

    public void addPackageContext(UserHandle user, Context context) {
        if (context.getPackageName() == null) {
            throw new NullPointerException("getPackageName() == null");
        }
        userPackageContexts.put(new Pair<>(user, context.getPackageName()), context);
    }

    @Override
    public Resources getResources() {
        return resources;
@@ -576,7 +613,16 @@ public class DpmMockContext extends MockContext {
        if (binder.getCallingUid() == SYSTEM_UID) {
            return; // Assume system has all permissions.
        }
        if (!callerPermissions.contains(permission)) {

        List<String> permissions = binder.callingPermissions.get(binder.getCallingUid());
        if (permissions == null) {
            // TODO: delete the following line. to do this without breaking any tests, first it's
            //       necessary to remove all tests that set it directly.
            permissions = callerPermissions;
//            throw new UnsupportedOperationException(
//                    "Caller UID " + binder.getCallingUid() + " doesn't exist");
        }
        if (!permissions.contains(permission)) {
            throw new SecurityException("Caller doesn't have " + permission + " : " + message);
        }
    }
@@ -751,14 +797,11 @@ public class DpmMockContext extends MockContext {
    @Override
    public Context createPackageContextAsUser(String packageName, int flags, UserHandle user)
            throws PackageManager.NameNotFoundException {
        if (!userContexts.containsKey(user)) {
            return super.createPackageContextAsUser(packageName, flags, user);
        }
        if (!getPackageName().equals(packageName)) {
            throw new UnsupportedOperationException(
                    "Creating a context as another package is not implemented");
        final Pair<UserHandle, String> key = new Pair<>(user, packageName);
        if (userPackageContexts.containsKey(key)) {
            return userPackageContexts.get(key);
        }
        return userContexts.get(user);
        throw new UnsupportedOperationException("No package " + packageName + " for user " + user);
    }

    @Override
+29 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.devicepolicy;

import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -70,6 +71,34 @@ public abstract class DpmTestBase extends AndroidTestCase {
        return mMockContext;
    }

    protected interface DpmRunnable {
        public void run(DevicePolicyManager dpm) throws Exception;
    }

    /**
     * Simulate an RPC from {@param caller} to the service context ({@link #mContext}).
     *
     * The caller sees its own context. The server also sees its own separate context, with the
     * appropriate calling UID and calling permissions fields already set up.
     */
    protected void runAsCaller(DpmMockContext caller, DevicePolicyManagerServiceTestable dpms,
            DpmRunnable action) {
        final DpmMockContext serviceContext = mMockContext;

        final long origId = serviceContext.binder.clearCallingIdentity();
        try {
            serviceContext.binder.callingUid = caller.binder.callingUid;
            serviceContext.binder.callingPid = caller.binder.callingPid;
            serviceContext.binder.callingPermissions.put(caller.binder.callingUid,
                    caller.permissions);
            action.run(new DevicePolicyManagerTestable(caller, dpms));
        } catch (Exception e) {
            throw new AssertionError(e);
        } finally {
            serviceContext.binder.restoreCallingIdentity(origId);
        }
    }

    protected void markPackageAsInstalled(String packageName, ApplicationInfo ai, int userId)
            throws Exception {
        final PackageInfo pi = DpmTestUtils.cloneParcelable(