This is an automated email from the ASF dual-hosted git repository. dahn pushed a commit to branch 4.19 in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit a0080a04fe66b1b1cf1f67bb674a079d7cab857a Author: nvazquez <nicovazque...@gmail.com> AuthorDate: Sun May 11 22:48:41 2025 -0300 Adding privilege checks on user and account operations Co-authored-by: Harikrishna <harikrishna.patn...@gmail.com> --- api/src/main/java/com/cloud/user/Account.java | 1 + .../contrail/management/MockAccountManager.java | 4 + .../configuration/ConfigurationManagerImpl.java | 37 ++++++ .../resourcelimit/ResourceLimitManagerImpl.java | 6 + .../main/java/com/cloud/user/AccountManager.java | 1 + .../java/com/cloud/user/AccountManagerImpl.java | 128 ++++++++++++++++++++- .../ConfigurationManagerImplTest.java | 47 ++++++++ .../com/cloud/user/AccountManagerImplTest.java | 101 +++++++++++++++- .../AccountManagerImplVolumeDeleteEventTest.java | 2 + .../com/cloud/user/MockAccountManagerImpl.java | 4 + 10 files changed, 321 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/com/cloud/user/Account.java b/api/src/main/java/com/cloud/user/Account.java index bb9838f137a..2b815531116 100644 --- a/api/src/main/java/com/cloud/user/Account.java +++ b/api/src/main/java/com/cloud/user/Account.java @@ -71,6 +71,7 @@ public interface Account extends ControlledEntity, InternalIdentity, Identity { } public static final long ACCOUNT_ID_SYSTEM = 1; + public static final long ACCOUNT_ID_ADMIN = 2; public String getAccountName(); diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index a63c5b68e57..353b062b90b 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -521,4 +521,8 @@ public class MockAccountManager extends ManagerBase implements AccountManager { public UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccount user) { return null; } + + @Override + public void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount) { + } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 11d478ccf62..4c03122e463 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -47,11 +47,13 @@ import javax.naming.ConfigurationException; import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.user.AccountManagerImpl; import com.cloud.utils.crypt.DBEncryptionUtil; import com.cloud.host.HostTagVO; import com.cloud.storage.StoragePoolTagVO; import com.cloud.storage.VolumeApiServiceImpl; import com.googlecode.ipv6.IPv6Address; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupService; @@ -470,6 +472,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati private long _defaultPageSize = Long.parseLong(Config.DefaultPageSize.getDefaultValue()); private static final String DOMAIN_NAME_PATTERN = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{1,63}$"; private Set<String> configValuesForValidation = new HashSet<String>(); + private Set<String> configKeysAllowedOnlyForDefaultAdmin = new HashSet<String>(); private Set<String> weightBasedParametersForValidation = new HashSet<String>(); private Set<String> overprovisioningFactorsForValidation = new HashSet<String>(); @@ -533,6 +536,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati populateConfigValuesForValidationSet(); weightBasedParametersForValidation(); overProvisioningFactorsForValidation(); + populateConfigKeysAllowedOnlyForDefaultAdmin(); initMessageBusListener(); return true; } @@ -596,6 +600,11 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati overprovisioningFactorsForValidation.add(CapacityManager.StorageOverprovisioningFactor.key()); } + protected void populateConfigKeysAllowedOnlyForDefaultAdmin() { + configKeysAllowedOnlyForDefaultAdmin.add(AccountManagerImpl.listOfRoleTypesAllowedForOperationsOfSameRoleType.key()); + configKeysAllowedOnlyForDefaultAdmin.add(AccountManagerImpl.allowOperationsOnUsersInSameAccount.key()); + } + private void initMessageBusListener() { messageBus.subscribe(EventTypes.EVENT_CONFIGURATION_VALUE_EDIT, new MessageSubscriber() { @Override @@ -1183,6 +1192,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati s_logger.error("Missing configuration variable " + name + " in configuration table"); return "Invalid configuration variable."; } + validateConfigurationAllowedOnlyForDefaultAdmin(name, value); final String configScope = cfg.getScope(); if (scope != null) { @@ -1347,6 +1357,33 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati return String.format("Invalid value for configuration [%s].", name); } + protected void validateConfigurationAllowedOnlyForDefaultAdmin(String configName, String value) { + if (configKeysAllowedOnlyForDefaultAdmin.contains(configName)) { + final Long userId = CallContext.current().getCallingUserId(); + if (userId != User.UID_ADMIN) { + throw new CloudRuntimeException("Only default admin is allowed to change this setting"); + } + + if (AccountManagerImpl.listOfRoleTypesAllowedForOperationsOfSameRoleType.key().equals(configName)) { + if (value != null && !value.isBlank()) { + List<String> validRoleTypes = Arrays.stream(RoleType.values()) + .map(Enum::name) + .collect(Collectors.toList()); + + boolean allValid = Arrays.stream(value.split(",")) + .map(String::trim) + .allMatch(validRoleTypes::contains); + + if (!allValid) { + throw new CloudRuntimeException("Invalid role types provided in value"); + } + } else { + throw new CloudRuntimeException("Value for role types must not be empty"); + } + } + } + } + /** * A valid value should be an integer between min and max (the values from the range). */ diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 959a0dc3bb2..0e9fc0f80ef 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -784,6 +784,7 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim } else { _accountMgr.checkAccess(caller, null, true, account); } + _accountMgr.verifyCallerPrivilegeForUserOrAccountOperations(account); ownerType = ResourceOwnerType.Account; ownerId = accountId; @@ -853,6 +854,11 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim throw new InvalidParameterValueException("Please specify a valid domain ID."); } _accountMgr.checkAccess(callerAccount, domain); + Account account = _entityMgr.findById(Account.class, accountId); + if (account == null) { + throw new InvalidParameterValueException("Unable to find account " + accountId); + } + _accountMgr.verifyCallerPrivilegeForUserOrAccountOperations(account); if (resourceType != null) { resourceTypes.add(resourceType); diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index 1d7611d5b54..98939cb7deb 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -200,4 +200,5 @@ public interface AccountManager extends AccountService, Configurable { UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccount user); + void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount); } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 180f4e49521..cd07303a82d 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -20,6 +20,7 @@ import java.net.InetAddress; import java.net.URLEncoder; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -386,6 +387,22 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M true, ConfigKey.Scope.Domain); + public static ConfigKey<String> listOfRoleTypesAllowedForOperationsOfSameRoleType = new ConfigKey<>("Hidden", + String.class, + "role.types.allowed.for.operations.on.accounts.of.same.role.type", + "Admin, ResourceAdmin, DomainAdmin", + "Comma separated list of role types that are allowed to do operations on accounts or users of the same role type within a domain.", + true, + ConfigKey.Scope.Domain); + + public static ConfigKey<Boolean> allowOperationsOnUsersInSameAccount = new ConfigKey<>("Hidden", + Boolean.class, + "allow.operations.on.users.in.same.account", + "true", + "Allow operations on users among them in the same account", + true, + ConfigKey.Scope.Domain); + protected AccountManagerImpl() { super(); } @@ -1369,7 +1386,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M /** * if there is any permission under the requested role that is not permitted for the caller, refuse */ - private void checkRoleEscalation(Account caller, Account requested) { + protected void checkRoleEscalation(Account caller, Account requested) { if (s_logger.isDebugEnabled()) { s_logger.debug(String.format("Checking if user of account %s [%s] with role-id [%d] can create an account of type %s [%s] with role-id [%d]", caller.getAccountName(), @@ -1471,7 +1488,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // users can't exist in same account assertUserNotAlreadyInAccount(duplicatedUser, account); } - + verifyCallerPrivilegeForUserOrAccountOperations(account); UserVO user = null; user = createUser(account.getId(), userName, password, firstName, lastName, email, timeZone, userUUID, source); return user; @@ -1488,10 +1505,14 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @ActionEvent(eventType = EventTypes.EVENT_USER_UPDATE, eventDescription = "Updating User") public UserAccount updateUser(UpdateUserCmd updateUserCmd) { UserVO user = retrieveAndValidateUser(updateUserCmd); + Account account = retrieveAndValidateAccount(user); + User caller = CallContext.current().getCallingUser(); + checkAccess(caller, account); + verifyCallerPrivilegeForUserOrAccountOperations(user); + s_logger.debug("Updating user with Id: " + user.getUuid()); validateAndUpdateApiAndSecretKeyIfNeeded(updateUserCmd, user); - Account account = retrieveAndValidateAccount(user); validateAndUpdateFirstNameIfNeeded(updateUserCmd, user); validateAndUpdateLastNameIfNeeded(updateUserCmd, user); @@ -1514,6 +1535,85 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return _userAccountDao.findById(user.getId()); } + @Override + public void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount) { + s_logger.debug(String.format("Verifying whether the caller has the correct privileges based on the user's role type and API permissions: %s", userAccount)); + + checkCallerRoleTypeAllowedForUserOrAccountOperations(userAccount, null); + checkCallerApiPermissionsForUserOrAccountOperations(userAccount); + } + + protected void verifyCallerPrivilegeForUserOrAccountOperations(User user) { + s_logger.debug(String.format("Verifying whether the caller has the correct privileges based on the user's role type and API permissions: %s", user)); + + Account userAccount = getAccount(user.getAccountId()); + checkCallerRoleTypeAllowedForUserOrAccountOperations(userAccount, user); + checkCallerApiPermissionsForUserOrAccountOperations(userAccount); + } + + protected void checkCallerRoleTypeAllowedForUserOrAccountOperations(Account userAccount, User user) { + Account callingAccount = getCurrentCallingAccount(); + RoleType callerRoleType = getRoleType(callingAccount); + RoleType userAccountRoleType = getRoleType(userAccount); + + if (RoleType.Unknown == callerRoleType || RoleType.Unknown == userAccountRoleType) { + String errMsg = String.format("The role type of account [%s, %s] or [%s, %s] is unknown", + callingAccount.getName(), callingAccount.getUuid(), userAccount.getName(), userAccount.getUuid()); + throw new PermissionDeniedException(errMsg); + } + + boolean isCallerSystemOrDefaultAdmin = callingAccount.getId() == Account.ACCOUNT_ID_SYSTEM || callingAccount.getId() == Account.ACCOUNT_ID_ADMIN; + if (isCallerSystemOrDefaultAdmin) { + s_logger.trace(String.format("Admin account [%s, %s] performing this operation for user account [%s, %s] ", callingAccount.getName(), callingAccount.getUuid(), userAccount.getName(), userAccount.getUuid())); + } else if (callerRoleType.getId() < userAccountRoleType.getId()) { + s_logger.trace(String.format("The calling account [%s] has a higher role type than the user account [%s]", + callingAccount, userAccount)); + } else if (callerRoleType.getId() == userAccountRoleType.getId()) { + if (callingAccount.getId() != userAccount.getId()) { + String allowedRoleTypes = listOfRoleTypesAllowedForOperationsOfSameRoleType.valueInDomain(callingAccount.getDomainId()); + boolean updateAllowed = allowedRoleTypes != null && + Arrays.stream(allowedRoleTypes.split(",")) + .map(String::trim) + .anyMatch(role -> role.equals(callerRoleType.toString())); + if (BooleanUtils.isFalse(updateAllowed)) { + String errMsg = String.format("The calling account [%s, %s] is not allowed to perform this operation on users from other accounts " + + "of the same role type within the domain", callingAccount.getName(), callingAccount.getUuid()); + s_logger.error(errMsg); + throw new PermissionDeniedException(errMsg); + } + } else if ((callingAccount.getId() == userAccount.getId()) && user != null) { + Boolean allowOperationOnUsersinSameAccount = allowOperationsOnUsersInSameAccount.valueInDomain(callingAccount.getDomainId()); + User callingUser = CallContext.current().getCallingUser(); + if (callingUser.getId() != user.getId() && BooleanUtils.isFalse(allowOperationOnUsersinSameAccount)) { + String errMsg = "The user operations are not allowed by the users in the same account"; + s_logger.error(errMsg); + throw new PermissionDeniedException(errMsg); + } + } + } else { + String errMsg = String.format("The calling account [%s, %s] has a lower role type than the user account [%s, %s]", + callingAccount.getName(), callingAccount.getUuid(), userAccount.getName(), userAccount.getUuid()); + throw new PermissionDeniedException(errMsg); + } + } + + protected void checkCallerApiPermissionsForUserOrAccountOperations(Account userAccount) { + Account callingAccount = getCurrentCallingAccount(); + boolean isCallerRootAdmin = callingAccount.getId() == Account.ACCOUNT_ID_SYSTEM || isRootAdmin(callingAccount.getId()); + + if (isCallerRootAdmin) { + s_logger.trace(String.format("Admin account [%s, %s] performing this operation for user account [%s, %s] ", callingAccount.getName(), callingAccount.getUuid(), userAccount.getName(), userAccount.getUuid())); + } else if (isRootAdmin(userAccount.getAccountId())) { + String errMsg = String.format("Account [%s, %s] cannot perform this operation for user account [%s, %s] ", callingAccount.getName(), callingAccount.getUuid(), userAccount.getName(), userAccount.getUuid()); + s_logger.error(errMsg); + throw new PermissionDeniedException(errMsg); + } else { + s_logger.debug(String.format("Checking calling account [%s, %s] permission to perform this operation for user account [%s, %s] ", callingAccount.getName(), callingAccount.getUuid(), userAccount.getName(), userAccount.getUuid())); + checkRoleEscalation(callingAccount, userAccount); + s_logger.debug(String.format("Calling account [%s, %s] is allowed to perform this operation for user account [%s, %s] ", callingAccount.getName(), callingAccount.getUuid(), userAccount.getName(), userAccount.getUuid())); + } + } + /** * Updates the password in the user POJO if needed. If no password is provided, then the password is not updated. * The following validations are executed if 'password' is not null. Admins (root admins or domain admins) can execute password updates without entering the current password. @@ -1760,6 +1860,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } checkAccess(caller, AccessType.OperateEntry, true, account); + verifyCallerPrivilegeForUserOrAccountOperations(user); boolean success = doSetUserStatus(userId, State.DISABLED); if (success) { @@ -1801,6 +1902,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } checkAccess(caller, AccessType.OperateEntry, true, account); + verifyCallerPrivilegeForUserOrAccountOperations(user); boolean success = Transaction.execute(new TransactionCallback<Boolean>() { @Override @@ -1853,6 +1955,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } checkAccess(caller, AccessType.OperateEntry, true, account); + verifyCallerPrivilegeForUserOrAccountOperations(user); // make sure the account is enabled too // if the user is either locked already or disabled already, don't change state...only lock currently enabled @@ -1909,6 +2012,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } checkIfAccountManagesProjects(accountId); + verifyCallerPrivilegeForUserOrAccountOperations(account); CallContext.current().putContextParameter(Account.class, account.getUuid()); @@ -1971,6 +2075,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // Check if user performing the action is allowed to modify this account Account caller = getCurrentCallingAccount(); checkAccess(caller, AccessType.OperateEntry, true, account); + verifyCallerPrivilegeForUserOrAccountOperations(account); boolean success = enableAccount(account.getId()); if (success) { @@ -2004,6 +2109,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } checkAccess(caller, AccessType.OperateEntry, true, account); + verifyCallerPrivilegeForUserOrAccountOperations(account); if (lockAccount(account.getId())) { CallContext.current().putContextParameter(Account.class, account.getUuid()); @@ -2034,6 +2140,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } checkAccess(caller, AccessType.OperateEntry, true, account); + verifyCallerPrivilegeForUserOrAccountOperations(account); if (disableAccount(account.getId())) { CallContext.current().putContextParameter(Account.class, account.getUuid()); @@ -2079,6 +2186,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // Check if user performing the action is allowed to modify this account Account caller = getCurrentCallingAccount(); checkAccess(caller, _domainMgr.getDomain(account.getDomainId())); + verifyCallerPrivilegeForUserOrAccountOperations(account); if(newAccountName != null) { @@ -2160,6 +2268,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // don't allow to delete the user from the account of type Project checkAccountAndAccess(user, account); + verifyCallerPrivilegeForUserOrAccountOperations(user); + return Transaction.execute((TransactionCallback<Boolean>) status -> deleteAndCleanupUser(user)); } @@ -2178,6 +2288,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M UserVO user = getValidUserVO(id); Account oldAccount = _accountDao.findById(user.getAccountId()); checkAccountAndAccess(user, oldAccount); + verifyCallerPrivilegeForUserOrAccountOperations(user); long domainId = oldAccount.getDomainId(); long newAccountId = getNewAccountId(domainId, cmd.getAccountName(), cmd.getAccountId()); @@ -2515,6 +2626,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } } + if (!Account.Type.PROJECT.equals(accountType)) { + AccountVO newAccount = new AccountVO(accountName, domainId, networkDomain, accountType, roleId, uuid); + verifyCallerPrivilegeForUserOrAccountOperations(newAccount); + } + // Create the account return Transaction.execute(new TransactionCallback<AccountVO>() { @Override @@ -2863,8 +2979,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } final ControlledEntity account = getAccount(getUserAccountById(userId).getAccountId()); //Extracting the Account from the userID of the requested user. User caller = CallContext.current().getCallingUser(); - preventRootDomainAdminAccessToRootAdminKeys(caller, account); checkAccess(caller, account); + verifyCallerPrivilegeForUserOrAccountOperations(user); Map<String, String> keys = new HashMap<String, String>(); keys.put("apikey", user.getApiKey()); @@ -2920,8 +3036,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } Account account = _accountDao.findById(user.getAccountId()); - preventRootDomainAdminAccessToRootAdminKeys(user, account); checkAccess(caller, null, true, account); + verifyCallerPrivilegeForUserOrAccountOperations(user); // don't allow updating system user if (user.getId() == User.UID_SYSTEM) { @@ -3377,7 +3493,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M public ConfigKey<?>[] getConfigKeys() { return new ConfigKey<?>[] {UseSecretKeyInResponse, enableUserTwoFactorAuthentication, userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer, - userAllowMultipleAccounts}; + userAllowMultipleAccounts, listOfRoleTypesAllowedForOperationsOfSameRoleType, allowOperationsOnUsersInSameAccount}; } public List<UserTwoFactorAuthenticator> getUserTwoFactorAuthenticationProviders() { diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index 8f4dbbe675d..f4541cd49f3 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@ -16,12 +16,16 @@ // under the License. package com.cloud.configuration; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.cloud.exception.InvalidParameterValueException; import com.cloud.storage.StorageManager; +import com.cloud.user.AccountManagerImpl; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.dc.dao.DataCenterDao; @@ -97,6 +101,7 @@ public class ConfigurationManagerImplTest { @Before public void setUp() throws Exception { configurationManagerImplSpy._configDepot = configDepot; + configurationManagerImplSpy.populateConfigKeysAllowedOnlyForDefaultAdmin(); } @Test @@ -474,4 +479,46 @@ public class ConfigurationManagerImplTest { Assert.assertThrows(InvalidParameterValueException.class, () -> configurationManagerImplSpy.checkIfDomainIsChildDomain(diskOfferingMock, accountMock, userMock, filteredDomainIds)); } + + @Test + public void testValidateConfigurationAllowedOnlyForDefaultAdmin_withAdminUser_shouldNotThrowException() { + CallContext callContext = mock(CallContext.class); + when(callContext.getCallingUserId()).thenReturn(User.UID_ADMIN); + try (MockedStatic<CallContext> ignored = Mockito.mockStatic(CallContext.class)) { + when(CallContext.current()).thenReturn(callContext); + configurationManagerImplSpy.validateConfigurationAllowedOnlyForDefaultAdmin(AccountManagerImpl.listOfRoleTypesAllowedForOperationsOfSameRoleType.key(), "Admin"); + } + } + + @Test + public void testValidateConfigurationAllowedOnlyForDefaultAdmin_withNonAdminUser_shouldThrowException() { + CallContext callContext = mock(CallContext.class); + when(callContext.getCallingUserId()).thenReturn(123L); + try (MockedStatic<CallContext> ignored = Mockito.mockStatic(CallContext.class)) { + when(CallContext.current()).thenReturn(callContext); + assertThrows(CloudRuntimeException.class, () -> + configurationManagerImplSpy.validateConfigurationAllowedOnlyForDefaultAdmin(AccountManagerImpl.allowOperationsOnUsersInSameAccount.key(), "Admin") + ); + } + } + + @Test + public void testValidateConfigurationAllowedOnlyForDefaultAdmin_withNonRestrictedKey_shouldNotThrowException() { + CallContext callContext = mock(CallContext.class); + try (MockedStatic<CallContext> ignored = Mockito.mockStatic(CallContext.class)) { + when(CallContext.current()).thenReturn(callContext); + configurationManagerImplSpy.validateConfigurationAllowedOnlyForDefaultAdmin("some.other.config.key", "Admin"); + } + } + + @Test(expected = CloudRuntimeException.class) + public void testValidateConfigurationAllowedOnlyForDefaultAdmin_withValidConfigNameAndInvalidValue_shouldThrowException() { + CallContext callContext = mock(CallContext.class); + try (MockedStatic<CallContext> mockedCallContext = Mockito.mockStatic(CallContext.class)) { + mockedCallContext.when(CallContext::current).thenReturn(callContext); + when(callContext.getCallingUserId()).thenReturn(User.UID_ADMIN); + String invalidValue = "Admin, SuperUser"; + configurationManagerImplSpy.validateConfigurationAllowedOnlyForDefaultAdmin(AccountManagerImpl.listOfRoleTypesAllowedForOperationsOfSameRoleType.key(), invalidValue); + } + } } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index f1cf0008676..8472a85cc30 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -108,6 +108,9 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock ConfigKey<Boolean> enableUserTwoFactorAuthenticationMock; + + @Mock + ConfigKey<Boolean> allowOperationsOnUsersInSameAccountMock; @Mock RoleService roleService; @@ -115,6 +118,9 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { public void setUp() throws Exception { enableUserTwoFactorAuthenticationMock = Mockito.mock(ConfigKey.class); accountManagerImpl.enableUserTwoFactorAuthentication = enableUserTwoFactorAuthenticationMock; + + allowOperationsOnUsersInSameAccountMock = Mockito.mock(ConfigKey.class); + accountManagerImpl.allowOperationsOnUsersInSameAccount = allowOperationsOnUsersInSameAccountMock; } @Before @@ -125,6 +131,8 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.doReturn(accountMockId).when(userVoMock).getAccountId(); Mockito.doReturn(userVoIdMock).when(userVoMock).getId(); + + Mockito.lenient().doNothing().when(accountManagerImpl).checkRoleEscalation(accountMock, accountMock); } @Test @@ -166,6 +174,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.lenient().when(securityChecker.checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class))).thenReturn(true); Mockito.when(_vmSnapshotDao.listByAccountId(Mockito.anyLong())).thenReturn(new ArrayList<VMSnapshotVO>()); Mockito.when(_autoscaleMgr.deleteAutoScaleVmGroupsByAccount(42l)).thenReturn(true); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(Account.class)); List<SSHKeyPairVO> sshkeyList = new ArrayList<SSHKeyPairVO>(); SSHKeyPairVO sshkey = new SSHKeyPairVO(); @@ -193,6 +202,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.when(_vmMgr.expunge(Mockito.any(UserVmVO.class))).thenReturn(false); Mockito.lenient().when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domain); Mockito.lenient().when(securityChecker.checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class))).thenReturn(true); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(Account.class)); Assert.assertTrue(accountManagerImpl.deleteUserAccount(42l)); // assert that this was NOT a clean delete @@ -261,7 +271,6 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { // Queried account - admin account AccountVO adminAccountMock = Mockito.mock(AccountVO.class); - Mockito.when(adminAccountMock.getAccountId()).thenReturn(2L); Mockito.when(_accountDao.findByIdIncludingRemoved(2L)).thenReturn(adminAccountMock); Mockito.lenient().when(accountService.isRootAdmin(2L)).thenReturn(true); Mockito.lenient().when(securityChecker.checkAccess(Mockito.any(Account.class), @@ -309,6 +318,12 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Test public void updateUserTestTimeZoneAndEmailNull() { + Mockito.when(userVoMock.getAccountId()).thenReturn(10L); + Mockito.doReturn(accountMock).when(accountManagerImpl).getAccount(10L); + Mockito.when(accountMock.getAccountId()).thenReturn(10L); + Mockito.doReturn(false).when(accountManagerImpl).isRootAdmin(10L); + Mockito.lenient().when(accountManagerImpl.getRoleType(Mockito.eq(accountMock))).thenReturn(RoleType.User); + prepareMockAndExecuteUpdateUserTest(0); } @@ -316,6 +331,11 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { public void updateUserTestTimeZoneAndEmailNotNull() { Mockito.when(UpdateUserCmdMock.getEmail()).thenReturn("email"); Mockito.when(UpdateUserCmdMock.getTimezone()).thenReturn("timezone"); + Mockito.when(userVoMock.getAccountId()).thenReturn(10L); + Mockito.doReturn(accountMock).when(accountManagerImpl).getAccount(10L); + Mockito.when(accountMock.getAccountId()).thenReturn(10L); + Mockito.doReturn(false).when(accountManagerImpl).isRootAdmin(10L); + Mockito.lenient().when(accountManagerImpl.getRoleType(Mockito.eq(accountMock))).thenReturn(RoleType.User); prepareMockAndExecuteUpdateUserTest(1); } @@ -333,14 +353,17 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.doReturn(true).when(userDaoMock).update(Mockito.anyLong(), Mockito.eq(userVoMock)); Mockito.doReturn(Mockito.mock(UserAccountVO.class)).when(userAccountDaoMock).findById(Mockito.anyLong()); + Mockito.doNothing().when(accountManagerImpl).checkAccess(nullable(User.class), nullable(Account.class)); accountManagerImpl.updateUser(UpdateUserCmdMock); + Mockito.lenient().doNothing().when(accountManagerImpl).checkRoleEscalation(accountMock, accountMock); + InOrder inOrder = Mockito.inOrder(userVoMock, accountManagerImpl, userDaoMock, userAccountDaoMock); inOrder.verify(accountManagerImpl).retrieveAndValidateUser(UpdateUserCmdMock); - inOrder.verify(accountManagerImpl).validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); inOrder.verify(accountManagerImpl).retrieveAndValidateAccount(userVoMock); + inOrder.verify(accountManagerImpl).validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); inOrder.verify(accountManagerImpl).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock); inOrder.verify(accountManagerImpl).validateAndUpdateLastNameIfNeeded(UpdateUserCmdMock, userVoMock); @@ -1203,8 +1226,8 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { accountManagerImpl.validateRoleChange(account, newRole, caller); } - @Test - public void checkIfAccountManagesProjectsTestNotThrowExceptionWhenTheAccountIsNotAProjectAdministrator() { + @Test + public void checkIfAccountManagesProjectsTestNotThrowExceptionWhenTheAccountIsNotAProjectAdministrator() { long accountId = 1L; List<Long> managedProjectIds = new ArrayList<>(); @@ -1341,4 +1364,74 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { accountManagerImpl.assertUserNotAlreadyInDomain(existingUser, originalAccount); } + + @Test + public void testCheckCallerRoleTypeAllowedToUpdateUserSameAccount() { + Mockito.lenient().when(accountManagerImpl.getCurrentCallingAccount()).thenReturn(accountMock); + Mockito.lenient().when(accountManagerImpl.getRoleType(Mockito.eq(accountMock))).thenReturn(RoleType.DomainAdmin); + + accountManagerImpl.checkCallerRoleTypeAllowedForUserOrAccountOperations(accountMock, userVoMock); + } + + @Test(expected = PermissionDeniedException.class) + public void testCheckCallerRoleTypeAllowedToUpdateUserLowerAccountRoleType() { + Account callingAccount = Mockito.mock(Account.class); + Mockito.lenient().when(callingAccount.getAccountId()).thenReturn(2L); + Mockito.lenient().doReturn(callingAccount).when(accountManagerImpl).getAccount(2L); + Mockito.lenient().when(accountManagerImpl.getCurrentCallingAccount()).thenReturn(callingAccount); + Mockito.lenient().when(accountManagerImpl.getRoleType(Mockito.eq(callingAccount))).thenReturn(RoleType.DomainAdmin); + Mockito.lenient().when(accountManagerImpl.getRoleType(Mockito.eq(accountMock))).thenReturn(RoleType.Admin); + accountManagerImpl.checkCallerRoleTypeAllowedForUserOrAccountOperations(accountMock, userVoMock); + } + + @Test + public void testcheckCallerApiPermissionsForUserOperationsRootAdminSameCaller() { + Mockito.lenient().when(accountManagerImpl.getCurrentCallingAccount()).thenReturn(accountMock); + Mockito.when(accountMock.getId()).thenReturn(2L); + Mockito.doReturn(true).when(accountManagerImpl).isRootAdmin(2L); + accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock); + } + + @Test(expected = PermissionDeniedException.class) + public void testcheckCallerApiPermissionsForUserOperationsRootAdminDifferentAccount() { + Mockito.lenient().when(accountManagerImpl.getCurrentCallingAccount()).thenReturn(callingAccount); + Mockito.lenient().when(callingAccount.getAccountId()).thenReturn(3L); + Mockito.lenient().doReturn(callingAccount).when(accountManagerImpl).getAccount(3L); + Mockito.lenient().doReturn(false).when(accountManagerImpl).isRootAdmin(3L); + + Mockito.when(accountMock.getAccountId()).thenReturn(2L); + Mockito.doReturn(true).when(accountManagerImpl).isRootAdmin(2L); + + accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock); + } + + @Test + public void testcheckCallerApiPermissionsForUserOperationsAllowedApis() { + Mockito.lenient().when(accountManagerImpl.getCurrentCallingAccount()).thenReturn(callingAccount); + Mockito.lenient().when(callingAccount.getAccountId()).thenReturn(3L); + Mockito.lenient().doReturn(callingAccount).when(accountManagerImpl).getAccount(3L); + Mockito.lenient().doReturn(false).when(accountManagerImpl).isRootAdmin(3L); + + Mockito.when(accountMock.getAccountId()).thenReturn(2L); + Mockito.doReturn(false).when(accountManagerImpl).isRootAdmin(2L); + + Mockito.lenient().doNothing().when(accountManagerImpl).checkRoleEscalation(callingAccount, accountMock); + + accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock); + } + + @Test(expected = PermissionDeniedException.class) + public void testcheckCallerApiPermissionsForUserOperationsNotAllowedApis() { + Mockito.lenient().when(accountManagerImpl.getCurrentCallingAccount()).thenReturn(callingAccount); + Mockito.lenient().when(callingAccount.getAccountId()).thenReturn(3L); + Mockito.lenient().doReturn(callingAccount).when(accountManagerImpl).getAccount(3L); + Mockito.lenient().doReturn(false).when(accountManagerImpl).isRootAdmin(3L); + + Mockito.when(accountMock.getAccountId()).thenReturn(2L); + Mockito.doReturn(false).when(accountManagerImpl).isRootAdmin(2L); + + Mockito.lenient().doThrow(PermissionDeniedException.class).when(accountManagerImpl).checkRoleEscalation(callingAccount, accountMock); + + accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock); + } } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplVolumeDeleteEventTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplVolumeDeleteEventTest.java index 21474a53ee7..31a12c0432c 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplVolumeDeleteEventTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplVolumeDeleteEventTest.java @@ -193,6 +193,7 @@ public class AccountManagerImplVolumeDeleteEventTest extends AccountManagetImplT public void destroyedVMRootVolumeUsageEvent() throws SecurityException, IllegalArgumentException, ReflectiveOperationException, AgentUnavailableException, ConcurrentOperationException, CloudException { Mockito.lenient().doReturn(vm).when(_vmMgr).destroyVm(nullable(Long.class), nullable(Boolean.class)); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(Account.class)); List<UsageEventVO> emittedEvents = deleteUserAccountRootVolumeUsageEvents(true); Assert.assertEquals(0, emittedEvents.size()); } @@ -204,6 +205,7 @@ public class AccountManagerImplVolumeDeleteEventTest extends AccountManagetImplT throws SecurityException, IllegalArgumentException, ReflectiveOperationException, AgentUnavailableException, ConcurrentOperationException, CloudException { Mockito.doNothing().when(vmStatsDaoMock).removeAllByVmId(Mockito.anyLong()); Mockito.lenient().when(_vmMgr.destroyVm(nullable(Long.class), nullable(Boolean.class))).thenReturn(vm); + Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations(Mockito.any(Account.class)); List<UsageEventVO> emittedEvents = deleteUserAccountRootVolumeUsageEvents(false); UsageEventVO event = emittedEvents.get(0); Assert.assertEquals(EventTypes.EVENT_VOLUME_DELETE, event.getType()); diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 96b73cc63dd..414b98aacfd 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -488,4 +488,8 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco public UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccount user) { return null; } + + @Override + public void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount) { + } }