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 fadb920938..376a939c0a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -42,8 +42,10 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.UserEmailInfo; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; @@ -63,6 +65,7 @@ import org.thingsboard.server.common.data.settings.UserDashboardsInfo; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; @@ -75,11 +78,14 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_ID_PARAM_DESCRIPTION; @@ -125,6 +131,9 @@ public class UserController extends BaseController { @Autowired private EntityQueryService entityQueryService; + @Autowired + private EntityService entityService; + @ApiOperation(value = "Get User (getUserById)", notes = "Fetch the User object based on the provided User Id. " + "If the user has the authority of 'SYS_ADMIN', the server does not perform additional checks. " + @@ -444,6 +453,54 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Get usersForAssign (getUsersForAssign)", + notes = "Returns page of user data objects that can be assigned to provided alarmId. " + + "Search is been executed by email, firstName and lastName fields. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getUsersForAssign( + @ApiParam(value = ALARM_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("alarmId") String strAlarmId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = USER_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = USER_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + checkParameter("alarmId", strAlarmId); + AlarmId alarmEntityId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmEntityId, Operation.READ); + SecurityUser currentUser = getCurrentUser(); + TenantId tenantId = currentUser.getTenantId(); + CustomerId originatorCustomerId = entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator()).get(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData pageData; + if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { + if (alarm.getCustomerId() == null) { + pageData = userService.findTenantAdmins(tenantId, pageLink); + } else { + ArrayList customerIds = new ArrayList<>(Collections.singletonList(new CustomerId(CustomerId.NULL_UUID))); + if (!CustomerId.NULL_UUID.equals(originatorCustomerId.getId())) { + customerIds.add(originatorCustomerId); + } + pageData = userService.findUsersByCustomerIds(tenantId, customerIds, pageLink); + } + } else { + pageData = userService.findCustomerUsers(tenantId, alarm.getCustomerId(), pageLink); + } + return pageData.mapData(user -> new UserEmailInfo(user.getId(), user.getEmail(), user.getFirstName(), user.getLastName())); + } catch (Exception e) { + throw handleException(e); + } + } + @ApiOperation(value = "Save user settings (saveUserSettings)", notes = "Save user settings represented in json format for authorized user. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index 838cd7270b..1a8f87713e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -119,6 +119,15 @@ public class CustomerUserPermissions extends AbstractPermissions { if (!Authority.CUSTOMER_USER.equals(userEntity.getAuthority())) { return false; } + + if (!user.getCustomerId().equals(userEntity.getCustomerId())) { + return false; + } + + if (Operation.READ.equals(operation)) { + return true; + } + return user.getId().equals(userId); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 2cc438eb4d..8f58f01225 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -810,6 +810,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } + public class EntityIdComparator implements Comparator { + + @Override + public int compare(D o1, D o2) { + return o1.getId().compareTo(o2.getId()); + } + + } + protected static ResultMatcher statusReason(Matcher matcher) { return jsonPath("$.message", matcher); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index 680cf19822..2d93e16f4f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -32,11 +32,14 @@ import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.UserEmailInfo; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -57,7 +60,6 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -70,6 +72,8 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { private IdComparator idComparator = new IdComparator<>(); private IdComparator userDataIdComparator = new IdComparator<>(); + private EntityIdComparator userIdComparator = new EntityIdComparator<>(); + private CustomerId customerNUULId = (CustomerId) createEntityId_NULL_UUID(new Customer()); @Autowired @@ -651,6 +655,89 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testGetUsersForAssign() throws Exception { + loginTenantAdmin(); + + String email = "testEmail1"; + List expectedCustomerUserIds = new ArrayList<>(); + expectedCustomerUserIds.add(customerUserId); + for (int i = 0; i < 45; i++) { + User customerUser = createCustomerUser( customerId); + customerUser.setEmail(email + StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)) + "@thingsboard.org"); + User user = doPost("/api/user", customerUser, User.class); + expectedCustomerUserIds.add(user.getId()); + } + List expectedTenantUserIds = new ArrayList<>(List.copyOf(expectedCustomerUserIds)); + expectedTenantUserIds.add(tenantAdminUserId); + + Device device = new Device(); + device.setName("testDevice"); + Device savedDevice = doPost("/api/device", device, Device.class); + + Alarm alarm = createTestAlarm(savedDevice); + + List loadedTenantUserIds = new ArrayList<>(); + PageLink pageLink = new PageLink(33, 0); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/users/assign/" + alarm.getId().getId().toString() + "?", + new TypeReference<>() {}, pageLink); + loadedTenantUserIds.addAll(pageData.getData().stream().map(UserEmailInfo::getId) + .collect(Collectors.toList())); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertEquals(1, loadedTenantUserIds.size()); + Assert.assertEquals(tenantAdminUserId, loadedTenantUserIds.get(0)); + + doDelete("/api/alarm/" + alarm.getId().getId().toString()); + + savedDevice.setCustomerId(customerId); + savedDevice = doPost("/api/customer/" + customerId.getId() + + "/device/" + savedDevice.getId().getId(), Device.class); + + alarm = createTestAlarm(savedDevice); + + List loadedUserIds = new ArrayList<>(); + pageLink = new PageLink(16, 0); + do { + pageData = doGetTypedWithPageLink("/api/users/assign/" + alarm.getId().getId().toString() + "?", + new TypeReference<>() {}, pageLink); + loadedUserIds.addAll(pageData.getData().stream().map(UserEmailInfo::getId) + .collect(Collectors.toList())); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + expectedTenantUserIds.sort(userIdComparator); + loadedUserIds.sort(userIdComparator); + + Assert.assertEquals(expectedTenantUserIds, loadedUserIds); + + loginCustomerUser(); + + loadedUserIds = new ArrayList<>(); + pageLink = new PageLink(16, 0); + do { + pageData = doGetTypedWithPageLink("/api/users/assign/" + alarm.getId().getId().toString() + "?", + new TypeReference<>() {}, pageLink); + loadedUserIds.addAll(pageData.getData().stream().map(UserEmailInfo::getId) + .collect(Collectors.toList())); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + expectedCustomerUserIds.sort(userIdComparator); + loadedUserIds.sort(userIdComparator); + + Assert.assertEquals(expectedCustomerUserIds, loadedUserIds); + } + @Test public void testDeleteUserWithDeleteRelationsOk() throws Exception { loginSysAdmin(); @@ -991,6 +1078,16 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { return loadedCustomerUsers; } + private Alarm createTestAlarm(Device device) { + Alarm alarm = new Alarm(); + alarm.setOriginator(device.getId()); + alarm.setCustomerId(device.getCustomerId()); + alarm.setSeverity(AlarmSeverity.MAJOR); + alarm.setType("testAlarm"); + alarm.setStartTs(System.currentTimeMillis()); + return doPost("/api/alarm", alarm, Alarm.class); + } + @Test public void testEmptyDashboardSettings() throws Exception { loginCustomerUser(); 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 e299f4ecb5..e1f16be1ed 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 @@ -77,6 +77,8 @@ public interface UserService extends EntityDaoService { PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink); + PageData findUsersByCustomerIds(TenantId tenantId, List customerIds, PageLink pageLink); + void deleteCustomerUsers(TenantId tenantId, CustomerId customerId); void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled); 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 4a41fc17a4..04b919b484 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 @@ -20,6 +20,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; @@ -102,6 +103,17 @@ public class JpaUserDao extends JpaAbstractSearchTextDao imple } + @Override + public PageData findUsersByCustomerIds(UUID tenantId, List customerIds, PageLink pageLink) { + return DaoUtil.toPageData( + userRepository + .findTenantAndCustomerUsers( + tenantId, + DaoUtil.toUUIDs(customerIds), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + @Override public PageData findAll(PageLink pageLink) { return DaoUtil.toPageData(userRepository.findAll(DaoUtil.toPageable(pageLink))); 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 e00c73763b..2f6866d45b 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 @@ -44,6 +44,14 @@ public interface UserRepository extends JpaRepository { @Param("authority") Authority authority, Pageable pageable); + @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " + + "AND u.customerId IN (:customerIds) " + + "AND LOWER(u.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + Page findTenantAndCustomerUsers(@Param("tenantId") UUID tenantId, + @Param("customerIds") Collection customerIds, + @Param("searchText") String searchText, + Pageable pageable); + @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " + "AND LOWER(u.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, 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 54d847a26e..268e138b2f 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.user; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; @@ -82,6 +83,16 @@ public interface UserDao extends Dao, TenantEntityDao { */ PageData findCustomerUsers(UUID tenantId, UUID customerId, PageLink pageLink); + /** + * Find users for alarm assignment by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of user entities + */ + PageData findUsersByCustomerIds(UUID tenantId, List customerIds, PageLink pageLink); + PageData findAll(PageLink pageLink); PageData findAllByAuthority(Authority authority, PageLink pageLink); 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 e3d30c8b01..5dc32b8eed 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 @@ -296,6 +296,15 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic return userDao.findCustomerUsers(tenantId.getId(), customerId.getId(), pageLink); } + @Override + public PageData findUsersByCustomerIds(TenantId tenantId, List customerIds, PageLink pageLink) { + log.trace("Executing findTenantAndCustomerUsers, tenantId [{}], customerIds [{}], pageLink [{}]", tenantId, customerIds, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + customerIds.forEach(customerId -> {validateId(customerId, "Incorrect customerId " + customerId);}); + return userDao.findUsersByCustomerIds(tenantId.getId(), customerIds, pageLink); + } + @Override public void deleteCustomerUsers(TenantId tenantId, CustomerId customerId) { log.trace("Executing deleteCustomerUsers, customerId [{}]", customerId); diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 87dd868138..214511452a 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -65,6 +65,7 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.UsageInfo; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserEmailInfo; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; @@ -2628,6 +2629,19 @@ public class RestClient implements Closeable { }, params).getBody(); } + public PageData getUsersForAssign(AlarmId alarmId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("alarmId", alarmId.getId().toString()); + addPageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + "/users/assign/{alarmId}" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public void setUserCredentialsEnabled(UserId userId, boolean userCredentialsEnabled) { restTemplate.postForLocation( baseURL + "/api/user/{userId}/userCredentialsEnabled?userCredentialsEnabled={userCredentialsEnabled}", diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts index 9a6b70ce29..39a5a20e6e 100644 --- a/ui-ngx/src/app/core/http/user.service.ts +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -51,6 +51,12 @@ export class UserService { defaultHttpOptionsFromConfig(config)); } + public getUsersForAssign(alarmId: string, pageLink: PageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/users/assign/${alarmId}${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + public getUser(userId: string, config?: RequestConfig): Observable { return this.http.get(`/api/user/${userId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts index bca84bc8a3..ce448d0321 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts @@ -152,7 +152,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe property: 'email', direction: Direction.ASC }); - return this.userService.findUsersByQuery(pageLink, {ignoreLoading: true}) + return this.userService.getUsersForAssign(this.alarmId, pageLink, {ignoreLoading: true}) .pipe( catchError(() => of(emptyPageData())), map(pageData => {