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

nvazquez pushed a commit to branch 4.18.2.3-RC20240802T1724
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit e7dce2bcce5ba52b7d36551e8b2f0c84e75d7570
Author: Abhishek Kumar <abhishek.mr...@gmail.com>
AuthorDate: Wed Jul 31 14:19:59 2024 +0530

    server, api: account and api entity access improvements
    
    Fixes domain-admin access check to prevent unauthorized access.
    Introduces a new non-dynamic global setting - api.allow.internal.db.ids
    to control whether to allow using internal DB IDs as API parameters or
    not. Default value for the global setting is false.
    
    Co-authored-by: Fabricio Duarte <fabricio.duarte...@gmail.com>
    Co-authored-by: nvazquez <nicovazque...@gmail.com>
    Co-authored-by: Abhishek Kumar <abhishek.mr...@gmail.com>
---
 .github/workflows/ci.yml                           |   1 +
 .../src/main/java/com/cloud/acl/DomainChecker.java |  68 ++++---
 .../java/com/cloud/user/AccountManagerImpl.java    |  18 +-
 .../test/java/com/cloud/acl/DomainCheckerTest.java | 166 +++++++++++++++++
 .../com/cloud/user/AccountManagerImplTest.java     |  58 ++++++
 test/integration/smoke/test_account_access.py      | 198 +++++++++++++++++++++
 6 files changed, 488 insertions(+), 21 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 087e2575f73..8c07af29b8c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,6 +32,7 @@ jobs:
       fail-fast: false
       matrix:
         tests: [ "smoke/test_accounts
+                  smoke/test_account_access
                   smoke/test_affinity_groups
                   smoke/test_affinity_groups_projects
                   smoke/test_annotations
diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java 
b/server/src/main/java/com/cloud/acl/DomainChecker.java
index a8c9ab84f7e..7930be49ec1 100644
--- a/server/src/main/java/com/cloud/acl/DomainChecker.java
+++ b/server/src/main/java/com/cloud/acl/DomainChecker.java
@@ -208,7 +208,7 @@ public class DomainChecker extends AdapterBase implements 
SecurityChecker {
 
             return true;
         } else if (entity instanceof Network && accessType != null && 
accessType == AccessType.UseEntry) {
-            _networkMgr.checkNetworkPermissions(caller, (Network)entity);
+            _networkMgr.checkNetworkPermissions(caller, (Network) entity);
         } else if (entity instanceof Network && accessType != null && 
accessType == AccessType.OperateEntry) {
             _networkMgr.checkNetworkOperatePermissions(caller, 
(Network)entity);
         } else if (entity instanceof VirtualRouter) {
@@ -216,30 +216,58 @@ public class DomainChecker extends AdapterBase implements 
SecurityChecker {
         } else if (entity instanceof AffinityGroup) {
             return false;
         } else {
-            if (_accountService.isNormalUser(caller.getId())) {
-                Account account = _accountDao.findById(entity.getAccountId());
-                String errorMessage = String.format("%s does not have 
permission to operate with resource", caller);
-                if (account != null && account.getType() == 
Account.Type.PROJECT) {
-                    //only project owner can delete/modify the project
-                    if (accessType != null && accessType == 
AccessType.ModifyProject) {
-                        if (!_projectMgr.canModifyProjectAccount(caller, 
account.getId())) {
-                            throw new PermissionDeniedException(errorMessage);
-                        }
-                    } else if (!_projectMgr.canAccessProjectAccount(caller, 
account.getId())) {
-                        throw new PermissionDeniedException(errorMessage);
-                    }
-                    checkOperationPermitted(caller, entity);
-                } else {
-                    if (caller.getId() != entity.getAccountId()) {
-                        throw new PermissionDeniedException(errorMessage);
-                    }
+            validateCallerHasAccessToEntityOwner(caller, entity, accessType);
+        }
+        return true;
+    }
+
+    protected void validateCallerHasAccessToEntityOwner(Account caller, 
ControlledEntity entity, AccessType accessType) {
+        PermissionDeniedException exception = new 
PermissionDeniedException("Caller does not have permission to operate with 
provided resource.");
+        String entityLog = String.format("entity [owner ID: %d, type: %s]", 
entity.getAccountId(),
+                entity.getEntityType().getSimpleName());
+
+        if (_accountService.isRootAdmin(caller.getId())) {
+            return;
+        }
+
+        if (caller.getId() == entity.getAccountId()) {
+            return;
+        }
+
+        Account owner = _accountDao.findById(entity.getAccountId());
+        if (owner == null) {
+            s_logger.error(String.format("Owner not found for %s", entityLog));
+            throw exception;
+        }
+
+        Account.Type callerAccountType = caller.getType();
+        if ((callerAccountType == Account.Type.DOMAIN_ADMIN || 
callerAccountType == Account.Type.RESOURCE_DOMAIN_ADMIN) &&
+                _domainDao.isChildDomain(caller.getDomainId(), 
owner.getDomainId())) {
+            return;
+        }
+
+        if (owner.getType() == Account.Type.PROJECT) {
+            // only project owner can delete/modify the project
+            if (accessType == AccessType.ModifyProject) {
+                if (!_projectMgr.canModifyProjectAccount(caller, 
owner.getId())) {
+                    s_logger.error(String.format("Caller ID: %d does not have 
permission to modify project with " +
+                            "owner ID: %d", caller.getId(), owner.getId()));
+                    throw exception;
                 }
+            } else if (!_projectMgr.canAccessProjectAccount(caller, 
owner.getId())) {
+                s_logger.error(String.format("Caller ID: %d does not have 
permission to access project with " +
+                        "owner ID: %d", caller.getId(), owner.getId()));
+                throw exception;
             }
+            checkOperationPermitted(caller, entity);
+            return;
         }
-        return true;
+
+        s_logger.error(String.format("Caller ID: %d does not have permission 
to access %s", caller.getId(), entityLog));
+        throw exception;
     }
 
-    private boolean checkOperationPermitted(Account caller, ControlledEntity 
entity) {
+    protected boolean checkOperationPermitted(Account caller, ControlledEntity 
entity) {
         User user = CallContext.current().getCallingUser();
         Project project = 
projectDao.findByProjectAccountId(entity.getAccountId());
         if (project == null) {
diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java 
b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
index 3684657faec..5c7aec5a1b7 100644
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@ -2717,7 +2717,9 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
             throw new InvalidParameterValueException("Unable to find user by 
id");
         }
         final ControlledEntity account = 
getAccount(getUserAccountById(userId).getAccountId()); //Extracting the Account 
from the userID of the requested user.
-        checkAccess(CallContext.current().getCallingUser(), account);
+        User caller = CallContext.current().getCallingUser();
+        preventRootDomainAdminAccessToRootAdminKeys(caller, account);
+        checkAccess(caller, account);
 
         Map<String, String> keys = new HashMap<String, String>();
         keys.put("apikey", user.getApiKey());
@@ -2726,6 +2728,19 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
         return keys;
     }
 
+    protected void preventRootDomainAdminAccessToRootAdminKeys(User caller, 
ControlledEntity account) {
+        if (isDomainAdminForRootDomain(caller) && 
isRootAdmin(account.getAccountId())) {
+            String msg = String.format("Caller Username %s does not have 
access to root admin keys", caller.getUsername());
+            s_logger.error(msg);
+            throw new PermissionDeniedException(msg);
+        }
+    }
+
+    protected boolean isDomainAdminForRootDomain(User callingUser) {
+        AccountVO caller = _accountDao.findById(callingUser.getAccountId());
+        return caller.getType() == Account.Type.DOMAIN_ADMIN && 
caller.getDomainId() == Domain.ROOT_DOMAIN;
+    }
+
     @Override
     public List<UserTwoFactorAuthenticator> 
listUserTwoFactorAuthenticationProviders() {
         return userTwoFactorAuthenticationProviders;
@@ -2760,6 +2775,7 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
         }
 
         Account account = _accountDao.findById(user.getAccountId());
+        preventRootDomainAdminAccessToRootAdminKeys(user, account);
         checkAccess(caller, null, true, account);
 
         // don't allow updating system user
diff --git a/server/src/test/java/com/cloud/acl/DomainCheckerTest.java 
b/server/src/test/java/com/cloud/acl/DomainCheckerTest.java
new file mode 100644
index 00000000000..a5ec41306d8
--- /dev/null
+++ b/server/src/test/java/com/cloud/acl/DomainCheckerTest.java
@@ -0,0 +1,166 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package com.cloud.acl;
+
+import org.apache.cloudstack.acl.ControlledEntity;
+import org.apache.cloudstack.acl.SecurityChecker;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.domain.dao.DomainDao;
+import com.cloud.exception.PermissionDeniedException;
+import com.cloud.projects.ProjectManager;
+import com.cloud.user.Account;
+import com.cloud.user.AccountService;
+import com.cloud.user.AccountVO;
+import com.cloud.user.dao.AccountDao;
+import com.cloud.utils.Ternary;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DomainCheckerTest {
+
+    @Mock
+    AccountService _accountService;
+    @Mock
+    AccountDao _accountDao;
+    @Mock
+    DomainDao _domainDao;
+    @Mock
+    ProjectManager _projectMgr;
+
+    @Spy
+    @InjectMocks
+    DomainChecker domainChecker;
+
+    private ControlledEntity getMockedEntity(long accountId) {
+        ControlledEntity entity = Mockito.mock(Account.class);
+        Mockito.when(entity.getAccountId()).thenReturn(accountId);
+        Mockito.when(entity.getEntityType()).thenReturn((Class)Account.class);
+        return entity;
+    }
+
+    @Test
+    public void testRootAdminHasAccess() {
+        Account rootAdmin = Mockito.mock(Account.class);
+        Mockito.when(rootAdmin.getId()).thenReturn(1L);
+        ControlledEntity entity = getMockedEntity(2L);
+        
Mockito.when(_accountService.isRootAdmin(rootAdmin.getId())).thenReturn(true);
+
+        domainChecker.validateCallerHasAccessToEntityOwner(rootAdmin, entity, 
SecurityChecker.AccessType.ModifyProject);
+    }
+
+    @Test
+    public void testCallerIsOwner() {
+        Account caller = Mockito.mock(Account.class);
+        Mockito.when(caller.getId()).thenReturn(1L);
+        ControlledEntity entity = getMockedEntity(1L);
+
+        domainChecker.validateCallerHasAccessToEntityOwner(caller, entity, 
SecurityChecker.AccessType.ModifyProject);
+    }
+
+    @Test(expected = PermissionDeniedException.class)
+    public void testOwnerNotFound() {
+        Account caller = Mockito.mock(Account.class);
+        Mockito.when(caller.getId()).thenReturn(1L);
+        ControlledEntity entity = getMockedEntity(2L);
+        
Mockito.when(_accountDao.findById(entity.getAccountId())).thenReturn(null);
+
+        domainChecker.validateCallerHasAccessToEntityOwner(caller, entity, 
SecurityChecker.AccessType.ModifyProject);
+    }
+
+    @Test
+    public void testDomainAdminHasAccess() {
+        Account caller = Mockito.mock(Account.class);
+        Mockito.when(caller.getId()).thenReturn(1L);
+        Mockito.when(caller.getDomainId()).thenReturn(100L);
+        Mockito.when(caller.getType()).thenReturn(Account.Type.DOMAIN_ADMIN);
+        ControlledEntity entity = getMockedEntity(2L);
+        AccountVO owner = Mockito.mock(AccountVO.class);
+        Mockito.when(owner.getDomainId()).thenReturn(101L);
+        
Mockito.when(_accountDao.findById(entity.getAccountId())).thenReturn(owner);
+        Mockito.when(_domainDao.isChildDomain(100L, 101L)).thenReturn(true);
+
+        domainChecker.validateCallerHasAccessToEntityOwner(caller, entity, 
SecurityChecker.AccessType.ModifyProject);
+    }
+
+    private Ternary<Account, ControlledEntity, AccountVO> 
getProjectAccessCheckResources() {
+        Account caller = Mockito.mock(Account.class);
+        Mockito.when(caller.getId()).thenReturn(100L);
+        Mockito.when(caller.getType()).thenReturn(Account.Type.PROJECT);
+        ControlledEntity entity = getMockedEntity(2L);
+        AccountVO projectAccount = Mockito.mock(AccountVO.class);
+        Mockito.when(projectAccount.getId()).thenReturn(2L);
+        
Mockito.when(projectAccount.getType()).thenReturn(Account.Type.PROJECT);
+        return new Ternary<>(caller, entity, projectAccount);
+    }
+
+    @Test
+    public void testProjectOwnerCanModify() {
+        Ternary<Account, ControlledEntity, AccountVO> resources = 
getProjectAccessCheckResources();
+        Account caller = resources.first();
+        ControlledEntity entity = resources.second();
+        AccountVO projectAccount = resources.third();
+        
Mockito.when(_accountDao.findById(entity.getAccountId())).thenReturn(projectAccount);
+        Mockito.when(_projectMgr.canModifyProjectAccount(caller, 
projectAccount.getId())).thenReturn(true);
+        
Mockito.doReturn(true).when(domainChecker).checkOperationPermitted(caller, 
entity);
+
+        domainChecker.validateCallerHasAccessToEntityOwner(caller, entity, 
SecurityChecker.AccessType.ModifyProject);
+    }
+
+    @Test(expected = PermissionDeniedException.class)
+    public void testProjectOwnerCannotModify() {
+        Ternary<Account, ControlledEntity, AccountVO> resources = 
getProjectAccessCheckResources();
+        Account caller = resources.first();
+        ControlledEntity entity = resources.second();
+        AccountVO projectAccount = resources.third();
+        
Mockito.when(_accountDao.findById(entity.getAccountId())).thenReturn(projectAccount);
+        Mockito.when(_projectMgr.canModifyProjectAccount(caller, 
projectAccount.getId())).thenReturn(false);
+
+        domainChecker.validateCallerHasAccessToEntityOwner(caller, entity, 
SecurityChecker.AccessType.ModifyProject);
+    }
+
+    @Test
+    public void testProjectOwnerCanAccess() {
+        Ternary<Account, ControlledEntity, AccountVO> resources = 
getProjectAccessCheckResources();
+        Account caller = resources.first();
+        ControlledEntity entity = resources.second();
+        AccountVO projectAccount = resources.third();
+        
Mockito.when(_accountDao.findById(entity.getAccountId())).thenReturn(projectAccount);
+        Mockito.when(_projectMgr.canAccessProjectAccount(caller, 
projectAccount.getId())).thenReturn(true);
+        
Mockito.doReturn(true).when(domainChecker).checkOperationPermitted(caller, 
entity);
+
+        domainChecker.validateCallerHasAccessToEntityOwner(caller, entity, 
SecurityChecker.AccessType.ListEntry);
+    }
+
+    @Test(expected = PermissionDeniedException.class)
+    public void testProjectOwnerCannotAccess() {
+        Ternary<Account, ControlledEntity, AccountVO> resources = 
getProjectAccessCheckResources();
+        Account caller = resources.first();
+        ControlledEntity entity = resources.second();
+        AccountVO projectAccount = resources.third();
+        
Mockito.when(_accountDao.findById(entity.getAccountId())).thenReturn(projectAccount);
+        Mockito.when(_projectMgr.canAccessProjectAccount(caller, 
projectAccount.getId())).thenReturn(false);
+
+        domainChecker.validateCallerHasAccessToEntityOwner(caller, entity, 
SecurityChecker.AccessType.ListEntry);
+    }
+
+}
diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java 
b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
index 2f3a68e20af..262fdeced3e 100644
--- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
+++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.HashMap;
 
+import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.acl.SecurityChecker.AccessType;
 import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
 import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
@@ -240,6 +241,63 @@ public class AccountManagerImplTest extends 
AccountManagetImplTestBase {
         accountManagerImpl.getKeys(_listkeyscmd);
     }
 
+    @Test(expected = PermissionDeniedException.class)
+    public void testGetUserKeysCmdDomainAdminRootAdminUser() {
+        CallContext.register(callingUser, callingAccount);
+        Mockito.when(_listkeyscmd.getID()).thenReturn(2L);
+        
Mockito.when(accountManagerImpl.getActiveUser(2L)).thenReturn(userVoMock);
+        
Mockito.when(userAccountDaoMock.findById(2L)).thenReturn(userAccountVO);
+        Mockito.when(userAccountVO.getAccountId()).thenReturn(2L);
+        
Mockito.when(userDetailsDaoMock.listDetailsKeyPairs(Mockito.anyLong())).thenReturn(null);
+
+        // 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),
+                Mockito.nullable(ControlledEntity.class), 
Mockito.nullable(AccessType.class), Mockito.anyString())).thenReturn(true);
+
+        // Calling account is domain admin of the ROOT domain
+        
Mockito.lenient().when(callingAccount.getType()).thenReturn(Account.Type.DOMAIN_ADMIN);
+        
Mockito.lenient().when(callingAccount.getDomainId()).thenReturn(Domain.ROOT_DOMAIN);
+
+        Mockito.lenient().when(callingUser.getAccountId()).thenReturn(2L);
+        
Mockito.lenient().when(_accountDao.findById(2L)).thenReturn(callingAccount);
+
+        
Mockito.lenient().when(accountService.isDomainAdmin(Mockito.anyLong())).thenReturn(Boolean.TRUE);
+        Mockito.lenient().when(accountMock.getAccountId()).thenReturn(2L);
+
+        accountManagerImpl.getKeys(_listkeyscmd);
+    }
+
+    @Test
+    public void testPreventRootDomainAdminAccessToRootAdminKeysNormalUser() {
+        User user = Mockito.mock(User.class);
+        ControlledEntity entity = Mockito.mock(ControlledEntity.class);
+        Mockito.when(user.getAccountId()).thenReturn(1L);
+        AccountVO account = Mockito.mock(AccountVO.class);
+        Mockito.when(account.getType()).thenReturn(Account.Type.NORMAL);
+        Mockito.when(_accountDao.findById(1L)).thenReturn(account);
+        accountManagerImpl.preventRootDomainAdminAccessToRootAdminKeys(user, 
entity);
+        Mockito.verify(accountManagerImpl, 
Mockito.never()).isRootAdmin(Mockito.anyLong());
+    }
+
+    @Test(expected = PermissionDeniedException.class)
+    public void 
testPreventRootDomainAdminAccessToRootAdminKeysRootDomainAdminUser() {
+        User user = Mockito.mock(User.class);
+        ControlledEntity entity = Mockito.mock(ControlledEntity.class);
+        Mockito.when(user.getAccountId()).thenReturn(1L);
+        AccountVO account = Mockito.mock(AccountVO.class);
+        Mockito.when(account.getType()).thenReturn(Account.Type.DOMAIN_ADMIN);
+        Mockito.when(account.getDomainId()).thenReturn(Domain.ROOT_DOMAIN);
+        Mockito.when(_accountDao.findById(1L)).thenReturn(account);
+        Mockito.when(entity.getAccountId()).thenReturn(1L);
+        
Mockito.lenient().when(securityChecker.checkAccess(Mockito.any(Account.class),
+                Mockito.nullable(ControlledEntity.class), 
Mockito.nullable(AccessType.class), Mockito.anyString())).thenReturn(true);
+        accountManagerImpl.preventRootDomainAdminAccessToRootAdminKeys(user, 
entity);
+    }
+
     @Test
     public void updateUserTestTimeZoneAndEmailNull() {
         prepareMockAndExecuteUpdateUserTest(0);
diff --git a/test/integration/smoke/test_account_access.py 
b/test/integration/smoke/test_account_access.py
new file mode 100644
index 00000000000..97eeced6386
--- /dev/null
+++ b/test/integration/smoke/test_account_access.py
@@ -0,0 +1,198 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+""" BVT tests for Account User Access
+"""
+# Import Local Modules
+from marvin.cloudstackTestCase import cloudstackTestCase
+from marvin.lib.utils import *
+from marvin.lib.base import (Account,
+                             User,
+                             Domain)
+from marvin.lib.common import (get_domain)
+from marvin.cloudstackAPI import (getUserKeys)
+from marvin.cloudstackException import CloudstackAPIException
+from nose.plugins.attrib import attr
+
+_multiprocess_shared_ = True
+
+class TestAccountAccess(cloudstackTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        testClient = super(TestAccountAccess, cls).getClsTestClient()
+        cls.apiclient = testClient.getApiClient()
+        cls.services = testClient.getParsedTestDataConfig()
+        cls.hypervisor = testClient.getHypervisorInfo()
+        cls._cleanup = []
+
+        # Get Zone, Domain and templates
+        cls.domain = get_domain(cls.apiclient)
+
+        cls.domains = []
+        cls.domain_admins = {}
+        cls.domain_users = {}
+        cls.account_users = {}
+
+        domain_data = {
+            "name": "domain_1"
+        }
+        cls.domain_1 = Domain.create(
+            cls.apiclient,
+            domain_data,
+        )
+        cls._cleanup.append(cls.domain_1)
+        cls.domains.append(cls.domain_1)
+        domain_data["name"] = "domain_11"
+        cls.domain_11 = Domain.create(
+            cls.apiclient,
+            domain_data,
+            parentdomainid=cls.domain_1.id
+        )
+        cls._cleanup.append(cls.domain_11)
+        cls.domains.append(cls.domain_11)
+        domain_data["name"] = "domain_12"
+        cls.domain_12 = Domain.create(
+            cls.apiclient,
+            domain_data,
+            parentdomainid=cls.domain_1.id
+        )
+        cls._cleanup.append(cls.domain_12)
+        cls.domains.append(cls.domain_12)
+        domain_data["name"] = "domain_2"
+        cls.domain_2 = Domain.create(
+            cls.apiclient,
+            domain_data,
+        )
+        cls._cleanup.append(cls.domain_2)
+        cls.domains.append(cls.domain_2)
+
+
+        for d in cls.domains:
+            cls.create_domainadmin_and_user(d)
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestAccountAccess, cls).tearDownClass()
+
+    @classmethod
+    def create_account(cls, domain, is_admin):
+        cls.debug(f"Creating account for domain {domain.name}, admin: 
{is_admin}")
+        data = {
+            "email": "admin-" + domain.name + "@test.com",
+            "firstname": "Admin",
+            "lastname": domain.name,
+            "username": "admin-" + domain.name,
+            "password": "password"
+        }
+        if is_admin == False:
+            data["email"] = "user-" + domain.name + "@test.com"
+            data["firstname"] = "User"
+            data["username"] = "user-" + domain.name
+        account = Account.create(
+            cls.apiclient,
+            data,
+            admin=is_admin,
+            domainid=domain.id
+        )
+        cls._cleanup.append(account)
+        if is_admin == True:
+            cls.domain_admins[domain.id] = account
+        else:
+            cls.domain_users[domain.id] = account
+
+        user = User.create(
+            cls.apiclient,
+            data,
+            account=account.name,
+            domainid=account.domainid)
+        cls._cleanup.append(user)
+        cls.account_users[account.id] = user
+
+    @classmethod
+    def create_domainadmin_and_user(cls, domain):
+        cls.debug(f"Creating accounts for domain #{domain.id} {domain.name}")
+        cls.create_account(domain, True)
+        cls.create_account(domain, False)
+
+    def get_user_keys(self, api_client, user_id):
+        getUserKeysCmd = getUserKeys.getUserKeysCmd()
+        getUserKeysCmd.id = user_id
+        return api_client.getUserKeys(getUserKeysCmd)
+
+    def is_child_domain(self, parent_domain, child_domain):
+        if not parent_domain or not child_domain:
+            return False
+        parent_domain_prefix = parent_domain.split('-')[0]
+        child_domain_prefix = child_domain.split('-')[0]
+        if not parent_domain_prefix or not child_domain_prefix:
+            return False
+        return child_domain_prefix.startswith(parent_domain_prefix)
+
+
+    @attr(tags=["advanced", "advancedns", "smoke", "sg"], 
required_hardware="false")
+    def test_01_user_access(self):
+        """
+        Test user account is not accessing any other account
+        """
+
+        domain_user_accounts = [value for value in self.domain_users.values()]
+        all_account_users = [value for value in self.account_users.values()]
+        for user_account in domain_user_accounts:
+            current_account_user = self.account_users[user_account.id]
+            self.debug(f"Check for account {user_account.name} with user 
{current_account_user.username}")
+            user_api_client = self.testClient.getUserApiClient(
+                UserName=user_account.name,
+                DomainName=user_account.domain
+            )
+            for user in all_account_users:
+                self.debug(f"Checking access for user {user.username} 
associated with account {user.account}")
+                try:
+                    self.get_user_keys(user_api_client, user.id)
+                    self.debug(f"API successful")
+                    if user.id != current_account_user.id:
+                        self.fail(f"User account #{user_account.id} was able 
to access another account #{user.id}")
+                except CloudstackAPIException as e:
+                    self.debug(f"Exception occurred: {e}")
+                    if user.id == current_account_user.id:
+                        self.fail(f"User account #{user_account.id} not able 
to access own account")
+
+    @attr(tags=["advanced", "advancedns", "smoke", "sg"], 
required_hardware="false")
+    def test_02_domain_admin_access(self):
+        """
+        Test domain admin account is not accessing any other account from 
unauthorized domain
+        """
+
+        domain_admin_accounts = [value for value in 
self.domain_admins.values()]
+        all_account_users = [value for value in self.account_users.values()]
+        for admin_account in domain_admin_accounts:
+            current_account_user = self.account_users[admin_account.id]
+            self.debug(f"Check for domain admin {admin_account.name} with user 
{current_account_user.username}, {current_account_user.domain}")
+            admin_api_client = self.testClient.getUserApiClient(
+                UserName=admin_account.name,
+                DomainName=admin_account.domain
+            )
+            for user in all_account_users:
+                self.debug(f"Checking access for user {user.username}, 
{user.domain} associated with account {user.account}")
+                try:
+                    self.get_user_keys(admin_api_client, user.id)
+                    self.debug(f"API successful")
+                    if self.is_child_domain(current_account_user.domain, 
user.domain) == False:
+                        self.fail(f"User account #{admin_account.id} was able 
to access another account #{user.id}")
+                except CloudstackAPIException as e:
+                    self.debug(f"Exception occurred: {e}")
+                    if self.is_child_domain(current_account_user.domain, 
user.domain) == True:
+                        self.fail(f"User account #{admin_account.id} not able 
to access own account")

Reply via email to