Wipe device or profile if max failed attempt reached
If the device or profile owner have set a max password failed
attempts policy, the device or profile should be wiped even if
DISALLOW_FACTORY_RESET / DISALLOW_REMOVE_USER /
DISALLOW_REMOVE_MANAGED_PROFILE was set by that admin. However
it should still fail if another device admin set the policy - this
is in line with what wipeData() does at the moment.
Bug: 34450538
Test: runtest -c com.android.server.devicepolicy.DevicePolicyManagerTest frameworks-services
Test: cts-tradefed run cts --module DevicePolicyManager --test com.android.cts.devicepolicy.DeviceOwnerPlusManagedProfileTest#testWipeData
Test: cts-tradefed run cts --module DevicePolicyManager --test com.android.cts.devicepolicy.ManagedProfileTest#testWipeData
Test: cts-tradefed run cts --module DevicePolicyManager --test com.android.cts.devicepolicy.DeviceOwnerTest#testDisallowFactoryReset
Change-Id: Ifac240692ce74432f7b57f3dfbbbac2a7282297b
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 623a0a5..3808f3e 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1609,6 +1609,11 @@
mContext.getSystemService(PowerManager.class).reboot(reason);
}
+ void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force)
+ throws IOException {
+ RecoverySystem.rebootWipeUserData(mContext, shutdown, reason, force);
+ }
+
boolean systemPropertiesGetBoolean(String key, boolean def) {
return SystemProperties.getBoolean(key, def);
}
@@ -4869,7 +4874,7 @@
}
}
- private void wipeDataNoLock(boolean wipeExtRequested, String reason, boolean force) {
+ private void forceWipeDeviceNoLock(boolean wipeExtRequested, String reason) {
wtfIfInLock();
if (wipeExtRequested) {
@@ -4878,94 +4883,87 @@
sm.wipeAdoptableDisks();
}
try {
- RecoverySystem.rebootWipeUserData(mContext, false /* shutdown */, reason, force);
+ mInjector.recoverySystemRebootWipeUserData(
+ /*shutdown=*/ false, reason, /*force=*/ true);
} catch (IOException | SecurityException e) {
Slog.w(LOG_TAG, "Failed requesting data wipe", e);
}
}
+ private void forceWipeUser(int userId) {
+ try {
+ IActivityManager am = mInjector.getIActivityManager();
+ if (am.getCurrentUser().id == userId) {
+ am.switchUser(UserHandle.USER_SYSTEM);
+ }
+
+ boolean userRemoved = mUserManagerInternal.removeUserEvenWhenDisallowed(userId);
+ if (!userRemoved) {
+ Slog.w(LOG_TAG, "Couldn't remove user " + userId);
+ } else if (isManagedProfile(userId)) {
+ sendWipeProfileNotification();
+ }
+ } catch (RemoteException re) {
+ // Shouldn't happen
+ }
+ }
+
@Override
public void wipeData(int flags) {
if (!mHasFeature) {
return;
}
- final int userHandle = mInjector.userHandleGetCallingUserId();
- enforceFullCrossUsersPermission(userHandle);
+ enforceFullCrossUsersPermission(mInjector.userHandleGetCallingUserId());
- final String source;
+ final ActiveAdmin admin;
synchronized (this) {
- // This API can only be called by an active device admin,
- // so try to retrieve it to check that the caller is one.
- final ActiveAdmin admin = getActiveAdminForCallerLocked(null,
- DeviceAdminInfo.USES_POLICY_WIPE_DATA);
- source = admin.info.getComponent().flattenToShortString();
-
- long ident = mInjector.binderClearCallingIdentity();
- try {
- final String restriction;
- if (userHandle == UserHandle.USER_SYSTEM) {
- restriction = UserManager.DISALLOW_FACTORY_RESET;
- } else if (isManagedProfile(userHandle)) {
- restriction = UserManager.DISALLOW_REMOVE_MANAGED_PROFILE;
- } else {
- restriction = UserManager.DISALLOW_REMOVE_USER;
- }
- if (isAdminAffectedByRestriction(
- admin.info.getComponent(), restriction, userHandle)) {
- throw new SecurityException("Cannot wipe data. " + restriction
- + " restriction is set for user " + userHandle);
- }
-
- if ((flags & WIPE_RESET_PROTECTION_DATA) != 0) {
- if (!isDeviceOwner(admin.info.getComponent(), userHandle)) {
- 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) {
- manager.wipe();
- }
- }
-
- } finally {
- mInjector.binderRestoreCallingIdentity(ident);
- }
+ admin = getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_WIPE_DATA);
}
- final boolean wipeExtRequested = (flags & WIPE_EXTERNAL_STORAGE) != 0;
- wipeDeviceNoLock(wipeExtRequested, userHandle,
- "DevicePolicyManager.wipeData() from " + source, /*force=*/ true);
+ String reason = "DevicePolicyManager.wipeData() from "
+ + admin.info.getComponent().flattenToShortString();
+ wipeDataNoLock(
+ admin.info.getComponent(), flags, reason, admin.getUserHandle().getIdentifier());
}
- private void wipeDeviceNoLock(
- boolean wipeExtRequested, final int userHandle, String reason, boolean force) {
+ private void wipeDataNoLock(ComponentName admin, int flags, String reason, int userId) {
wtfIfInLock();
long ident = mInjector.binderClearCallingIdentity();
try {
- // TODO If split user is enabled and the device owner is set in the primary user (rather
- // than system), we should probably trigger factory reset. Current code just remove
- // that user (but still clears FRP...)
- if (userHandle == UserHandle.USER_SYSTEM) {
- wipeDataNoLock(wipeExtRequested, reason, force);
+ // First check whether the admin is allowed to wipe the device/user/profile.
+ final String restriction;
+ if (userId == UserHandle.USER_SYSTEM) {
+ restriction = UserManager.DISALLOW_FACTORY_RESET;
+ } else if (isManagedProfile(userId)) {
+ restriction = UserManager.DISALLOW_REMOVE_MANAGED_PROFILE;
} else {
- try {
- IActivityManager am = mInjector.getIActivityManager();
- if (am.getCurrentUser().id == userHandle) {
- am.switchUser(UserHandle.USER_SYSTEM);
- }
+ restriction = UserManager.DISALLOW_REMOVE_USER;
+ }
+ if (isAdminAffectedByRestriction(admin, restriction, userId)) {
+ throw new SecurityException("Cannot wipe data. " + restriction
+ + " restriction is set for user " + userId);
+ }
- boolean userRemoved = force
- ? mUserManagerInternal.removeUserEvenWhenDisallowed(userHandle)
- : mUserManager.removeUser(userHandle);
- if (!userRemoved) {
- Slog.w(LOG_TAG, "Couldn't remove user " + userHandle);
- } else if (isManagedProfile(userHandle)) {
- sendWipeProfileNotification();
- }
- } catch (RemoteException re) {
- // Shouldn't happen
+ 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) {
+ manager.wipe();
+ }
+ }
+
+ // TODO If split user is enabled and the device owner is set in the primary user
+ // (rather than system), we should probably trigger factory reset. Current code just
+ // removes that user (but still clears FRP...)
+ if (userId == UserHandle.USER_SYSTEM) {
+ forceWipeDeviceNoLock(/*wipeExtRequested=*/ (flags & WIPE_EXTERNAL_STORAGE) != 0,
+ reason);
+ } else {
+ forceWipeUser(userId);
}
} finally {
mInjector.binderRestoreCallingIdentity(ident);
@@ -5105,25 +5103,21 @@
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.BIND_DEVICE_ADMIN, null);
+ boolean wipeData = false;
+ ActiveAdmin strictestAdmin = null;
final long ident = mInjector.binderClearCallingIdentity();
try {
- boolean wipeData = false;
- int identifier = 0;
synchronized (this) {
DevicePolicyData policy = getUserData(userHandle);
policy.mFailedPasswordAttempts++;
saveSettingsLocked(userHandle);
if (mHasFeature) {
- ActiveAdmin strictestAdmin = getAdminWithMinimumFailedPasswordsForWipeLocked(
+ strictestAdmin = getAdminWithMinimumFailedPasswordsForWipeLocked(
userHandle, /* parent */ false);
int max = strictestAdmin != null
? strictestAdmin.maximumFailedPasswordsForWipe : 0;
if (max > 0 && policy.mFailedPasswordAttempts >= max) {
- // Wipe the user/profile associated with the policy that was violated. This
- // is not necessarily calling user: if the policy that fired was from a
- // managed profile rather than the main user profile, we wipe former only.
wipeData = true;
- identifier = strictestAdmin.getUserHandle().getIdentifier();
}
sendAdminCommandForLockscreenPoliciesLocked(
@@ -5131,14 +5125,33 @@
DeviceAdminInfo.USES_POLICY_WATCH_LOGIN, userHandle);
}
}
- if (wipeData) {
- // Call without holding lock.
- wipeDeviceNoLock(false, identifier, "reportFailedPasswordAttempt()", false);
- }
} finally {
mInjector.binderRestoreCallingIdentity(ident);
}
+ if (wipeData && strictestAdmin != null) {
+ final int userId = strictestAdmin.getUserHandle().getIdentifier();
+ Slog.i(LOG_TAG, "Max failed password attempts policy reached for admin: "
+ + strictestAdmin.info.getComponent().flattenToShortString()
+ + ". Calling wipeData for user " + userId);
+
+ // Attempt to wipe the device/user/profile associated with the admin, as if the
+ // admin had called wipeData(). That way we can check whether the admin is actually
+ // allowed to wipe the device (e.g. a regular device admin shouldn't be able to wipe the
+ // device if the device owner has set DISALLOW_FACTORY_RESET, but the DO should be
+ // able to do so).
+ // IMPORTANT: Call without holding the lock to prevent deadlock.
+ try {
+ wipeDataNoLock(strictestAdmin.info.getComponent(),
+ /*flags=*/ 0,
+ /*reason=*/ "reportFailedPasswordAttempt()",
+ userId);
+ } catch (SecurityException e) {
+ Slog.w(LOG_TAG, "Failed to wipe user " + userId
+ + " after max failed password attempts reached.", e);
+ }
+ }
+
if (mInjector.securityLogIsLoggingEnabled()) {
SecurityLog.writeEvent(SecurityLog.TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT, /*result*/ 0,
/*method strength*/ 1);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 4927f0c..3b92a34 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -37,6 +37,7 @@
import com.android.internal.widget.LockPatternUtils;
import java.io.File;
+import java.io.IOException;
import java.util.Map;
/**
@@ -264,6 +265,12 @@
}
@Override
+ void recoverySystemRebootWipeUserData(boolean shutdown, String reason, boolean force)
+ throws IOException {
+ context.recoverySystem.rebootWipeUserData(shutdown, reason, force);
+ }
+
+ @Override
boolean systemPropertiesGetBoolean(String key, boolean def) {
return context.systemProperties.getBoolean(key, def);
}
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 8da47c8..f86a6b6 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -82,6 +82,7 @@
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
/**
@@ -3233,6 +3234,140 @@
}
}
+ public void testWipeDataDeviceOwner() throws Exception {
+ setDeviceOwner();
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_FACTORY_RESET,
+ UserHandle.SYSTEM))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_DEVICE_OWNER);
+
+ dpm.wipeData(0);
+ verify(mContext.recoverySystem).rebootWipeUserData(
+ /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true));
+ }
+
+ public void testWipeDataDeviceOwnerDisallowed() throws Exception {
+ setDeviceOwner();
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_FACTORY_RESET,
+ UserHandle.SYSTEM))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_SYSTEM);
+ try {
+ // The DO is not allowed to wipe the device if the user restriction was set
+ // by the system
+ dpm.wipeData(0);
+ fail("SecurityException not thrown");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ public void testMaximumFailedPasswordAttemptsReachedManagedProfile() throws Exception {
+ final int MANAGED_PROFILE_USER_ID = 15;
+ final int MANAGED_PROFILE_ADMIN_UID = UserHandle.getUid(MANAGED_PROFILE_USER_ID, 19436);
+ addManagedProfile(admin1, MANAGED_PROFILE_ADMIN_UID, admin1);
+
+ // Even if the caller is the managed profile, the current user is the user 0
+ when(mContext.iactivityManager.getCurrentUser())
+ .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0));
+
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_REMOVE_MANAGED_PROFILE,
+ UserHandle.of(MANAGED_PROFILE_USER_ID)))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_PROFILE_OWNER);
+
+ mContext.binder.callingUid = MANAGED_PROFILE_ADMIN_UID;
+ dpm.setMaximumFailedPasswordsForWipe(admin1, 3);
+
+ mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+ mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+ // Failed password attempts on the parent user are taken into account, as there isn't a
+ // separate work challenge.
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+
+ // The profile should be wiped even if DISALLOW_REMOVE_MANAGED_PROFILE is enabled, because
+ // both the user restriction and the policy were set by the PO.
+ verify(mContext.userManagerInternal).removeUserEvenWhenDisallowed(
+ MANAGED_PROFILE_USER_ID);
+ verifyZeroInteractions(mContext.recoverySystem);
+ }
+
+ public void testMaximumFailedPasswordAttemptsReachedManagedProfileDisallowed()
+ throws Exception {
+ final int MANAGED_PROFILE_USER_ID = 15;
+ final int MANAGED_PROFILE_ADMIN_UID = UserHandle.getUid(MANAGED_PROFILE_USER_ID, 19436);
+ addManagedProfile(admin1, MANAGED_PROFILE_ADMIN_UID, admin1);
+
+ // Even if the caller is the managed profile, the current user is the user 0
+ when(mContext.iactivityManager.getCurrentUser())
+ .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0));
+
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_REMOVE_MANAGED_PROFILE,
+ UserHandle.of(MANAGED_PROFILE_USER_ID)))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_SYSTEM);
+
+ mContext.binder.callingUid = MANAGED_PROFILE_ADMIN_UID;
+ dpm.setMaximumFailedPasswordsForWipe(admin1, 3);
+
+ mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+ mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+ // Failed password attempts on the parent user are taken into account, as there isn't a
+ // separate work challenge.
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+
+ // DISALLOW_REMOVE_MANAGED_PROFILE was set by the system, not the PO, so the profile is
+ // not wiped.
+ verify(mContext.userManagerInternal, never())
+ .removeUserEvenWhenDisallowed(anyInt());
+ verifyZeroInteractions(mContext.recoverySystem);
+ }
+
+ public void testMaximumFailedPasswordAttemptsReachedDeviceOwner() throws Exception {
+ setDeviceOwner();
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_FACTORY_RESET,
+ UserHandle.SYSTEM))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_DEVICE_OWNER);
+
+ dpm.setMaximumFailedPasswordsForWipe(admin1, 3);
+
+ mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+ mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+
+ // The device should be wiped even if DISALLOW_FACTORY_RESET is enabled, because both the
+ // user restriction and the policy were set by the DO.
+ verify(mContext.recoverySystem).rebootWipeUserData(
+ /*shutdown=*/ eq(false), anyString(), /*force=*/ eq(true));
+ }
+
+ public void testMaximumFailedPasswordAttemptsReachedDeviceOwnerDisallowed() throws Exception {
+ setDeviceOwner();
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_FACTORY_RESET,
+ UserHandle.SYSTEM))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_SYSTEM);
+
+ dpm.setMaximumFailedPasswordsForWipe(admin1, 3);
+
+ mContext.binder.callingUid = DpmMockContext.SYSTEM_UID;
+ mContext.callerPermissions.add(permission.BIND_DEVICE_ADMIN);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+ dpm.reportFailedPasswordAttempt(UserHandle.USER_SYSTEM);
+
+ // DISALLOW_FACTORY_RESET was set by the system, not the DO, so the device is not wiped.
+ verifyZeroInteractions(mContext.recoverySystem);
+ verify(mContext.userManagerInternal, never())
+ .removeUserEvenWhenDisallowed(anyInt());
+ }
+
public void testGetPermissionGrantState() throws Exception {
final String permission = "some.permission";
final String app1 = "com.example.app1";
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index 44bf547..2a82a52 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -55,6 +55,7 @@
import org.mockito.stubbing.Answer;
import java.io.File;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -154,6 +155,12 @@
}
}
+ public static class RecoverySystemForMock {
+ public void rebootWipeUserData(
+ boolean shutdown, String reason, boolean force) throws IOException {
+ }
+ }
+
public static class SystemPropertiesForMock {
public boolean getBoolean(String key, boolean def) {
return false;
@@ -263,6 +270,7 @@
public final UserManagerForMock userManagerForMock;
public final PowerManagerForMock powerManager;
public final PowerManagerInternal powerManagerInternal;
+ public final RecoverySystemForMock recoverySystem;
public final NotificationManager notificationManager;
public final IIpConnectivityMetrics iipConnectivityMetrics;
public final IWindowManager iwindowManager;
@@ -308,6 +316,7 @@
packageManagerInternal = mock(PackageManagerInternal.class);
powerManager = mock(PowerManagerForMock.class);
powerManagerInternal = mock(PowerManagerInternal.class);
+ recoverySystem = mock(RecoverySystemForMock.class);
notificationManager = mock(NotificationManager.class);
iipConnectivityMetrics = mock(IIpConnectivityMetrics.class);
iwindowManager = mock(IWindowManager.class);