From 2f07feca800737d126521e191d5c4158e97e16f9 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 28 Oct 2025 12:48:14 +0200 Subject: [PATCH] added validation that prohibits last tenant admin deletion --- .../server/controller/UserController.java | 3 +++ .../server/controller/UserControllerTest.java | 20 +++++++++++++++++++ .../server/dao/user/UserService.java | 1 + .../server/dao/sql/user/JpaUserDao.java | 5 +++++ .../server/dao/sql/user/UserRepository.java | 2 ++ .../thingsboard/server/dao/user/UserDao.java | 1 + .../server/dao/user/UserServiceImpl.java | 5 +++++ 7 files changed, 37 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index b9cb0c88b3..a2a7c993e9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -266,6 +266,9 @@ public class UserController extends BaseController { if (user.getAuthority() == Authority.SYS_ADMIN && getCurrentUser().getId().equals(userId)) { throw new ThingsboardException("Sysadmin is not allowed to delete himself", ThingsboardErrorCode.PERMISSION_DENIED); } + if (user.getAuthority() == Authority.TENANT_ADMIN && userService.countTenantAdmins(user.getTenantId()) == 1) { + throw new ThingsboardException("At least one tenant administrator must remain!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } tbUserService.delete(getTenantId(), getCurrentUser().getCustomerId(), user, getCurrentUser()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index d7aa6f5770..a08440ca4f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -284,6 +284,26 @@ public class UserControllerTest extends AbstractControllerTest { ActionType.ADDED, new DataValidationException(msgError)); } + @Test + public void testShouldNotDeleteLastTenantAdmin() throws Exception { + loginSysAdmin(); + + User tenantAdmin2 = new User(); + tenantAdmin2.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin2.setTenantId(tenantId); + tenantAdmin2.setEmail("tenant2@thingsboard.io"); + tenantAdmin2 = doPost("/api/user", tenantAdmin2, User.class); + + // delete second tenant admin - ok + doDelete("/api/user/" + tenantAdmin2.getId().getId().toString()) + .andExpect(status().isOk()); + + // delete last tenant admin - forbidden + doDelete("/api/user/" + tenantAdminUser.getId().getId().toString()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("At least one tenant administrator must remain!"))); + } + @Test public void testSaveUserWithInvalidEmail() throws Exception { loginSysAdmin(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index c016631064..160e02fbc8 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -109,4 +109,5 @@ public interface UserService extends EntityDaoService { void removeMobileSession(TenantId tenantId, String mobileToken); + int countTenantAdmins(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 35d15bab51..753955089c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -136,6 +136,11 @@ public class JpaUserDao extends JpaAbstractDao implements User DaoUtil.toPageable(pageLink))); } + @Override + public int countTenantAdmins(UUID tenantId) { + return userRepository.countByTenantIdAndAuthority(tenantId, Authority.TENANT_ADMIN); + } + @Override public Long countByTenantId(TenantId tenantId) { return userRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 0a30a859c6..2254377af3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -78,4 +78,6 @@ public interface UserRepository extends JpaRepository { "u.customerId, u.version, u.firstName, u.lastName, u.email, u.phone, u.additionalInfo) " + "FROM UserEntity u WHERE u.id > :id ORDER BY u.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + int countByTenantIdAndAuthority(UUID tenantId, Authority authority); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index b60b263ac8..127aa6141a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -101,4 +101,5 @@ public interface UserDao extends Dao, TenantEntityDao { PageData findByAuthorityAndTenantProfilesIds(Authority authority, List tenantProfilesIds, PageLink pageLink); + int countTenantAdmins(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 5c94ba1891..71a908189f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -485,6 +485,11 @@ public class UserServiceImpl extends AbstractCachedEntityService findMobileSessionInfo(TenantId tenantId, UserId userId) { return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE)) .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class));