This is an automated email from the ASF dual-hosted git repository. dahn pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit 650b5ec3dab2bc7773eb46b134114b974313f609 Merge: 99863c2fa5a 6bb6fe7b41f Author: Daan Hoogland <[email protected]> AuthorDate: Tue May 27 18:18:39 2025 +0200 Merge branch '4.20' .../cluster/KubernetesServiceHelper.java | 2 + api/src/main/java/com/cloud/user/Account.java | 1 + .../engine/orchestration/NetworkOrchestrator.java | 1 + .../main/java/com/cloud/user/dao/AccountDao.java | 8 +- .../java/com/cloud/user/dao/AccountDaoImpl.java | 25 +- .../api/command/QuotaConfigureEmailCmd.java | 2 + .../command/QuotaListEmailConfigurationCmd.java | 5 +- .../wrapper/xenbase/CitrixStartCommandWrapper.java | 3 +- .../cluster/KubernetesClusterManagerImpl.java | 254 +++++++++++++-- .../cluster/KubernetesClusterService.java | 8 +- .../cluster/KubernetesServiceHelperImpl.java | 8 + .../cluster/dao/KubernetesClusterDao.java | 4 +- .../cluster/dao/KubernetesClusterDaoImpl.java | 33 +- .../contrail/management/MockAccountManager.java | 4 + .../cloud/network/guru/VxlanGuestNetworkGuru.java | 3 + .../java/com/cloud/api/query/QueryManagerImpl.java | 6 +- .../configuration/ConfigurationManagerImpl.java | 39 ++- .../network/guru/ExternalGuestNetworkGuru.java | 25 +- .../com/cloud/network/guru/GuestNetworkGuru.java | 24 ++ .../resourcelimit/ResourceLimitManagerImpl.java | 6 + .../main/java/com/cloud/user/AccountManager.java | 1 + .../java/com/cloud/user/AccountManagerImpl.java | 355 +++++++++++++++------ .../ConfigurationManagerImplTest.java | 46 +++ .../com/cloud/user/AccountManagerImplTest.java | 104 +++++- .../AccountManagerImplVolumeDeleteEventTest.java | 1 + .../com/cloud/user/AccountManagetImplTestBase.java | 3 + .../com/cloud/user/MockAccountManagerImpl.java | 4 + 27 files changed, 790 insertions(+), 185 deletions(-) diff --cc plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java index b889b559eb2,ee517c1ccf9..f551344ec0e --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java @@@ -187,6 -216,34 +217,32 @@@ import org.apache.logging.log4j.Level public class KubernetesClusterManagerImpl extends ManagerBase implements KubernetesClusterService { private static final String DEFAULT_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME = "DefaultNetworkOfferingforKubernetesService"; + private static final List<Class<?>> PROJECT_KUBERNETES_ACCOUNT_ROLE_ALLOWED_APIS = Arrays.asList( + QueryAsyncJobResultCmd.class, + ListVMsCmd.class, + ListNetworksCmd.class, + ListPublicIpAddressesCmd.class, + AssociateIPAddrCmd.class, + DisassociateIPAddrCmd.class, + ListLoadBalancerRulesCmd.class, + CreateLoadBalancerRuleCmd.class, + UpdateLoadBalancerRuleCmd.class, + DeleteLoadBalancerRuleCmd.class, + AssignToLoadBalancerRuleCmd.class, + RemoveFromLoadBalancerRuleCmd.class, + ListLoadBalancerRuleInstancesCmd.class, + ListFirewallRulesCmd.class, + CreateFirewallRuleCmd.class, + UpdateFirewallRuleCmd.class, + DeleteFirewallRuleCmd.class, + ListNetworkACLsCmd.class, + CreateNetworkACLCmd.class, + DeleteNetworkACLCmd.class, + ListKubernetesClustersCmd.class, + ScaleKubernetesClusterCmd.class + ); + private static final String PROJECT_KUBERNETES_ACCOUNT_FIRST_NAME = "Kubernetes"; + private static final String PROJECT_KUBERNETES_ACCOUNT_LAST_NAME = "Service User"; - - private static final String DEFAULT_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT = "Network Offering used for CloudStack Kubernetes service"; private static final String DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME = "DefaultNSXNetworkOfferingforKubernetesService"; private static final String DEFAULT_NSX_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME = "DefaultNSXVPCNetworkOfferingforKubernetesService"; diff --cc server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 9db4c0e92c9,51e79ae72c4..21cc3a2b3bd --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@@ -50,7 -50,8 +50,7 @@@ import java.util.stream.Collectors import javax.inject.Inject; import javax.naming.ConfigurationException; - import com.cloud.resource.ResourceManager; -import com.cloud.user.AccountManagerImpl; + import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupService; @@@ -254,6 -255,6 +254,7 @@@ import com.cloud.org.Grouping import com.cloud.org.Grouping.AllocationState; import com.cloud.projects.Project; import com.cloud.projects.ProjectManager; ++import com.cloud.resource.ResourceManager; import com.cloud.server.ConfigurationServer; import com.cloud.server.ManagementService; import com.cloud.service.ServiceOfferingDetailsVO; @@@ -277,6 -278,6 +278,7 @@@ import com.cloud.user.Account import com.cloud.user.AccountDetailVO; import com.cloud.user.AccountDetailsDao; import com.cloud.user.AccountManager; ++import com.cloud.user.AccountManagerImpl; import com.cloud.user.AccountVO; import com.cloud.user.ResourceLimitService; import com.cloud.user.User; @@@ -1227,12 -1230,12 +1236,13 @@@ public class ConfigurationManagerImpl e logger.error("Missing configuration variable " + name + " in configuration table"); return "Invalid configuration variable."; } + validateConfigurationAllowedOnlyForDefaultAdmin(name, value); - String configScope = cfg.getScope(); + List<ConfigKey.Scope> configScope = cfg.getScopes(); if (scope != null) { - if (!configScope.contains(scope) && - !(ENABLE_ACCOUNT_SETTINGS_FOR_DOMAIN.value() && configScope.contains(ConfigKey.Scope.Account.toString()) && + ConfigKey.Scope scopeVal = ConfigKey.Scope.valueOf(scope); + if (!configScope.contains(scopeVal) && + !(ENABLE_ACCOUNT_SETTINGS_FOR_DOMAIN.value() && configScope.contains(ConfigKey.Scope.Account) && scope.equals(ConfigKey.Scope.Domain.toString()))) { logger.error("Invalid scope id provided for the parameter " + name); return "Invalid scope id provided for the parameter " + name; diff --cc server/src/main/java/com/cloud/user/AccountManagerImpl.java index 68a820459ae,a0cd5113812..19eba061e13 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@@ -1537,6 -1575,85 +1575,85 @@@ public class AccountManagerImpl extend return _userAccountDao.findById(user.getId()); } + @Override + public void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount) { + 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) { + 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) { + 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()) { + logger.trace(String.format("The calling account [%s, %s] has a higher role type than the user account [%s, %s]", + callingAccount.getName(), callingAccount.getUuid(), userAccount.getName(), userAccount.getUuid())); + } else if (callerRoleType.getId() == userAccountRoleType.getId()) { + if (callingAccount.getId() != userAccount.getId()) { - String allowedRoleTypes = listOfRoleTypesAllowedForOperationsOfSameRoleType.valueInDomain(callingAccount.getDomainId()); ++ String allowedRoleTypes = listOfRoleTypesAllowedForOperationsOfSameRoleType.valueInScope(ConfigKey.Scope.Domain, 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()); + logger.error(errMsg); + throw new PermissionDeniedException(errMsg); + } + } else if ((callingAccount.getId() == userAccount.getId()) && user != null) { - Boolean allowOperationOnUsersinSameAccount = allowOperationsOnUsersInSameAccount.valueInDomain(callingAccount.getDomainId()); ++ Boolean allowOperationOnUsersinSameAccount = allowOperationsOnUsersInSameAccount.valueInScope(ConfigKey.Scope.Domain, 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"; + 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) { + 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()); + logger.error(errMsg); + throw new PermissionDeniedException(errMsg); + } else { + 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); + 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. diff --cc server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index 8c714b57cdb,1309842b706..9b988c0058f --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@@ -45,10 -45,11 +45,12 @@@ import com.cloud.storage.StorageManager import com.cloud.storage.dao.VMTemplateZoneDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.user.Account; + import com.cloud.user.AccountManagerImpl; import com.cloud.user.User; +import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.SearchCriteria; + import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@@ -56,7 -56,7 +58,8 @@@ import org.apache.cloudstack.api.comman import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.zone.DeleteZoneCmd; +import org.apache.cloudstack.config.Configuration; + import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigKey; @@@ -863,25 -856,45 +867,67 @@@ public class ConfigurationManagerImplTe Assert.assertTrue(result); } + @Test + public void testResetConfigurations() { + Long poolId = 1L; + ResetCfgCmd cmd = Mockito.mock(ResetCfgCmd.class); + Mockito.when(cmd.getCfgName()).thenReturn("pool.storage.capacity.disablethreshold"); + Mockito.when(cmd.getStoragepoolId()).thenReturn(poolId); + Mockito.when(cmd.getZoneId()).thenReturn(null); + Mockito.when(cmd.getClusterId()).thenReturn(null); + Mockito.when(cmd.getAccountId()).thenReturn(null); + Mockito.when(cmd.getDomainId()).thenReturn(null); + Mockito.when(cmd.getImageStoreId()).thenReturn(null); + + ConfigurationVO cfg = new ConfigurationVO("Advanced", "DEFAULT", "test", "pool.storage.capacity.disablethreshold", null, "description"); + cfg.setScope(10); + cfg.setDefaultValue(".85"); + Mockito.when(configDao.findByName("pool.storage.capacity.disablethreshold")).thenReturn(cfg); + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(Mockito.mock(StoragePoolVO.class)); + + Pair<Configuration, String> result = configurationManagerImplSpy.resetConfiguration(cmd); + Assert.assertEquals(".85", result.second()); + } ++ + @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); + Assert.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); + } + } }
