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);
+         }
+     }
  }

Reply via email to