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

Commit aba2df76 authored by Momoko Hattori's avatar Momoko Hattori
Browse files

Reject last full admin user in revokeUserAdmin() under a flag

Commit 35366ec0 added a config flag
(config_disallowRemovingLastAdminUser) to disallow the removal of last
full admin user on the system. When it is enabled, revokeUserAdmin()
should also avoid revoking Admin status from the last full admin user.

Bug: 411194997
Test: atest FrameworksMockingServicesTests:UserManagerServiceMockedTest
Test: Manually test with CL:33082444 as follows:
  $ adb shell cmd user list -v
  2 users:

  0: id=0, type=system.HEADLESS, flags=ADMIN|INITIALIZED|PRIMARY|SYSTEM
  1: id=10, type=full.SECONDARY, flags=ADMIN|FULL|INITIALIZED|MAIN
  $ adb shell cmd user revoke-user-admin 10
  $ adb shell cmd user list -v | grep id=10
  1: id=10, type=full.SECONDARY, flags=ADMIN|FULL|INITIALIZED|MAIN
Flag: android.multiuser.disallow_removing_last_admin_user

Change-Id: I0c7f99dcece6c01967e07c21f06c00b05e00433c
parent f59abc3d
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ public class UserJourneyLogger {
    public static final int ERROR_CODE_USER_ALREADY_AN_ADMIN = 5;
    public static final int ERROR_CODE_USER_IS_NOT_AN_ADMIN = 6;
    public static final int ERROR_CODE_INVALID_USER_TYPE = 7;
    public static final int ERROR_CODE_USER_IS_LAST_ADMIN = 8;

    @IntDef(prefix = {"ERROR_CODE"}, value = {
            ERROR_CODE_UNSPECIFIED,
@@ -78,7 +79,8 @@ public class UserJourneyLogger {
            ERROR_CODE_USER_ALREADY_AN_ADMIN,
            ERROR_CODE_USER_IS_NOT_AN_ADMIN,
            ERROR_CODE_INVALID_SESSION_ID,
            ERROR_CODE_INVALID_USER_TYPE
            ERROR_CODE_INVALID_USER_TYPE,
            ERROR_CODE_USER_IS_LAST_ADMIN,
    })
    public @interface UserJourneyErrorCode {
    }
+30 −15
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_ABORTED;
import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_INVALID_USER_TYPE;
import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_UNSPECIFIED;
import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_ALREADY_AN_ADMIN;
import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_IS_LAST_ADMIN;
import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_IS_NOT_AN_ADMIN;
import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_DEMOTE_MAIN_USER;
import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_PROMOTE_MAIN_USER;
@@ -2337,14 +2338,14 @@ public class UserManagerService extends IUserManager.Stub {
                            currentUserId, userId, /* userType */ "", /* userFlags */ -1);
                    return;
                } else if (user.info.isAdmin()) {
                    // Exit if the user is already an Admin.
                    // Exit if the user is already an admin.
                    mUserJourneyLogger.logUserJourneyFinishWithError(currentUserId,
                        user.info, USER_JOURNEY_GRANT_ADMIN,
                        ERROR_CODE_USER_ALREADY_AN_ADMIN);
                    return;
                } else if (user.info.isProfile() || user.info.isGuest()
                        || user.info.isRestricted()) {
                    // Profiles, guest users or restricted profiles cannot become an Admin.
                    // Profiles, guest users or restricted profiles cannot become an admin.
                    mUserJourneyLogger.logUserJourneyFinishWithError(currentUserId,
                            user.info, USER_JOURNEY_GRANT_ADMIN, ERROR_CODE_INVALID_USER_TYPE);
                    return;
@@ -2376,15 +2377,21 @@ public class UserManagerService extends IUserManager.Stub {
                            USER_JOURNEY_REVOKE_ADMIN, currentUserId, userId, "", -1);
                    return;
                } else if (!user.info.isAdmin()) {
                    // Exit if user is not an Admin.
                    // Exit if user is not an admin.
                    mUserJourneyLogger.logUserJourneyFinishWithError(currentUserId, user.info,
                            USER_JOURNEY_REVOKE_ADMIN, ERROR_CODE_USER_IS_NOT_AN_ADMIN);
                    return;
                } else if ((user.info.flags & UserInfo.FLAG_SYSTEM) != 0) {
                    // System user must always be an Admin.
                    // System user cannot lose its admin status.
                    mUserJourneyLogger.logUserJourneyFinishWithError(currentUserId, user.info,
                            USER_JOURNEY_REVOKE_ADMIN, ERROR_CODE_INVALID_USER_TYPE);
                    return;
                } else if (isNonRemovableLastAdminUserLU(user.info)) {
                    // This is the last admin user and this device requires that it not lose its
                    // admin status.
                    mUserJourneyLogger.logUserJourneyFinishWithError(currentUserId, user.info,
                            USER_JOURNEY_REVOKE_ADMIN, ERROR_CODE_USER_IS_LAST_ADMIN);
                    return;
                }
                user.info.flags ^= UserInfo.FLAG_ADMIN;
                writeUserLP(user);
@@ -7043,18 +7050,11 @@ public class UserManagerService extends IUserManager.Stub {
                    msg);
            return UserManager.REMOVE_RESULT_ALREADY_BEING_REMOVED;
        }
        if (android.multiuser.Flags.disallowRemovingLastAdminUser()) {
            if (getContextResources().getBoolean(R.bool.config_disallowRemovingLastAdminUser)) {
                // For HSUM, the headless system user is currently flagged as an admin user now.
                // Thus we must exclude it when checking for the last admin user, and only consider
                // full admin users. b/419105275 will investigate not making HSU an admin.
                if (isLastFullAdminUserLU(userData.info)) {
                    Slogf.e(LOG_TAG, "User %d can not be %s, last admin user cannot be removed.",
                            userId, msg);
        if (isNonRemovableLastAdminUserLU(userData.info)) {
            Slogf.e(LOG_TAG, "User %d can not be %s, last admin user cannot be removed.", userId,
                    msg);
            return UserManager.REMOVE_RESULT_ERROR_LAST_ADMIN_USER;
        }
            }
        }
        return UserManager.REMOVE_RESULT_USER_IS_REMOVABLE;
    }

