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

Commit 815e3f9e authored by Tetiana Meronyk's avatar Tetiana Meronyk
Browse files

Require credential authentication when deleting a user

Since deleting a user is an irreversible and high-impact action, Lock Screen in added after confirmation dialog when a user tries to remove themselves or an admin removes another user.

Bug: 342395399
Test: atest UserSettingsTest && atest UserDetailsSettingsTest
Flag: android.multiuser.require_pin_before_user_deletion
Change-Id: Ibcebe3f5391a423d451a387fe708d07d96604d4f
parent c0ed9adc
Loading
Loading
Loading
Loading
+42 −0
Original line number Diff line number Diff line
@@ -18,11 +18,13 @@ package com.android.settings.users;

import static android.os.UserHandle.USER_NULL;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.pm.UserInfo;
import android.multiuser.Flags;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.Trace;
@@ -30,6 +32,9 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
@@ -38,6 +43,7 @@ import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedPreference;
@@ -80,9 +86,12 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
    /** Whether to enable the app_copying fragment. */
    private static final boolean SHOW_APP_COPYING_PREF = false;
    private static final int MESSAGE_PADDING = 20;
    @VisibleForTesting
    static final int REQUEST_CONFIRM_REMOVE = 1;

    private UserManager mUserManager;
    private UserCapabilities mUserCaps;
    private ActivityResultLauncher mUserRemovalCredentialConfirmationActivityResultLauncher;
    private boolean mGuestUserAutoCreated;
    private final AtomicBoolean mGuestCreationScheduled = new AtomicBoolean();
    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
@@ -121,6 +130,11 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
                com.android.internal.R.bool.config_guestUserAutoCreated);

        initialize(context, getArguments());
        if (Flags.requirePinBeforeUserDeletion()) {
            mUserRemovalCredentialConfirmationActivityResultLauncher = registerForActivityResult(
                    new ActivityResultContracts.StartActivityForResult(),
                    result -> onRemoveUserConfirmationActivityLauncherResult(result));
        }
    }

    @Override
