From afad8783699b1ba6f3c7ee5961d6ddc2bd771dc1 Mon Sep 17 00:00:00 2001 From: Alan Treadway Date: Tue, 19 Jan 2016 15:15:08 +0000 Subject: [PATCH] Add explicit and persistent user provisioning state. Add explicit modelling of provisioning state so that integration of management provisioning flows with packages such as setup-wizard are cleaner, and can be more direct. Previously we relied upon USER_SETUP_COMPLETE secure setting and HOME intents to signal intent, but this is not very clear and can be fragile. Bug: 25858670 Change-Id: Idc56a040f710c3aee281db420f21717da3960722 --- .../dpm/src/com/android/commands/dpm/Dpm.java | 11 +- .../app/admin/DevicePolicyManager.java | 90 ++++++++++ .../app/admin/IDevicePolicyManager.aidl | 3 + .../DevicePolicyManagerService.java | 106 ++++++++++- .../devicepolicy/DevicePolicyManagerTest.java | 167 +++++++++++++++++- 5 files changed, 370 insertions(+), 7 deletions(-) diff --git a/cmds/dpm/src/com/android/commands/dpm/Dpm.java b/cmds/dpm/src/com/android/commands/dpm/Dpm.java index 6dc3cd1d48e00..b83484d998249 100644 --- a/cmds/dpm/src/com/android/commands/dpm/Dpm.java +++ b/cmds/dpm/src/com/android/commands/dpm/Dpm.java @@ -18,6 +18,7 @@ package com.android.commands.dpm; import android.app.ActivityManagerNative; import android.app.IActivityManager; +import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; import android.content.ComponentName; import android.content.Context; @@ -143,6 +144,10 @@ public final class Dpm extends BaseCommand { mDevicePolicyManager.removeActiveAdmin(mComponent, UserHandle.USER_SYSTEM); throw e; } + + mDevicePolicyManager.setUserProvisioningState( + DevicePolicyManager.STATE_USER_SETUP_FINALIZED, mUserId); + System.out.println("Success: Device owner set to package " + mComponent); System.out.println("Active admin set to component " + mComponent.toShortString()); } @@ -161,6 +166,10 @@ public final class Dpm extends BaseCommand { mDevicePolicyManager.removeActiveAdmin(mComponent, mUserId); throw e; } + + mDevicePolicyManager.setUserProvisioningState( + DevicePolicyManager.STATE_USER_SETUP_FINALIZED, mUserId); + System.out.println("Success: Active admin and profile owner set to " + mComponent.toShortString() + " for user " + mUserId); } @@ -180,4 +189,4 @@ public final class Dpm extends BaseCommand { throw new IllegalArgumentException ("Invalid integer argument '" + argument + "'", e); } } -} \ No newline at end of file +} diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 25c54fa522d9c..b098d045d19ff 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -16,6 +16,7 @@ package android.app.admin; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; @@ -53,6 +54,8 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.InetSocketAddress; import java.net.Proxy; import java.security.KeyFactory; @@ -279,6 +282,21 @@ public class DevicePolicyManager { public static final String ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE = "android.app.action.PROVISION_MANAGED_SHAREABLE_DEVICE"; + /** + * Activity action: Finalizes management provisioning, should be used after user-setup + * has been completed and {@link #getUserProvisioningState()} returns one of: + * + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PROVISION_FINALIZATION + = "android.app.action.PROVISION_FINALIZATION"; + /** * A {@link android.os.Parcelable} extra of type {@link android.os.PersistableBundle} that * allows a mobile device management application or NFC programmer application which starts @@ -860,6 +878,44 @@ public class DevicePolicyManager { */ public static final int PERMISSION_GRANT_STATE_DENIED = 2; + /** + * No management for current user in-effect. This is the default. + * @hide + */ + public static final int STATE_USER_UNMANAGED = 0; + + /** + * Management partially setup, user setup needs to be completed. + * @hide + */ + public static final int STATE_USER_SETUP_INCOMPLETE = 1; + + /** + * Management partially setup, user setup completed. + * @hide + */ + public static final int STATE_USER_SETUP_COMPLETE = 2; + + /** + * Management setup and active on current user. + * @hide + */ + public static final int STATE_USER_SETUP_FINALIZED = 3; + + /** + * Management partially setup on a managed profile. + * @hide + */ + public static final int STATE_USER_PROFILE_COMPLETE = 4; + + /** + * @hide + */ + @IntDef({STATE_USER_UNMANAGED, STATE_USER_SETUP_INCOMPLETE, STATE_USER_SETUP_COMPLETE, + STATE_USER_SETUP_FINALIZED, STATE_USER_PROFILE_COMPLETE}) + @Retention(RetentionPolicy.SOURCE) + public @interface UserProvisioningState {} + /** * Return true if the given administrator component is currently * active (enabled) in the system. @@ -5202,6 +5258,40 @@ public class DevicePolicyManager { } } + /** + * @return the {@link UserProvisioningState} for the current user - for unmanaged users will + * return {@link #STATE_USER_UNMANAGED} + * @hide + */ + @UserProvisioningState + public int getUserProvisioningState() { + if (mService != null) { + try { + return mService.getUserProvisioningState(); + } catch (RemoteException e) { + Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e); + } + } + return STATE_USER_UNMANAGED; + } + + /** + * Set the {@link UserProvisioningState} for the supplied user, if they are managed. + * + * @param state to store + * @param userHandle for user + * @hide + */ + public void setUserProvisioningState(@UserProvisioningState int state, int userHandle) { + if (mService != null) { + try { + mService.setUserProvisioningState(state, userHandle); + } catch (RemoteException e) { + Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e); + } + } + } + /** * @hide * Indicates the entity that controls the device or profile owner. A user/profile is considered diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 2b378a4fbaff9..25cadf93ad6f6 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -268,6 +268,9 @@ interface IDevicePolicyManager { int getOrganizationColor(in ComponentName admin); int getOrganizationColorForUser(int userHandle); + int getUserProvisioningState(); + void setUserProvisioningState(int state, int userHandle); + void setAffiliationIds(in ComponentName admin, in List 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 74d46592a70ca..696604163337a 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -196,6 +196,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private static final String ATTR_PERMISSION_PROVIDER = "permission-provider"; private static final String ATTR_SETUP_COMPLETE = "setup-complete"; + private static final String ATTR_PROVISIONING_STATE = "provisioning-state"; private static final String ATTR_PERMISSION_POLICY = "permission-policy"; private static final String ATTR_DELEGATED_CERT_INSTALLER = "delegated-cert-installer"; @@ -358,6 +359,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { int mPasswordOwner = -1; long mLastMaximumTimeToLock = -1; boolean mUserSetupComplete = false; + int mUserProvisioningState; int mPermissionPolicy; final ArrayMap mAdminMap = new ArrayMap<>(); @@ -2008,6 +2010,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { out.attribute(null, ATTR_SETUP_COMPLETE, Boolean.toString(true)); } + if (policy.mUserProvisioningState != DevicePolicyManager.STATE_USER_UNMANAGED) { + out.attribute(null, ATTR_PROVISIONING_STATE, + Integer.toString(policy.mUserProvisioningState)); + } if (policy.mPermissionPolicy != DevicePolicyManager.PERMISSION_POLICY_PROMPT) { out.attribute(null, ATTR_PERMISSION_POLICY, Integer.toString(policy.mPermissionPolicy)); @@ -2145,6 +2151,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (userSetupComplete != null && Boolean.toString(true).equals(userSetupComplete)) { policy.mUserSetupComplete = true; } + String provisioningState = parser.getAttributeValue(null, ATTR_PROVISIONING_STATE); + if (!TextUtils.isEmpty(provisioningState)) { + policy.mUserProvisioningState = Integer.parseInt(provisioningState); + } String permissionPolicy = parser.getAttributeValue(null, ATTR_PERMISSION_POLICY); if (!TextUtils.isEmpty(permissionPolicy)) { policy.mPermissionPolicy = Integer.parseInt(permissionPolicy); @@ -5352,6 +5362,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { policy.mDelegatedCertInstallerPackage = null; policy.mApplicationRestrictionsManagingPackage = null; policy.mStatusBarDisabled = false; + policy.mUserProvisioningState = DevicePolicyManager.STATE_USER_UNMANAGED; saveSettingsLocked(userId); final long ident = mInjector.binderClearCallingIdentity(); @@ -5378,6 +5389,98 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return getUserData(userHandle).mUserSetupComplete; } + @Override + public int getUserProvisioningState() { + if (!mHasFeature) { + return DevicePolicyManager.STATE_USER_UNMANAGED; + } + int userHandle = mInjector.userHandleGetCallingUserId(); + return getUserProvisioningState(userHandle); + } + + private int getUserProvisioningState(int userHandle) { + return getUserData(userHandle).mUserProvisioningState; + } + + @Override + public void setUserProvisioningState(int newState, int userHandle) { + if (!mHasFeature) { + return; + } + + if (userHandle != mOwners.getDeviceOwnerUserId() && !mOwners.hasProfileOwner(userHandle) + && getManagedUserId(userHandle) == -1) { + // No managed device, user or profile, so setting provisioning state makes no sense. + throw new IllegalStateException("Not allowed to change provisioning state unless a " + + "device or profile owner is set."); + } + + synchronized (this) { + boolean transitionCheckNeeded = true; + + // Calling identity/permission checks. + final int callingUid = mInjector.binderGetCallingUid(); + if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) { + // ADB shell can only move directly from un-managed to finalized as part of directly + // setting profile-owner or device-owner. + if (getUserProvisioningState(userHandle) != + DevicePolicyManager.STATE_USER_UNMANAGED + || newState != DevicePolicyManager.STATE_USER_SETUP_FINALIZED) { + throw new IllegalStateException("Not allowed to change provisioning state " + + "unless current provisioning state is unmanaged, and new state is " + + "finalized."); + } + transitionCheckNeeded = false; + } else { + // For all other cases, caller must have MANAGE_PROFILE_AND_DEVICE_OWNERS. + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, null); + } + + final DevicePolicyData policyData = getUserData(userHandle); + if (transitionCheckNeeded) { + // Optional state transition check for non-ADB case. + checkUserProvisioningStateTransition(policyData.mUserProvisioningState, newState); + } + policyData.mUserProvisioningState = newState; + saveSettingsLocked(userHandle); + } + } + + private void checkUserProvisioningStateTransition(int currentState, int newState) { + // Valid transitions for normal use-cases. + switch (currentState) { + case DevicePolicyManager.STATE_USER_UNMANAGED: + // Can move to any state from unmanaged (except itself as an edge case).. + if (newState != DevicePolicyManager.STATE_USER_UNMANAGED) { + return; + } + break; + case DevicePolicyManager.STATE_USER_SETUP_INCOMPLETE: + case DevicePolicyManager.STATE_USER_SETUP_COMPLETE: + // Can only move to finalized from these states. + if (newState == DevicePolicyManager.STATE_USER_SETUP_FINALIZED) { + return; + } + break; + case DevicePolicyManager.STATE_USER_PROFILE_COMPLETE: + // Current user has a managed-profile, but current user is not managed, so + // rather than moving to finalized state, go back to unmanaged once + // profile provisioning is complete. + if (newState == DevicePolicyManager.STATE_USER_UNMANAGED) { + return; + } + break; + case DevicePolicyManager.STATE_USER_SETUP_FINALIZED: + // Cannot transition out of finalized. + break; + } + + // Didn't meet any of the accepted state transition checks above, throw appropriate error. + throw new IllegalStateException("Cannot move to user provisioning state [" + newState + "] " + + "from state [" + currentState + "]"); + } + @Override public void setProfileEnabled(ComponentName who) { if (!mHasFeature) { @@ -5697,7 +5800,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { for (int u = 0; u < userCount; u++) { DevicePolicyData policy = getUserData(mUserData.keyAt(u)); pw.println(); - pw.println(" Enabled Device Admins (User " + policy.mUserHandle + "):"); + pw.println(" Enabled Device Admins (User " + policy.mUserHandle + + ", provisioningState: " + policy.mUserProvisioningState + "):"); final int N = policy.mAdminList.size(); for (int i=0; i OWNER_SETUP_PERMISSIONS = Arrays.asList( + permission.MANAGE_DEVICE_ADMINS, permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, + permission.MANAGE_USERS, permission.INTERACT_ACROSS_USERS_FULL); + private DpmMockContext mContext; public DevicePolicyManager dpm; public DevicePolicyManagerServiceTestable dpms; @@ -1543,4 +1548,156 @@ public class DevicePolicyManagerTest extends DpmTestBase { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; assertTrue(dpm.isAffiliatedUser()); } + + public void testGetUserProvisioningState_defaultResult() { + assertEquals(DevicePolicyManager.STATE_USER_UNMANAGED, dpm.getUserProvisioningState()); + } + + public void testSetUserProvisioningState_permission() throws Exception { + setupProfileOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + exerciseUserProvisioningTransitions(DpmMockContext.CALLER_USER_HANDLE, + DevicePolicyManager.STATE_USER_SETUP_FINALIZED); + } + + public void testSetUserProvisioningState_unprivileged() throws Exception { + setupProfileOwner(); + try { + dpm.setUserProvisioningState(DevicePolicyManager.STATE_USER_SETUP_FINALIZED, + DpmMockContext.CALLER_USER_HANDLE); + fail("Expected SecurityException"); + } catch (SecurityException expected) { + } + } + + public void testSetUserProvisioningState_noManagement() { + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + try { + dpm.setUserProvisioningState(DevicePolicyManager.STATE_USER_SETUP_FINALIZED, + DpmMockContext.CALLER_USER_HANDLE); + fail("IllegalStateException expected"); + } catch (IllegalStateException e) { + MoreAsserts.assertContainsRegex("change provisioning state unless a .* owner is set", + e.getMessage()); + } + assertEquals(DevicePolicyManager.STATE_USER_UNMANAGED, dpm.getUserProvisioningState()); + } + + public void testSetUserProvisioningState_deviceOwnerFromSetupWizard() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + setupDeviceOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + exerciseUserProvisioningTransitions(UserHandle.USER_SYSTEM, + DevicePolicyManager.STATE_USER_SETUP_COMPLETE, + DevicePolicyManager.STATE_USER_SETUP_FINALIZED); + } + + public void testSetUserProvisioningState_deviceOwnerFromSetupWizardAlternative() + throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + setupDeviceOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + exerciseUserProvisioningTransitions(UserHandle.USER_SYSTEM, + DevicePolicyManager.STATE_USER_SETUP_INCOMPLETE, + DevicePolicyManager.STATE_USER_SETUP_FINALIZED); + } + + public void testSetUserProvisioningState_deviceOwnerWithoutSetupWizard() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + setupDeviceOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + exerciseUserProvisioningTransitions(UserHandle.USER_SYSTEM, + DevicePolicyManager.STATE_USER_SETUP_FINALIZED); + } + + public void testSetUserProvisioningState_managedProfileFromSetupWizard_primaryUser() + throws Exception { + setupProfileOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + exerciseUserProvisioningTransitions(DpmMockContext.CALLER_USER_HANDLE, + DevicePolicyManager.STATE_USER_PROFILE_COMPLETE, + DevicePolicyManager.STATE_USER_UNMANAGED); + } + + public void testSetUserProvisioningState_managedProfileFromSetupWizard_managedProfile() + throws Exception { + setupProfileOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + exerciseUserProvisioningTransitions(DpmMockContext.CALLER_USER_HANDLE, + DevicePolicyManager.STATE_USER_SETUP_COMPLETE, + DevicePolicyManager.STATE_USER_SETUP_FINALIZED); + } + + public void testSetUserProvisioningState_managedProfileWithoutSetupWizard() throws Exception { + setupProfileOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + exerciseUserProvisioningTransitions(DpmMockContext.CALLER_USER_HANDLE, + DevicePolicyManager.STATE_USER_SETUP_FINALIZED); + } + + public void testSetUserProvisioningState_illegalTransitionOutOfFinalized1() throws Exception { + setupProfileOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + try { + exerciseUserProvisioningTransitions(DpmMockContext.CALLER_USER_HANDLE, + DevicePolicyManager.STATE_USER_SETUP_FINALIZED, + DevicePolicyManager.STATE_USER_UNMANAGED); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + MoreAsserts.assertContainsRegex("Cannot move to user provisioning state", + e.getMessage()); + } + } + + public void testSetUserProvisioningState_illegalTransitionToAnotherInProgressState() + throws Exception { + setupProfileOwner(); + mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); + + try { + exerciseUserProvisioningTransitions(DpmMockContext.CALLER_USER_HANDLE, + DevicePolicyManager.STATE_USER_SETUP_INCOMPLETE, + DevicePolicyManager.STATE_USER_SETUP_COMPLETE); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + MoreAsserts.assertContainsRegex("Cannot move to user provisioning state", + e.getMessage()); + } + } + + private void exerciseUserProvisioningTransitions(int userId, int... states) { + assertEquals(DevicePolicyManager.STATE_USER_UNMANAGED, dpm.getUserProvisioningState()); + for (int state : states) { + dpm.setUserProvisioningState(state, userId); + assertEquals(state, dpm.getUserProvisioningState()); + } + } + + private void setupProfileOwner() throws Exception { + mContext.callerPermissions.addAll(OWNER_SETUP_PERMISSIONS); + + setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_UID); + dpm.setActiveAdmin(admin1, false); + assertTrue(dpm.setProfileOwner(admin1, null, DpmMockContext.CALLER_USER_HANDLE)); + + mContext.callerPermissions.removeAll(OWNER_SETUP_PERMISSIONS); + } + + private void setupDeviceOwner() throws Exception { + mContext.callerPermissions.addAll(OWNER_SETUP_PERMISSIONS); + + setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_SYSTEM_USER_UID); + dpm.setActiveAdmin(admin1, false); + assertTrue(dpm.setDeviceOwner(admin1, null, UserHandle.USER_SYSTEM)); + + mContext.callerPermissions.removeAll(OWNER_SETUP_PERMISSIONS); + } }