This is an automated email from the ASF dual-hosted git repository.

RosiKyu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 70860301eb1 small refactors on account manager (#11784)
70860301eb1 is described below

commit 70860301eb14cb0dc6af1c8546de9997e515d166
Author: dahn <[email protected]>
AuthorDate: Wed Nov 11 19:15:38 2026 +0100

    small refactors on account manager (#11784)
    
    * small refactors on account manager
    
    * fix duplicate import + tests
    
    * account manager: group entities by resolved domain id in checkAccess
---
 .../java/com/cloud/user/AccountManagerImpl.java    | 34 +++++-----
 .../com/cloud/user/AccountManagerImplTest.java     | 75 ++++++++++++++++++++++
 2 files changed, 92 insertions(+), 17 deletions(-)

diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java 
b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
index 0fd46bc8440..e72700b8f82 100644
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@ -125,7 +125,6 @@ import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.ObjectUtils;
-import org.apache.commons.lang3.StringUtils;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 
@@ -212,6 +211,7 @@ import com.cloud.user.dao.UserDataDao;
 import com.cloud.utils.ConstantTimeComparator;
 import com.cloud.utils.NumbersUtil;
 import com.cloud.utils.Pair;
+import com.cloud.utils.StringUtils;
 import com.cloud.utils.Ternary;
 import com.cloud.utils.UuidUtils;
 import com.cloud.utils.component.ComponentContext;
@@ -647,7 +647,6 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
                 return false;  //account is deleted or does not exist
             }
             return (isRootAdmin(accountId)) || (isDomainAdmin(accountId)) || 
(isResourceDomainAdmin(accountId)) || (acct.getType() == 
Account.Type.READ_ONLY_ADMIN);
-
         }
         return false;
     }
