Implement user affiliation

A user/profile is considered affiliated if it is managed by the same
entity as the device. This is determined by having the device owner and
profile owners specify a set of opaque affiliation ids each. If the sets
intersect, they must have come from the same source, which means that the
device owner and profile owner are controlled by the same entity.

BUG=25599229

Change-Id: I393fe0de70272307ed3c811aaba4b48a5109c562
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a655bfd..53f7b29 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -67,6 +67,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Public interface for managing policies enforced on a device. Most clients of this class must be
@@ -5193,4 +5194,46 @@
             return 0;
         }
     }
+
+    /**
+     * @hide
+     * Indicates the entity that controls the device or profile owner. A user/profile is considered
+     * affiliated if it is managed by the same entity as the device.
+     *
+     * <p> By definition, the user that the device owner runs on is always affiliated. Any other
+     * user/profile is considered affiliated if the following conditions are both met:
+     * <ul>
+     * <li>The device owner and the user's/profile's profile owner have called this method,
+     *   specifying a set of opaque affiliation ids each. If the sets specified by the device owner
+     *   and a profile owner intersect, they must have come from the same source, which means that
+     *   the device owner and profile owner are controlled by the same entity.</li>
+     * <li>The device owner's and profile owner's package names are the same.</li>
+     * </ul>
+     *
+     * @param admin Which profile or device owner this request is associated with.
+     * @param ids A set of opaque affiliation ids.
+     */
+    public void setAffiliationIds(@NonNull ComponentName admin, Set<String> ids) {
+        try {
+            mService.setAffiliationIds(admin, new ArrayList<String>(ids));
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed talking with device policy service", e);
+        }
+    }
+
+    /**
+     * @hide
+     * Returns whether this user/profile is affiliated with the device. See
+     * {@link #setAffiliationIds} for the definition of affiliation.
+     *
+     * @return whether this user/profile is affiliated with the device.
+     */
+    public boolean isAffiliatedUser() {
+        try {
+            return mService != null && mService.isAffiliatedUser();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed talking with device policy service", e);
+            return false;
+        }
+    }
 }
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 82115a2..57865f4 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -267,4 +267,7 @@
     void setOrganizationColor(in ComponentName admin, in int color);
     int getOrganizationColor(in ComponentName admin);
     int getOrganizationColorForUser(int userHandle);
+
+    void setAffiliationIds(in ComponentName admin, in List<String> ids);
+    boolean isAffiliatedUser();
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index f68a299..7232562 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -176,6 +176,8 @@
 
     private static final String TAG_STATUS_BAR = "statusbar";
 
+    private static final String TAG_AFFILIATION_ID = "affiliation-id";
+
     private static final String ATTR_DISABLED = "disabled";
 
     private static final String DO_NOT_ASK_CREDENTIALS_ON_BOOT_XML =
@@ -372,6 +374,8 @@
 
         String mApplicationRestrictionsManagingPackage;
 
+        Set<String> mAffiliationIds = new ArraySet<>();
+
         public DevicePolicyData(int userHandle) {
             mUserHandle = userHandle;
         }
@@ -2026,6 +2030,12 @@
                 out.endTag(null, DO_NOT_ASK_CREDENTIALS_ON_BOOT_XML);
             }
 
+            for (String id : policy.mAffiliationIds) {
+                out.startTag(null, TAG_AFFILIATION_ID);
+                out.attribute(null, "id", id);
+                out.endTag(null, TAG_AFFILIATION_ID);
+            }
+
             out.endTag(null, "policies");
 
             out.endDocument();
@@ -2100,6 +2110,7 @@
             policy.mLockTaskPackages.clear();
             policy.mAdminList.clear();
             policy.mAdminMap.clear();
+            policy.mAffiliationIds.clear();
             while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
                 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
@@ -2157,6 +2168,8 @@
                             parser.getAttributeValue(null, ATTR_DISABLED));
                 } else if (DO_NOT_ASK_CREDENTIALS_ON_BOOT_XML.equals(tag)) {
                     policy.doNotAskCredentialsOnBoot = true;
+                } else if (TAG_AFFILIATION_ID.equals(tag)) {
+                    policy.mAffiliationIds.add(parser.getAttributeValue(null, "id"));
                 } else {
                     Slog.w(LOG_TAG, "Unknown tag: " + tag);
                     XmlUtils.skipCurrentTag(parser);
@@ -7741,4 +7754,48 @@
                     : ActiveAdmin.DEF_ORGANIZATION_COLOR;
         }
     }
