Wiping and relinquishing org-owned devices

Add the following functionality, on devices with a managed profile
created during provisionining (and as such, considered
organization-owned):

* Let the Profile Owner relinquish a device by calling
  DevicePolicyManager.wipeData. The device then transitions
  to a fully-personal device.
* Let the Profile Owner wipe the entire device by calling
  wipeData on the parent profile DevicePolicyManager instance.

Bug: 138709470
Test: Manual with TestDPC.
Test: atest CtsDevicePolicyManagerTestCases:com.android.cts.devicepolicy.OrgOwnedProfileOwnerTest
Test: atest com.android.cts.devicepolicy.MixedManagedProfileOwnerTest#testDeviceIdAttestationForProfileOwner
Change-Id: If3cc9741079592cb07bc1ef5ccca8fb2b57a52e9
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 50adb7c..9a5444c 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -88,7 +88,6 @@
 import android.util.ArraySet;
 import android.util.Log;
 
-import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.Preconditions;
@@ -4198,6 +4197,12 @@
      * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
      * be able to call this method; if it has not, a security exception will be thrown.
      *
+     * If the caller is a profile owner of an organization-owned managed profile, it may
+     * additionally call this method on the parent instance.
+     * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
+     * entire device, while calling it on the current profile instance would relinquish the device
+     * for personal use, removing the work profile and all policies set by the profile owner.
+     *
      * @param flags Bit mask of additional options: currently supported flags are
      *            {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
      *            {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
@@ -4205,10 +4210,7 @@
      *            that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}
      */
     public void wipeData(int flags) {
-        throwIfParentInstance("wipeData");
-        final String wipeReasonForUser = mContext.getString(
-                R.string.work_profile_deleted_description_dpm_wipe);
-        wipeDataInternal(flags, wipeReasonForUser);
+        wipeDataInternal(flags, "");
     }
 
     /**
@@ -4221,6 +4223,12 @@
      * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
      * be able to call this method; if it has not, a security exception will be thrown.
      *
+     * If the caller is a profile owner of an organization-owned managed profile, it may
+     * additionally call this method on the parent instance.
+     * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
+     * entire device, while calling it on the current profile instance would relinquish the device
+     * for personal use, removing the work profile and all policies set by the profile owner.
+     *
      * @param flags Bit mask of additional options: currently supported flags are
      *            {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA} and
      *            {@link #WIPE_EUICC}.
@@ -4232,7 +4240,6 @@
      *            {@link #WIPE_SILENTLY} is set.
      */
     public void wipeData(int flags, @NonNull CharSequence reason) {
-        throwIfParentInstance("wipeData");
         Preconditions.checkNotNull(reason, "reason string is null");
         Preconditions.checkStringNotEmpty(reason, "reason string is empty");
         Preconditions.checkArgument((flags & WIPE_SILENTLY) == 0, "WIPE_SILENTLY cannot be set");
@@ -4250,7 +4257,7 @@
     private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser) {
         if (mService != null) {
             try {
-                mService.wipeDataWithReason(flags, wipeReasonForUser);
+                mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -9287,6 +9294,12 @@
      * <li>{@link #setRequiredStrongAuthTimeout}</li>
      * </ul>
      *
+     * <p>The following methods can be called by the profile owner of a managed profile
+     * on an organization-owned device:
+     * <ul>
+     * <li>{@link #wipeData}</li>
+     * </ul>
+     *
      * @return a new instance of {@link DevicePolicyManager} that acts on the parent profile.
      * @throws SecurityException if {@code admin} is not a profile owner.
      */
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 4894751..591d151 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -102,7 +102,7 @@
 
     void lockNow(int flags, boolean parent);
 
-    void wipeDataWithReason(int flags, String wipeReasonForUser);
+    void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent);
 
     ComponentName setGlobalProxy(in ComponentName admin, String proxySpec, String exclusionList);
     ComponentName getGlobalProxyAdmin(int userHandle);
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index a5bbab6..a0aa186 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -404,6 +404,8 @@
     <!-- Content text for a notification. The Title of the notification is "Work profile deleted",
         This indicates that a work profile has been deleted because the maximum failed password attempts as been reached. [CHAR LIMIT=NONE]-->
     <string name="work_profile_deleted_reason_maximum_password_failure">Too many password attempts</string>
+    <!-- Shows up as the reason for the work profile deletion when the admin of an organization-owend device relinquishes it. [CHAR LIMIT=NONE] -->
+    <string name="device_ownership_relinquished">Admin relinquished device for personal use</string>
 
     <!-- Content title for a notification. This notification indicates that the device is managed
          and network logging was activated by a device owner. [CHAR LIMIT=NONE]-->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 5a1c652..1d51339 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1193,6 +1193,7 @@
   <java-symbol type="string" name="work_profile_deleted_details" />
   <java-symbol type="string" name="work_profile_deleted_description_dpm_wipe" />
   <java-symbol type="string" name="work_profile_deleted_reason_maximum_password_failure" />
+  <java-symbol type="string" name="device_ownership_relinquished" />
   <java-symbol type="string" name="network_logging_notification_title" />
   <java-symbol type="string" name="network_logging_notification_text" />
   <java-symbol type="string" name="factory_reset_warning" />
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 99f484e..ea987c0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -5568,6 +5568,13 @@
         }
     }
 
+    private void enforceProfileOwnerOfCorpOwnedDevice(ActiveAdmin admin) {
+        if (!isProfileOwnerOfOrganizationOwnedDevicte(admin)) {
+            throw new SecurityException(String.format("Provided admin %s is either not a profile "
+                    + "owner or not on a corporate-owned device.", admin));
+        }
+    }
+
     @Override
     public boolean approveCaCert(String alias, int userId, boolean approval) {
         enforceManageUsers();
@@ -6613,27 +6620,83 @@
     }
 
     @Override
-    public void wipeDataWithReason(int flags, String wipeReasonForUser) {
+    public void wipeDataWithReason(int flags, String wipeReasonForUser,
+            boolean calledOnParentInstance) {
         if (!mHasFeature) {
             return;
         }
-        Preconditions.checkStringNotEmpty(wipeReasonForUser, "wipeReasonForUser is null or empty");
+
         enforceFullCrossUsersPermission(mInjector.userHandleGetCallingUserId());
 
         final ActiveAdmin admin;
         synchronized (getLockObject()) {
             admin = getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_WIPE_DATA);
         }
+
+        if (admin == null) {
+            throw new SecurityException(String.format("No active admin for user %d",
+                    mInjector.userHandleGetCallingUserId()));
+        }
+
+        boolean calledByProfileOwnerOnOrgOwnedDevice =
+                isProfileOwnerOfOrganizationOwnedDevicte(admin);
+
+        if (calledOnParentInstance && !calledByProfileOwnerOnOrgOwnedDevice) {
+            throw new SecurityException("Wiping the entire device can only be done by a profile"
+                    + "owner on organization-owned device.");
+        }
+
+        if ((flags & WIPE_RESET_PROTECTION_DATA) != 0) {
+            if (!isDeviceOwner(admin) && !calledByProfileOwnerOnOrgOwnedDevice) {
+                throw new SecurityException(
+                        "Only device owners or proflie owners of organization-owned device"
+                        + " can set WIPE_RESET_PROTECTION_DATA");
+            }
+        }
+
+        if (TextUtils.isEmpty(wipeReasonForUser)) {
+            if (calledByProfileOwnerOnOrgOwnedDevice && !calledOnParentInstance) {
+                wipeReasonForUser = mContext.getString(R.string.device_ownership_relinquished);
+            } else {
+                wipeReasonForUser = mContext.getString(
+                        R.string.work_profile_deleted_description_dpm_wipe);
+            }
+        }
+
+        int userId = admin.getUserHandle().getIdentifier();
+        if (calledByProfileOwnerOnOrgOwnedDevice) {
+            // When wipeData is called on the parent instance, it implies wiping the entire device.
+            if (calledOnParentInstance) {
+                userId = UserHandle.USER_SYSTEM;
+            } else {
+                // when wipeData is _not_ called on the parent instance, it implies relinquishing
+                // control over the device, wiping only the work profile. So the user restriction
+                // on profile removal needs to be removed first.
+
+                final long ident = mInjector.binderClearCallingIdentity();
+                try {
+                    // Clear restriction as user.
+                    mUserManager.setUserRestriction(
+                            UserManager.DISALLOW_REMOVE_MANAGED_PROFILE, false,
+                            UserHandle.SYSTEM);
+                } finally {
+                    mInjector.binderRestoreCallingIdentity(ident);
+                }
+            }
+        }
+
         DevicePolicyEventLogger
                 .createEvent(DevicePolicyEnums.WIPE_DATA_WITH_REASON)
                 .setAdmin(admin.info.getComponent())
                 .setInt(flags)
                 .write();
-        String internalReason = "DevicePolicyManager.wipeDataWithReason() from "
-                + admin.info.getComponent().flattenToShortString();
+        String internalReason = String.format(
+                "DevicePolicyManager.wipeDataWithReason() from %s, organization-owned? %s",
+                admin.info.getComponent().flattenToShortString(),
+                calledByProfileOwnerOnOrgOwnedDevice);
+
         wipeDataNoLock(
-                admin.info.getComponent(), flags, internalReason, wipeReasonForUser,
-                admin.getUserHandle().getIdentifier());
+                admin.info.getComponent(), flags, internalReason, wipeReasonForUser, userId);
     }
 
     private void wipeDataNoLock(ComponentName admin, int flags, String internalReason,
@@ -6657,10 +6720,6 @@
             }
 
             if ((flags & WIPE_RESET_PROTECTION_DATA) != 0) {
-                if (!isDeviceOwner(admin, userId)) {
-                    throw new SecurityException(
-                            "Only device owner admins can set WIPE_RESET_PROTECTION_DATA");
-                }
                 PersistentDataBlockManager manager = (PersistentDataBlockManager)
                         mContext.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE);
                 if (manager != null) {
@@ -7954,6 +8013,35 @@
         }
     }
 
+    /**
+     * Returns true if the provided {@code admin} is a profile owner and the profile is marked
+     * as organization-owned.
+     * The {@code admin} parameter must be obtained by the service by calling
+     * {@code getActiveAdminForCallerLocked} or one of the similar variants, not caller-supplied
+     * input.
+     */
+    private boolean isProfileOwnerOfOrganizationOwnedDevicte(@Nullable ActiveAdmin admin) {
+        if (admin == null) {
+            return false;
+        }
+
+        final int adminUserId = admin.getUserHandle().getIdentifier();
+
+        if (!isProfileOwner(admin.info.getComponent(), adminUserId)) {
+            Slog.w(LOG_TAG, String.format("%s is not profile owner of user %d",
+                    admin.info.getComponent(), adminUserId));
+            return false;
+        }
+
+        if (!canProfileOwnerAccessDeviceIds(adminUserId)) {
+            Slog.w(LOG_TAG, String.format("Profile owner of user %d does not own the device.",
+                    adminUserId));
+            return false;
+        }
+
+        return true;
+    }
+
     @Override
     public ComponentName getDeviceOwnerComponent(boolean callingUserOnly) {
         if (!mHasFeature) {
@@ -12614,6 +12702,24 @@
             Slog.i(LOG_TAG, String.format("Granting Device ID access to %s, for user %d",
                         who.flattenToString(), userId));
 
+            // First, set restriction on removing the profile.
+            final long ident = mInjector.binderClearCallingIdentity();
+            try {
+                // Clear restriction as user.
+                UserHandle parentUser = mUserManager.getProfileParent(UserHandle.of(userId));
+                if (!parentUser.isSystem()) {
+                    throw new IllegalStateException(
+                            String.format("Only the profile owner of a managed profile on the"
+                                + " primary user can be granted access to device identifiers, not"
+                                + " on user %d", parentUser.getIdentifier()));
+                }
+
+                mUserManager.setUserRestriction(UserManager.DISALLOW_REMOVE_MANAGED_PROFILE, true,
+                        parentUser);
+            } finally {
+                mInjector.binderRestoreCallingIdentity(ident);
+            }
+
             // setProfileOwnerCanAccessDeviceIds will trigger writing of the profile owner
             // data, no need to do it manually.
             mOwners.setProfileOwnerCanAccessDeviceIds(userId);
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 f270724..eef77ee 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -5162,11 +5162,14 @@
         configureProfileOwnerForDeviceIdAccess(admin1, DpmMockContext.CALLER_USER_HANDLE);
     }
 
-    private static void configureContextForAccess(DpmMockContext context, boolean granted) {
+    private void configureContextForAccess(DpmMockContext context, boolean granted) {
         when(context.spiedContext.checkCallingPermission(
                 android.Manifest.permission.GRANT_PROFILE_OWNER_DEVICE_IDS_ACCESS))
                 .thenReturn(granted ? PackageManager.PERMISSION_GRANTED
                         : PackageManager.PERMISSION_DENIED);
+
+        when(getServices().userManager.getProfileParent(any()))
+                .thenReturn(UserHandle.SYSTEM);
     }
 
     public void testGrantDeviceIdsAccess_byAuthorizedManagedProvisioning() throws Exception {
@@ -5433,6 +5436,9 @@
     }
 
     private void configureProfileOwnerForDeviceIdAccess(ComponentName who, int userId) {
+        when(getServices().userManager.getProfileParent(eq(UserHandle.of(userId))))
+                .thenReturn(UserHandle.SYSTEM);
+
         final long ident = mServiceContext.binder.clearCallingIdentity();
         mServiceContext.binder.callingUid =
                 UserHandle.getUid(DpmMockContext.CALLER_USER_HANDLE, DpmMockContext.SYSTEM_UID);