@@ -8841,6 +8841,21 @@ public class UserManagerService extends IUserManager.Stub {
        return userInfo.isMain() && isMainUserPermanentAdmin();
    }

    /**
     * Returns true if we must not delete this user or revoke its admin status because it is the
     * last admin user on a device that requires there to always be at least one admin.
     */
    @GuardedBy("mUsersLock")
    private boolean isNonRemovableLastAdminUserLU(UserInfo userInfo) {
        return android.multiuser.Flags.disallowRemovingLastAdminUser()
                && getContextResources().getBoolean(R.bool.config_disallowRemovingLastAdminUser)
                // For HSUM, the headless system user is currently flagged as an admin user now.
                // Thus we must exclude it when checking for the last admin user, and only consider
                // full admin users.
                // TODO(b/419105275): Investigate not making HSU an admin.
                && isLastFullAdminUserLU(userInfo);
    }

    /** Returns if the user is the last admin user that is a full user. */
    @GuardedBy("mUsersLock")
    @VisibleForTesting
+25 −1
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import static android.content.pm.UserInfo.FLAG_ADMIN;
import static android.content.pm.UserInfo.FLAG_FULL;
import static android.multiuser.Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION;
import static android.multiuser.Flags.FLAG_DEMOTE_MAIN_USER;
import static android.multiuser.Flags.FLAG_DISALLOW_REMOVING_LAST_ADMIN_USER;
import static android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES;
import static android.multiuser.Flags.FLAG_LOGOUT_USER_API;
import static android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE;
@@ -236,10 +237,10 @@ public final class UserManagerServiceMockedTest {
        doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any());
        mockIsLowRamDevice(false);

        // Called when getting boot user. config_hsumBootStrategy is 0 by default.
        mSpyResources = spy(mSpiedContext.getResources());
        when(mSpiedContext.getResources()).thenReturn(mSpyResources);
        mockHsumBootStrategy(BOOT_TO_PREVIOUS_OR_FIRST_SWITCHABLE_USER);
        mockDisallowRemovingLastAdminUser(false);

        doReturn(mSpyResources).when(Resources::getSystem);

@@ -1822,6 +1823,19 @@ public final class UserManagerServiceMockedTest {
        assertThat(mUsers.get(UserHandle.USER_SYSTEM).info.isAdmin()).isTrue();
    }

    @Test
    @RequiresFlagsEnabled(FLAG_DISALLOW_REMOVING_LAST_ADMIN_USER)
    public void testRevokeUserAdminFailsForLastFullAdmin() {
        mockDisallowRemovingLastAdminUser(true);
        // Mark system user as headless so that it is not a full admin user.
        setSystemUserHeadless(true);
        addAdminUser(USER_ID);

        mUms.revokeUserAdmin(USER_ID);

        assertThat(mUsers.get(USER_ID).info.isAdmin()).isTrue();
    }

    /**
     * Returns true if the user's XML file has Default restrictions
     * @param userId Id of the user.
@@ -1958,6 +1972,16 @@ public final class UserManagerServiceMockedTest {
                .getInteger(com.android.internal.R.integer.config_hsumBootStrategy);
    }

    private void mockDisallowRemovingLastAdminUser(boolean disallow) {
        boolean previousValue = mSpyResources
                .getBoolean(com.android.internal.R.bool.config_disallowRemovingLastAdminUser);
        Log.d(TAG, "mockDisallowRemovingLastAdminUser(): will return " + disallow + " instead of "
                + previousValue);
        doReturn(disallow)
                .when(mSpyResources)
                .getBoolean(com.android.internal.R.bool.config_disallowRemovingLastAdminUser);
    }

    private void mockUserIsInCall(boolean isInCall) {
        when(mTelecomManager.isInCall()).thenReturn(isInCall);
    }