+
+    @Override
+    public void setAffiliationIds(ComponentName admin, List<String> ids) {
+        final Set<String> affiliationIds = new ArraySet<String>(ids);
+        final int callingUserId = mInjector.userHandleGetCallingUserId();
+
+        synchronized (this) {
+            getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+            getUserData(callingUserId).mAffiliationIds = affiliationIds;
+            saveSettingsLocked(callingUserId);
+            if (callingUserId != UserHandle.USER_SYSTEM && isDeviceOwner(admin, callingUserId)) {
+                // Affiliation ids specified by the device owner are additionally stored in
+                // UserHandle.USER_SYSTEM's DevicePolicyData.
+                getUserData(UserHandle.USER_SYSTEM).mAffiliationIds = affiliationIds;
+                saveSettingsLocked(UserHandle.USER_SYSTEM);
+            }
+        }
+    }
+
+    @Override
+    public boolean isAffiliatedUser() {
+        final int callingUserId = mInjector.userHandleGetCallingUserId();
+
+        synchronized (this) {
+            if (mOwners.getDeviceOwnerUserId() == callingUserId) {
+                // The user that the DO is installed on is always affiliated.
+                return true;
+            }
+            final ComponentName profileOwner = getProfileOwner(callingUserId);
+            if (profileOwner == null
+                    || !profileOwner.getPackageName().equals(mOwners.getDeviceOwnerPackageName())) {
+                return false;
+            }
+            final Set<String> userAffiliationIds = getUserData(callingUserId).mAffiliationIds;
+            final Set<String> deviceAffiliationIds =
+                    getUserData(UserHandle.USER_SYSTEM).mAffiliationIds;
+            for (String id : userAffiliationIds) {
+                if (deviceAffiliationIds.contains(id)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 672058b..536fb70 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -34,6 +34,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.test.MoreAsserts;
+import android.util.ArraySet;
 import android.util.Pair;
 
 import org.mockito.ArgumentCaptor;
@@ -44,6 +45,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
@@ -1476,4 +1478,69 @@
             assertNull(dpm.getLongSupportMessage(admin1));
         }
     }
+
+    /**
+     * Test for:
+     * {@link DevicePolicyManager#setAffiliationIds}
+     * {@link DevicePolicyManager#isAffiliatedUser}
+     */
+    public void testUserAffiliation() throws Exception {
+        mContext.callerPermissions.add(permission.MANAGE_DEVICE_ADMINS);
+        mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS);
+        mContext.callerPermissions.add(permission.INTERACT_ACROSS_USERS_FULL);
+
+        // Check that the system user is unaffiliated.
+        mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
+        assertFalse(dpm.isAffiliatedUser());
+
+        // Set a device owner on the system user. Check that the system user becomes affiliated.
+        setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_SYSTEM_USER_UID);
+        dpm.setActiveAdmin(admin1, /* replace =*/ false);
+        assertTrue(dpm.setDeviceOwner(admin1, "owner-name"));
+        assertTrue(dpm.isAffiliatedUser());
+
+        // Install a profile owner whose package name matches the device owner on a test user. Check
+        // that the test user is unaffiliated.
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+        setAsProfileOwner(admin2);
+        assertFalse(dpm.isAffiliatedUser());
+
+        // Have the profile owner specify a set of affiliation ids. Check that the test user remains
+        // unaffiliated.
+        final Set<String> userAffiliationIds = new ArraySet<>();
+        userAffiliationIds.add("red");
+        userAffiliationIds.add("green");
+        userAffiliationIds.add("blue");
+        dpm.setAffiliationIds(admin2, userAffiliationIds);
+        assertFalse(dpm.isAffiliatedUser());
+
+        // Have the device owner specify a set of affiliation ids that do not intersect with those
+        // specified by the profile owner. Check that the test user remains unaffiliated.
+        final Set<String> deviceAffiliationIds = new ArraySet<>();
+        deviceAffiliationIds.add("cyan");
+        deviceAffiliationIds.add("yellow");
+        deviceAffiliationIds.add("magenta");
+        mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
+        dpm.setAffiliationIds(admin1, deviceAffiliationIds);
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+        assertFalse(dpm.isAffiliatedUser());
+
+        // Have the profile owner specify a set of affiliation ids that intersect with those
+        // specified by the device owner. Check that the test user becomes affiliated.
+        userAffiliationIds.add("yellow");
+        dpm.setAffiliationIds(admin2, userAffiliationIds);
+        assertTrue(dpm.isAffiliatedUser());
+
+        // Change the profile owner to one whose package name does not match the device owner. Check
+        // that the test user is not affiliated anymore.
+        dpm.clearProfileOwner(admin2);
+        final ComponentName admin = new ComponentName("test", "test");
+        markPackageAsInstalled(admin.getPackageName(), null, DpmMockContext.CALLER_USER_HANDLE);
+        assertTrue(dpm.setProfileOwner(admin, "owner-name", DpmMockContext.CALLER_USER_HANDLE));
+        assertFalse(dpm.isAffiliatedUser());
+
+        // Check that the system user remains affiliated.
+        mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
+        assertTrue(dpm.isAffiliatedUser());
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmTestBase.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmTestBase.java
index c557ab7..3dc1a9a 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmTestBase.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmTestBase.java
@@ -65,6 +65,22 @@
         return mMockContext;
     }
 
+    protected void markPackageAsInstalled(String packageName, ApplicationInfo ai, int userId)
+            throws Exception {
+        final PackageInfo pi = DpmTestUtils.cloneParcelable(
+                mRealTestContext.getPackageManager().getPackageInfo(
+                        mRealTestContext.getPackageName(), 0));
+        assertTrue(pi.applicationInfo.flags != 0);
+
+        if (ai != null) {
+            pi.applicationInfo = ai;
+        }
+
+        doReturn(pi).when(mMockContext.ipackageManager).getPackageInfo(
+                eq(packageName),
+                eq(0),
+                eq(userId));
+    }
 
     protected void setUpPackageManagerForAdmin(ComponentName admin, int packageUid)
             throws Exception {
@@ -124,17 +140,6 @@
                 eq(UserHandle.getUserId(packageUid)));
 
         // Set up getPackageInfo().
-
-        final PackageInfo pi = DpmTestUtils.cloneParcelable(
-                mRealTestContext.getPackageManager().getPackageInfo(
-                        admin.getPackageName(), 0));
-        assertTrue(pi.applicationInfo.flags != 0);
-
-        pi.applicationInfo = ai;
-
-        doReturn(pi).when(mMockContext.ipackageManager).getPackageInfo(
-                eq(admin.getPackageName()),
-                eq(0),
-                eq(UserHandle.getUserId(packageUid)));
+        markPackageAsInstalled(admin.getPackageName(), ai, UserHandle.getUserId(packageUid));
     }
 }