From a7516bbd5520b9c7e638db51b1420b99440b5500 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 30 Jul 2024 11:25:08 +0530 Subject: [PATCH 1/3] test: improve purge expunged resources b/g task testcase (#467) * test: improve purge expunged resources b/g task testcase Failures were seen for during purging of expunged resources via bacground task during different test runs. This PR tries to make sure b/g task execution is not skipped after MS restrat in a multi-MS environment. It also updates expunged.resources.purge.interval to allow running task again in 60s. Signed-off-by: Abhishek Kumar * new string formatting --------- Signed-off-by: Abhishek Kumar --- test/integration/smoke/test_purge_expunged_vms.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/integration/smoke/test_purge_expunged_vms.py b/test/integration/smoke/test_purge_expunged_vms.py index 4d885dc68e1..0fe55991059 100644 --- a/test/integration/smoke/test_purge_expunged_vms.py +++ b/test/integration/smoke/test_purge_expunged_vms.py @@ -273,6 +273,7 @@ class TestPurgeExpungedVms(cloudstackTestCase): return False self.debug("Restarting all management server") for idx, server_ip in enumerate(server_ips): + self.debug(f"Restarting management server #{idx} with IP {server_ip}") sshClient = SshClient( server_ip, 22, @@ -283,6 +284,9 @@ class TestPurgeExpungedVms(cloudstackTestCase): sshClient.execute(command) command = "service cloudstack-management start" sshClient.execute(command) + if idx == 0: + # Wait before restarting other management servers to make the first as oldest running + time.sleep(10) # Waits for management to come up in 10 mins, when it's up it will continue timeout = time.time() + (10 * 60) @@ -349,15 +353,18 @@ class TestPurgeExpungedVms(cloudstackTestCase): @skipTestIf("hypervisorIsSimulator") @attr(tags=["advanced"], required_hardware="true") def test_06_purge_expunged_vm_background_task(self): - purge_task_delay = 60 + purge_task_delay = 120 self.changeConfiguration('expunged.resources.purge.enabled', 'true') self.changeConfiguration('expunged.resources.purge.delay', purge_task_delay) + self.changeConfiguration('expunged.resources.purge.interval', int(purge_task_delay/2)) self.changeConfiguration('expunged.resources.purge.keep.past.days', 1) if len(self.staticConfigurations) > 0: self.restartAllManagementServers() - wait = 2 * purge_task_delay - logging.info("Waiting for 2x%d = %d seconds for background task to execute" % (purge_task_delay, wait)) + wait_multiple = 2 + wait = wait_multiple * purge_task_delay + logging.info(f"Waiting for {wait_multiple}x{purge_task_delay} = {wait} seconds for background task to execute") time.sleep(wait) + logging.debug("Validating expunged VMs") self.validatePurgedVmEntriesInDb( [self.vm_ids[self.timestamps[0]], self.vm_ids[self.timestamps[1]], self.vm_ids[self.timestamps[2]]], None From a794462da195b6ed38a285eae136efa6c8e2ccbc Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Wed, 7 Aug 2024 21:18:24 +0530 Subject: [PATCH 2/3] server, api: account and api entity access improvements (#470) Backports CVE fix from upstream 4.18.2.3 https://cloudstack.apache.org/blog/security-release-advisory-4.19.1.1-4.18.2.3 (cherry picked from commit e7dce2bcce5ba52b7d36551e8b2f0c84e75d7570) Signed-off-by: Rohit Yadav Co-authored-by: Abhishek Kumar Co-authored-by: Fabricio Duarte Co-authored-by: nvazquez --- .github/workflows/ci.yml | 1 + .../java/com/cloud/acl/DomainChecker.java | 70 +++++-- .../com/cloud/user/AccountManagerImpl.java | 18 +- .../java/com/cloud/acl/DomainCheckerTest.java | 166 +++++++++++++++ .../cloud/user/AccountManagerImplTest.java | 58 +++++ test/integration/smoke/test_account_access.py | 198 ++++++++++++++++++ 6 files changed, 489 insertions(+), 22 deletions(-) create mode 100644 server/src/test/java/com/cloud/acl/DomainCheckerTest.java create mode 100644 test/integration/smoke/test_account_access.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9279776a549..4da36b0b747 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; } - private boolean checkOperationPermitted(Account caller, ControlledEntity entity) { + 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; + } + + s_logger.error(String.format("Caller ID: %d does not have permission to access %s", caller.getId(), entityLog)); + throw exception; + } + + 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 keys = new HashMap(); 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 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 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 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 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 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 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") From 8890e710528c9bc16fe9a77002978303326a25e4 Mon Sep 17 00:00:00 2001 From: mprokopchuk Date: Tue, 13 Aug 2024 12:09:41 -0700 Subject: [PATCH 3/3] Provide encryption key for DATA volume type (in addition to ROOT) to copy volume. --- .../com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 50c6c0c9b0c..ff55235d245 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -2447,7 +2447,8 @@ public class KVMStorageProcessor implements StorageProcessor { destPool = storagePoolMgr.getStoragePool(destPrimaryStore.getPoolType(), destPrimaryStore.getUuid()); try { - if (srcVol.getPassphrase() != null && srcVol.getVolumeType().equals(Volume.Type.ROOT)) { + Volume.Type volumeType = srcVol.getVolumeType(); + if (srcVol.getPassphrase() != null && (Volume.Type.ROOT.equals(volumeType) || Volume.Type.DATADISK.equals(volumeType))) { volume.setQemuEncryptFormat(QemuObject.EncryptFormat.LUKS); storagePoolMgr.copyPhysicalDisk(volume, destVolumeName, destPool, cmd.getWaitInMillSeconds(), srcVol.getPassphrase(), destVol.getPassphrase(), srcVol.getProvisioningType()); } else {