@@ -782,23 +781,13 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
         HashMap<Long, List<ControlledEntity>> domains = new HashMap<>();
 
         for (ControlledEntity entity : entities) {
-            long domainId = entity.getDomainId();
-            if (entity.getAccountId() != -1 && domainId == -1) { // If account 
exists domainId should too so calculate
-                // it. This condition might be hit for templates or entities 
which miss domainId in their tables
-                Account account = 
ApiDBUtils.findAccountById(entity.getAccountId());
-                domainId = account != null ? account.getDomainId() : -1;
-            }
+            long domainId = getDomainIdFor(entity);
             if (entity.getAccountId() != -1 && domainId != -1 && !(entity 
instanceof VirtualMachineTemplate)
                     && !(entity instanceof Network && (accessType == 
AccessType.UseEntry || accessType == AccessType.OperateEntry))
                     && !(entity instanceof AffinityGroup) && !(entity 
instanceof VirtualRouter)
                     && !(entity instanceof DnsServer) && !(entity instanceof 
DnsZone)) {
-                List<ControlledEntity> toBeChecked = 
domains.get(entity.getDomainId());
                 // for templates, we don't have to do cross domains check
-                if (toBeChecked == null) {
-                    toBeChecked = new ArrayList<>();
-                    domains.put(domainId, toBeChecked);
-                }
-                toBeChecked.add(entity);
+                domains.computeIfAbsent(domainId, k -> new 
ArrayList<>()).add(entity);
             }
             boolean granted = false;
             for (SecurityChecker checker : _securityCheckers) {
@@ -840,6 +829,17 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
 
     }
 
+    private long getDomainIdFor(ControlledEntity entity) {
+        long domainId = entity.getDomainId();
+        if (entity.getAccountId() != -1 && domainId == -1) {
+            // If account exists domainId should too so calculate it.
+            // This condition might be hit for templates or entities which 
miss domainId in their tables
+            Account account = 
ApiDBUtils.findAccountById(entity.getAccountId());
+            domainId = account != null ? account.getDomainId() : -1;
+        }
+        return domainId;
+    }
+
     @Override
     public void validateAccountHasAccessToResource(Account account, AccessType 
accessType, Object resource) {
         Class<?> resourceClass = resource.getClass();
@@ -3067,11 +3067,11 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
             final Boolean ApiSourceCidrChecksEnabled = 
ApiServiceConfiguration.ApiSourceCidrChecksEnabled.value();
 
             if (ApiSourceCidrChecksEnabled) {
-                logger.debug("CIDRs from which account '{}' is allowed to 
perform API calls: {}", account.toString(), accessAllowedCidrs);
+                logger.debug("CIDRs from which account '{}' is allowed to 
perform API calls: {}", account, accessAllowedCidrs);
 
                 // Block when is not in the list of allowed IPs
                 if (!NetUtils.isIpInCidrList(loginIpAddress, 
accessAllowedCidrs.split(","))) {
-                    logger.warn("Request by account '{}' was denied since {} 
does not match {}", account.toString(), loginIpAddress.toString().replace("/", 
""), accessAllowedCidrs);
+                    logger.warn("Request by account '{}' was denied since {} 
does not match {}", account, loginIpAddress.toString().replace("/", ""), 
accessAllowedCidrs);
                     throw new CloudAuthenticationException("Failed to 
authenticate user '" + username + "' in domain '" + domain.getPath() + "' from 
ip "
                             + loginIpAddress.toString().replace("/", "") + "; 
please provide valid credentials");
                 }
@@ -3241,7 +3241,7 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
                     if (unsignedRequestBuffer.length() != 0) {
                         unsignedRequestBuffer.append("&");
                     }
-                    
unsignedRequestBuffer.append(paramName).append("=").append(URLEncoder.encode(paramValue,
 com.cloud.utils.StringUtils.getPreferredCharset()));
+                    
unsignedRequestBuffer.append(paramName).append("=").append(URLEncoder.encode(paramValue,
 StringUtils.getPreferredCharset()));
                 }
             }
 
diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java 
b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
index 955c939f4fa..a2cc9495405 100644
--- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
+++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
@@ -75,6 +75,7 @@ import org.mockito.Mockito;
 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 
 import com.cloud.acl.DomainChecker;
+import com.cloud.api.ApiDBUtils;
 import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
 import com.cloud.domain.Domain;
 import com.cloud.domain.DomainVO;
@@ -451,6 +452,80 @@ public class AccountManagerImplTest extends 
AccountManagentImplTestBase {
         accountManagerImpl.preventRootDomainAdminAccessToRootAdminKeys(user, 
entity);
     }
 
+    @Test
+    public void 
checkAccessResolvesDomainIdFromAccountWhenEntityDomainIdMissing() {
+        Account caller = Mockito.mock(Account.class);
+        Mockito.when(caller.getId()).thenReturn(999L);
+        
Mockito.doReturn(false).when(accountManagerImpl).isRootAdmin(Mockito.anyLong());
+
+        ControlledEntity entity = Mockito.mock(ControlledEntity.class);
+        Mockito.when(entity.getDomainId()).thenReturn(-1L);
+        Mockito.when(entity.getAccountId()).thenReturn(10L);
+
+        Account resolvedAccount = Mockito.mock(Account.class);
+        Mockito.when(resolvedAccount.getDomainId()).thenReturn(7L);
+
+        Domain domain = Mockito.mock(Domain.class);
+        Mockito.when(_domainMgr.getDomain(7L)).thenReturn(domain);
+
+        Mockito.when(securityChecker.checkAccess(caller, entity, 
AccessType.ListEntry, "someApi")).thenReturn(true);
+        Mockito.when(securityChecker.checkAccess(caller, 
domain)).thenReturn(true);
+
+        try (MockedStatic<ApiDBUtils> apiDBUtilsMocked = 
Mockito.mockStatic(ApiDBUtils.class)) {
+            apiDBUtilsMocked.when(() -> 
ApiDBUtils.findAccountById(10L)).thenReturn(resolvedAccount);
+
+            accountManagerImpl.checkAccess(caller, AccessType.ListEntry, 
false, "someApi", entity);
+        }
+
+        // domainId for the entity had to be resolved via its account 
(entity.getDomainId() == -1),
+        // so the domain-level check must have run against the account's 
domain, not against -1.
+        Mockito.verify(_domainMgr).getDomain(7L);
+        Mockito.verify(_domainMgr, Mockito.never()).getDomain(-1L);
+    }
+
+    @Test
+    public void checkAccessKeepsAllEntitiesGroupedUnderResolvedDomainId() {
+        Account caller = Mockito.mock(Account.class);
+        Mockito.when(caller.getId()).thenReturn(999L);
+        
Mockito.doReturn(false).when(accountManagerImpl).isRootAdmin(Mockito.anyLong());
+
+        // Both entities are missing their own domainId and resolve, via 
different accounts, to the same domain.
+        ControlledEntity entity1 = Mockito.mock(ControlledEntity.class);
+        Mockito.when(entity1.getDomainId()).thenReturn(-1L);
+        Mockito.when(entity1.getAccountId()).thenReturn(10L);
+
+        ControlledEntity entity2 = Mockito.mock(ControlledEntity.class);
+        Mockito.when(entity2.getDomainId()).thenReturn(-1L);
+        Mockito.when(entity2.getAccountId()).thenReturn(20L);
+
+        Account resolvedAccount1 = Mockito.mock(Account.class);
+        Mockito.when(resolvedAccount1.getDomainId()).thenReturn(7L);
+        Account resolvedAccount2 = Mockito.mock(Account.class);
+        Mockito.when(resolvedAccount2.getDomainId()).thenReturn(7L);
+
+        Domain domain = Mockito.mock(Domain.class);
+        Mockito.when(_domainMgr.getDomain(7L)).thenReturn(domain);
+
+        Mockito.when(securityChecker.checkAccess(caller, entity1, 
AccessType.ListEntry, "someApi")).thenReturn(true);
+        Mockito.when(securityChecker.checkAccess(caller, entity2, 
AccessType.ListEntry, "someApi")).thenReturn(true);
+        Mockito.when(securityChecker.checkAccess(caller, domain))
+                .thenThrow(new PermissionDeniedException("denied", caller, 
Collections.emptyList()));
+
+        PermissionDeniedException thrown;
+        try (MockedStatic<ApiDBUtils> apiDBUtilsMocked = 
Mockito.mockStatic(ApiDBUtils.class)) {
+            apiDBUtilsMocked.when(() -> 
ApiDBUtils.findAccountById(10L)).thenReturn(resolvedAccount1);
+            apiDBUtilsMocked.when(() -> 
ApiDBUtils.findAccountById(20L)).thenReturn(resolvedAccount2);
+
+            thrown = Assert.assertThrows(PermissionDeniedException.class,
+                    () -> accountManagerImpl.checkAccess(caller, 
AccessType.ListEntry, false, "someApi", entity1, entity2));
+        }
+
+        // Both entities resolve to the same domain, so they must both be 
grouped under that single domain
+        // key and both show up as violations, instead of the second entity 
silently displacing the first.
+        Assert.assertEquals(2, thrown.getEntitiesInViolation().size());
+        
Assert.assertTrue(thrown.getEntitiesInViolation().containsAll(Arrays.asList(entity1,
 entity2)));
+    }
+
     @Test
     public void updateUserTestTimeZoneAndEmailNull() {
         Mockito.when(userVoMock.getAccountId()).thenReturn(10L);

Reply via email to