@@ -516,10 +530,38 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
    }

    private void removeUser() {
        if (Flags.requirePinBeforeUserDeletion() && runUserRemovalKeyguardConfirmation()) {
            // User deletion will be handled when the credential authentication result is successful
            return;
        }
        mUserManager.removeUser(mUserInfo.id);
        finishFragment();
    }

    /**
     * Shows keyguard validation activity if screen lock is set for the current user. If screen lock
     * is not set up, no activity is launched.
     *
     * @return true if the authentication activity has been launched.
     */
    @VisibleForTesting
    boolean runUserRemovalKeyguardConfirmation() {
        final ChooseLockSettingsHelper.Builder builder =
                new ChooseLockSettingsHelper.Builder(getActivity(), this);
        return builder
                .setActivityResultLauncher(mUserRemovalCredentialConfirmationActivityResultLauncher)
                .setRequestCode(REQUEST_CONFIRM_REMOVE)
                .setUserId(UserHandle.myUserId())
                .show();
    }

    private void onRemoveUserConfirmationActivityLauncherResult(ActivityResult result) {
        if (result.getResultCode() == Activity.RESULT_OK) {
            mUserManager.removeUser(mUserInfo.id);
            finishFragment();
        }
    }

    /**
     * @param isNewUser indicates if a user was created recently, for new users
     *                  AppRestrictionsFragment should set the default restrictions
+43 −3
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.password.ChooseLockGeneric;
import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.widget.MainSwitchBarController;
import com.android.settings.widget.SettingsMainSwitchBar;
@@ -151,7 +152,8 @@ public class UserSettings extends SettingsPreferenceFragment

    private static final IntentFilter USER_REMOVED_INTENT_FILTER;

    private static final int DIALOG_CONFIRM_REMOVE = 1;
    @VisibleForTesting
    static final int DIALOG_CONFIRM_REMOVE = 1;
    private static final int DIALOG_ADD_USER = 2;
    // Dialogs with id 3 and 4 got removed
    private static final int DIALOG_USER_CANNOT_MANAGE = 5;
@@ -177,6 +179,8 @@ public class UserSettings extends SettingsPreferenceFragment
    private static final int REQUEST_CHOOSE_LOCK = 10;
    private static final int REQUEST_EDIT_GUEST = 11;
    private static final int REQUEST_ADD_USER = 12;
    @VisibleForTesting
    static final int REQUEST_DELETE_USER = 13;

    static final int RESULT_GUEST_REMOVED = 100;

@@ -213,6 +217,7 @@ public class UserSettings extends SettingsPreferenceFragment
    SparseArray<Bitmap> mUserIcons = new SparseArray<>();
    private int mRemovingUserId = -1;
    private boolean mAddingUser;
    private boolean mUserRemovalCredentialConfirmationPending;
    private boolean mGuestUserAutoCreated;
    private String mConfigSupervisedUserCreationPackage;
    private String mAddingUserName;
@@ -589,6 +594,13 @@ public class UserSettings extends SettingsPreferenceFragment
        } else if (mGuestUserAutoCreated && requestCode == REQUEST_EDIT_GUEST
                && resultCode == RESULT_GUEST_REMOVED) {
            scheduleGuestCreation();
        } else if (Flags.requirePinBeforeUserDeletion() && requestCode == REQUEST_DELETE_USER) {
            if (resultCode == Activity.RESULT_OK) {
                removeUserNow();
            } else {
                mRemovingUserId = -1;
                mUserRemovalCredentialConfirmationPending = false;
            }
        } else {
            mCreateUserDialogController.onActivityResult(requestCode, resultCode, data);
            mEditUserInfoController.onActivityResult(requestCode, resultCode, data);
@@ -721,6 +733,11 @@ public class UserSettings extends SettingsPreferenceFragment
                        UserDialogs.createRemoveDialog(getActivity(), mRemovingUserId,
                                new DialogInterface.OnClickListener() {
                                    public void onClick(DialogInterface dialog, int which) {
                                        if (Flags.requirePinBeforeUserDeletion()
                                                && runUserRemovalKeyguardConfirmation()) {
                                            mUserRemovalCredentialConfirmationPending = true;
                                            return;
                                        }
                                        removeUserNow();
                                    }
                                }
@@ -889,6 +906,21 @@ public class UserSettings extends SettingsPreferenceFragment
        }
    }

    /**
     * Shows keyguard validation activity if screen lock is set for a user being deleted. If screen
     * lock is not set up, no activity is launched.
     *
     * @return true if the authentication activity has been launched.
     */
    @VisibleForTesting
    boolean runUserRemovalKeyguardConfirmation() {
        final ChooseLockSettingsHelper.Builder builder =
                new ChooseLockSettingsHelper.Builder(getActivity(), this);
        return builder
                .setRequestCode(REQUEST_DELETE_USER)
                .show();
    }

    private Dialog buildEditCurrentUserDialog() {
        final Activity activity = getActivity();
        if (activity == null) {
@@ -1000,7 +1032,13 @@ public class UserSettings extends SettingsPreferenceFragment
    private void removeUserNow() {
        if (mRemovingUserId == UserHandle.myUserId()) {
            removeThisUser();
        } else {
            synchronized (mUserLock) {
                mRemovingUserId = -1;
                mUserRemovalCredentialConfirmationPending = false;
            }
        } else if (!Flags.requirePinBeforeUserDeletion()) {
            // This method is only called when a user deletes themselves so this part of code is
            // never executed and can be removed.
            ThreadUtils.postOnBackgroundThread(new Runnable() {
                @Override
                public void run() {
@@ -1745,7 +1783,9 @@ public class UserSettings extends SettingsPreferenceFragment
    @Override
    public void onDismiss(DialogInterface dialog) {
        synchronized (mUserLock) {
            if (!mUserRemovalCredentialConfirmationPending) {
                mRemovingUserId = -1;
            }
            updateUserList();
            if (mCreateUserDialogController.isActive()) {
                mCreateUserDialogController.finish();
+41 −1
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@ import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
import static android.os.UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
import static android.os.UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;

import static com.android.settings.users.UserDetailsSettings.REQUEST_CONFIRM_REMOVE;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assume.assumeTrue;
@@ -61,6 +63,7 @@ import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.SubSettings;
import com.android.settings.testutils.shadow.ShadowDevicePolicyManager;
import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedPreference;
@@ -88,7 +91,8 @@ import java.util.List;
@Config(shadows = {
        ShadowUserManager.class,
        com.android.settings.testutils.shadow.ShadowFragment.class,
        ShadowDevicePolicyManager.class
        ShadowDevicePolicyManager.class,
        ShadowLockPatternUtils.class
})
public class UserDetailsSettingsTest {

@@ -627,6 +631,42 @@ public class UserDetailsSettingsTest {
        verify(mFragment).showDialog(DIALOG_CONFIRM_REMOVE);
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_REQUIRE_PIN_BEFORE_USER_DELETION)
    public void runKeyguardConfirmation_userHasScreenLock_shouldLaunchAuthenticationActivity() {
        setupSelectedUser();
        mFragment.mUserInfo = mUserInfo;
        mUserManager.setIsAdminUser(true);

        mUserManager.addProfile(new UserInfo(UserHandle.myUserId(), "Bob", null,
                UserInfo.FLAG_FULL | UserInfo.FLAG_MAIN));

        ShadowLockPatternUtils.setKeyguardStoredPasswordQuality(
                DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
        doNothing().when(mFragment).startActivityForResult(any(), anyInt(), any());
        ShadowUserManager.getShadow().setProfileIdsWithDisabled(new int[]{UserHandle.myUserId()});

        assertThat(mFragment.runUserRemovalKeyguardConfirmation()).isTrue();
        verify(mFragment).startActivityForResult(any(Intent.class), eq(REQUEST_CONFIRM_REMOVE),
                any());
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_REQUIRE_PIN_BEFORE_USER_DELETION)
    public void runKeyguardConfirmation_userHasNoScreenLock_shouldNotLaunchAuthentication() {
        setupSelectedUser();
        mFragment.mUserInfo = mUserInfo;
        mUserManager.setIsAdminUser(true);

        mUserManager.addProfile(new UserInfo(UserHandle.myUserId(), "Bob", null,
                UserInfo.FLAG_FULL | UserInfo.FLAG_MAIN));

        ShadowUserManager.getShadow().setProfileIdsWithDisabled(new int[]{UserHandle.myUserId()});
        assertThat(mFragment.runUserRemovalKeyguardConfirmation()).isFalse();
        verify(mFragment, never()).startActivityForResult(any(Intent.class),
                eq(REQUEST_CONFIRM_REMOVE), any());
    }

    @Test
    public void onPreferenceClick_removeClicked_canNotDelete_doNothing() {
        setupSelectedUser();
+73 −0
Original line number Diff line number Diff line
@@ -20,6 +20,9 @@ import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
import static android.os.UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
import static android.os.UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;

import static com.android.settings.users.UserSettings.DIALOG_CONFIRM_REMOVE;
import static com.android.settings.users.UserSettings.REQUEST_DELETE_USER;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
@@ -37,6 +40,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import android.app.Dialog;
import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
@@ -62,6 +66,7 @@ import android.text.SpannableStringBuilder;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Button;

import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
@@ -73,6 +78,7 @@ import com.android.settings.SettingsActivity;
import com.android.settings.SubSettings;
import com.android.settings.testutils.shadow.SettingsShadowResources;
import com.android.settings.testutils.shadow.ShadowDevicePolicyManager;
import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedPreference;
@@ -106,6 +112,7 @@ import java.util.List;
        ShadowUserManager.class,
        ShadowDevicePolicyManager.class,
        SettingsShadowResources.class,
        ShadowLockPatternUtils.class,
        com.android.settings.testutils.shadow.ShadowFragment.class,
})
public class UserSettingsTest {
@@ -419,6 +426,72 @@ public class UserSettingsTest {
        verify(menuItem, never()).setTitle(AdditionalMatchers.not(eq(defaultTitle)));
    }


    @Test
    @RequiresFlagsEnabled(Flags.FLAG_REQUIRE_PIN_BEFORE_USER_DELETION)
    public void removeUserSelf_userHasScreenlock_shouldAskForCredentials() {
        doReturn(SWITCHABILITY_STATUS_OK).when(mUserManager).getUserSwitchability();

        ShadowLockPatternUtils.setKeyguardStoredPasswordQuality(
                DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);

        doReturn(mUserManager).when(mActivity).getSystemService(Context.USER_SERVICE);
        doNothing().when(mFragment).startActivityForResult(any(), anyInt(), any());

        UserInfo user =  new UserInfo(UserHandle.myUserId(),
                SECONDARY_USER_NAME, null,
                UserInfo.FLAG_FULL | UserInfo.FLAG_INITIALIZED,
                UserManager.USER_TYPE_FULL_SECONDARY);
        doReturn(user).when(mUserManager).getUserInfo(anyInt());
        doReturn(UserHandle.myUserId()).when(mUserManager).getCredentialOwnerProfile(anyInt());

        doReturn(new int[]{UserHandle.myUserId()}).when(mUserManager)
                .getProfileIdsWithDisabled(UserHandle.myUserId());

        Dialog confirmDialog = mFragment.onCreateDialog(DIALOG_CONFIRM_REMOVE);
        confirmDialog.show();

        Button positiveButton = confirmDialog.findViewById(android.R.id.button1);
        assertThat(positiveButton).isNotNull();

        positiveButton.performClick();

        assertThat(mFragment.runUserRemovalKeyguardConfirmation()).isTrue();
        verify(mFragment).startActivityForResult(any(Intent.class), eq(REQUEST_DELETE_USER), any());
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_REQUIRE_PIN_BEFORE_USER_DELETION)
    public void removeUserSelf_userHasNoScreenlock_shouldNotAskForCredentials() {
        doReturn(SWITCHABILITY_STATUS_OK).when(mUserManager).getUserSwitchability();

        doReturn(mUserManager).when(mActivity).getSystemService(Context.USER_SERVICE);
        doNothing().when(mFragment).startActivityForResult(any(), anyInt(), any());

        UserInfo user =  new UserInfo(UserHandle.myUserId(),
                SECONDARY_USER_NAME, null,
                UserInfo.FLAG_FULL | UserInfo.FLAG_INITIALIZED,
                UserManager.USER_TYPE_FULL_SECONDARY);
        doReturn(user).when(mUserManager).getUserInfo(anyInt());
        doReturn(UserHandle.myUserId()).when(mUserManager).getCredentialOwnerProfile(anyInt());

        doReturn(new int[]{UserHandle.myUserId()}).when(mUserManager)
                .getProfileIdsWithDisabled(UserHandle.myUserId());

        Dialog confirmDialog = mFragment.onCreateDialog(DIALOG_CONFIRM_REMOVE);
        confirmDialog.show();

        Button positiveButton = confirmDialog.findViewById(android.R.id.button1);
        assertThat(positiveButton).isNotNull();

        positiveButton.performClick();

        assertThat(mFragment.runUserRemovalKeyguardConfirmation()).isFalse();
        verify(mFragment, never()).startActivityForResult(any(Intent.class),
                eq(REQUEST_DELETE_USER), any());
    }


    @Test
    public void updateUserList_canAddUserAndSwitchUser_shouldShowAddUser() {
        mUserCapabilities.mCanAddUser = true;