From d57e1f8f9764a9d42a524fcd81e752187f5f2dd4 Mon Sep 17 00:00:00 2001 From: Surya Srinivasan Date: Sun, 8 Feb 2026 02:44:39 +0530 Subject: [PATCH 1/6] Fix account deletion blocked by deleted project admin mappings --- .../com/cloud/user/AccountManagerImpl.java | 29 ++++++++++++++----- .../cloud/user/AccountManagerImplTest.java | 21 ++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index d4c23e8d62be..b2fcc658e017 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -2110,16 +2110,31 @@ public boolean deleteUserAccount(long accountId) { return deleteAccount(account, callerUserId, caller); } - protected void checkIfAccountManagesProjects(long accountId) { - List managedProjectIds = _projectAccountDao.listAdministratedProjectIds(accountId); - if (!CollectionUtils.isEmpty(managedProjectIds)) { - throw new InvalidParameterValueException(String.format( - "Unable to delete account [%s], because it manages the following project(s): %s. Please, remove the account from these projects or demote it to a regular project role first.", - accountId, managedProjectIds - )); +protected void checkIfAccountManagesProjects(long accountId) { + List managedProjectIds = _projectAccountDao.listAdministratedProjectIds(accountId); + + if (CollectionUtils.isEmpty(managedProjectIds)) { + return; + } + + List activeManagedProjects = new ArrayList<>(); + + for (Long projectId : managedProjectIds) { + ProjectVO project = _projectDao.findById(projectId); + if (project != null && project.getRemoved() == null) { + activeManagedProjects.add(projectId); } } + if (!activeManagedProjects.isEmpty()) { + throw new InvalidParameterValueException(String.format( + "Unable to delete account [%s], because it manages the following project(s): %s. Please, remove the account from these projects or demote it to a regular project role first.", + accountId, activeManagedProjects + )); + } +} + + protected boolean isDeleteNeeded(AccountVO account, long accountId, Account caller) { if (account == null) { logger.info(String.format("The account, identified by id %d, doesn't exist", accountId )); diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 2aeb43469d19..b53eff2b70ac 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -26,6 +26,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Date; + import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.Role; @@ -75,6 +77,7 @@ import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.projects.ProjectVO; @RunWith(MockitoJUnitRunner.class) public class AccountManagerImplTest extends AccountManagetImplTestBase { @@ -1589,4 +1592,22 @@ public void testcheckCallerApiPermissionsForUserOperationsNotAllowedApis() { accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock); } + + @Test + public void testCheckIfAccountManagesOnlyDeletedProjectsDoesNotThrow() { + long accountId = 42L; + long projectId = 100L; + + Mockito.when(projectAccountDao.listAdministratedProjectIds(accountId)) + .thenReturn(List.of(projectId)); + + ProjectVO deletedProject = Mockito.mock(ProjectVO.class); + Mockito.when(deletedProject.getRemoved()).thenReturn(new Date()); + + Mockito.when(projectDao.findById(projectId)) + .thenReturn(deletedProject); + + accountManager.checkIfAccountManagesProjects(accountId); + } + } From 29a09c22f102eb90973db080c9cb28973d449812 Mon Sep 17 00:00:00 2001 From: Surya Srinivasan Date: Mon, 9 Feb 2026 18:47:17 +0530 Subject: [PATCH 2/6] Fix indentation per review --- server/src/main/java/com/cloud/user/AccountManagerImpl.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index b2fcc658e017..be4dcf5b5f05 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -2127,10 +2127,12 @@ protected void checkIfAccountManagesProjects(long accountId) { } if (!activeManagedProjects.isEmpty()) { - throw new InvalidParameterValueException(String.format( + throw new InvalidParameterValueException( + String.format( "Unable to delete account [%s], because it manages the following project(s): %s. Please, remove the account from these projects or demote it to a regular project role first.", accountId, activeManagedProjects - )); + ) + ); } } From c645292b5db2a2c33e584fa3cb93db11d730ab04 Mon Sep 17 00:00:00 2001 From: Surya Srinivasan Date: Mon, 9 Feb 2026 21:26:40 +0530 Subject: [PATCH 3/6] Update server/src/main/java/com/cloud/user/AccountManagerImpl.java Co-authored-by: dahn --- .../com/cloud/user/AccountManagerImpl.java | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index be4dcf5b5f05..2e0884cbce24 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -2110,31 +2110,30 @@ public boolean deleteUserAccount(long accountId) { return deleteAccount(account, callerUserId, caller); } -protected void checkIfAccountManagesProjects(long accountId) { - List managedProjectIds = _projectAccountDao.listAdministratedProjectIds(accountId); + protected void checkIfAccountManagesProjects(long accountId) { + List managedProjectIds = _projectAccountDao.listAdministratedProjectIds(accountId); - if (CollectionUtils.isEmpty(managedProjectIds)) { - return; - } + if (CollectionUtils.isEmpty(managedProjectIds)) { + return; + } - List activeManagedProjects = new ArrayList<>(); + List activeManagedProjects = new ArrayList<>(); - for (Long projectId : managedProjectIds) { - ProjectVO project = _projectDao.findById(projectId); - if (project != null && project.getRemoved() == null) { - activeManagedProjects.add(projectId); + for (Long projectId : managedProjectIds) { + ProjectVO project = _projectDao.findById(projectId); + if (project != null && project.getRemoved() == null) { + activeManagedProjects.add(projectId); + } } - } - if (!activeManagedProjects.isEmpty()) { - throw new InvalidParameterValueException( - String.format( - "Unable to delete account [%s], because it manages the following project(s): %s. Please, remove the account from these projects or demote it to a regular project role first.", - accountId, activeManagedProjects - ) - ); + if (!activeManagedProjects.isEmpty()) { + throw new InvalidParameterValueException( + String.format( + "Unable to delete account [%s], because it manages the following project(s): %s. Please, remove the account from these projects or demote it to a regular project role first.", + accountId, activeManagedProjects + )); + } } -} protected boolean isDeleteNeeded(AccountVO account, long accountId, Account caller) { From 235efbe501a8f7ae072139b3fe88e6f79095f30d Mon Sep 17 00:00:00 2001 From: Surya Srinivasan Date: Tue, 10 Feb 2026 18:14:49 +0530 Subject: [PATCH 4/6] Fix test mocks to use injected _projectAccountDao and _projectDao --- .../com/cloud/user/AccountManagerImplTest.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index b53eff2b70ac..2a38011fe856 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -82,6 +82,9 @@ @RunWith(MockitoJUnitRunner.class) public class AccountManagerImplTest extends AccountManagetImplTestBase { + @InjectMocks + private AccountManagerImpl accountManagerImpl; + @Mock private UserVmManagerImpl _vmMgr; @Mock @@ -120,9 +123,16 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock private ProjectAccountVO projectAccountVO; + @Mock private Project project; + @Mock + private ProjectAccountDao _projectAccountDao; + + @Mock + private ProjectDao _projectDao; + @Mock PasswordPolicyImpl passwordPolicyMock; @@ -1598,16 +1608,17 @@ public void testCheckIfAccountManagesOnlyDeletedProjectsDoesNotThrow() { long accountId = 42L; long projectId = 100L; - Mockito.when(projectAccountDao.listAdministratedProjectIds(accountId)) + Mockito.when(_projectAccountDao.listAdministratedProjectIds(accountId)) .thenReturn(List.of(projectId)); ProjectVO deletedProject = Mockito.mock(ProjectVO.class); Mockito.when(deletedProject.getRemoved()).thenReturn(new Date()); - Mockito.when(projectDao.findById(projectId)) + Mockito.when(_projectDao.findById(projectId)) .thenReturn(deletedProject); - accountManager.checkIfAccountManagesProjects(accountId); + accountManagerImpl.checkIfAccountManagesProjects(accountId); + } } From 77cb72fbcb097473af3d618478a75d26c7bc7b16 Mon Sep 17 00:00:00 2001 From: Surya Srinivasan Date: Tue, 10 Feb 2026 19:18:35 +0530 Subject: [PATCH 5/6] Fix missing imports for ProjectAccountDao, ProjectDao and InjectMocks --- .../src/test/java/com/cloud/user/AccountManagerImplTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 2a38011fe856..b95f2a182177 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -56,6 +56,7 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.InjectMocks; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import com.cloud.acl.DomainChecker; @@ -78,6 +79,8 @@ import com.cloud.vm.VMInstanceVO; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.projects.ProjectVO; +import com.cloud.projects.dao.ProjectAccountDao; +import com.cloud.projects.dao.ProjectDao; @RunWith(MockitoJUnitRunner.class) public class AccountManagerImplTest extends AccountManagetImplTestBase { From 58ffd9db4d5ae40330a45fbedbc398126b5acf57 Mon Sep 17 00:00:00 2001 From: Surya Srinivasan Date: Wed, 11 Feb 2026 00:16:54 +0530 Subject: [PATCH 6/6] Fix AccountManagerImplTest by spying AccountManagerImpl to allow stubbing internal methods --- .../src/test/java/com/cloud/user/AccountManagerImplTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index b95f2a182177..a2684bfe8d60 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -57,8 +57,10 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.InjectMocks; +import org.mockito.Spy; import org.springframework.beans.factory.NoSuchBeanDefinitionException; + import com.cloud.acl.DomainChecker; import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; import com.cloud.domain.Domain; @@ -85,6 +87,7 @@ @RunWith(MockitoJUnitRunner.class) public class AccountManagerImplTest extends AccountManagetImplTestBase { + @Spy @InjectMocks private AccountManagerImpl accountManagerImpl;