Merge pull request #8256 from imbeacon/fix/alarm-assignment-user-issues

Added getting user list for alarm assignment and ability to assign alarms for customer users
This commit is contained in:
Andrew Shvayka 2023-04-07 15:00:22 +03:00 committed by GitHub
commit 8769911694
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 236 additions and 2 deletions

View File

@ -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<UserEmailInfo> 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<User> pageData;
if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) {
if (alarm.getCustomerId() == null) {
pageData = userService.findTenantAdmins(tenantId, pageLink);
} else {
ArrayList<CustomerId> 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')")

View File

@ -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);
}

View File

@ -810,6 +810,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
public class EntityIdComparator<D extends EntityId> implements Comparator<D> {
@Override
public int compare(D o1, D o2) {
return o1.getId().compareTo(o2.getId());
}
}
protected static <T> ResultMatcher statusReason(Matcher<T> matcher) {
return jsonPath("$.message", matcher);
}

View File

@ -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<User> idComparator = new IdComparator<>();
private IdComparator<UserEmailInfo> userDataIdComparator = new IdComparator<>();
private EntityIdComparator<UserId> 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<UserId> 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<UserId> 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<UserId> loadedTenantUserIds = new ArrayList<>();
PageLink pageLink = new PageLink(33, 0);
PageData<UserEmailInfo> 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<UserId> 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();

View File

@ -77,6 +77,8 @@ public interface UserService extends EntityDaoService {
PageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink);
PageData<User> findUsersByCustomerIds(TenantId tenantId, List<CustomerId> customerIds, PageLink pageLink);
void deleteCustomerUsers(TenantId tenantId, CustomerId customerId);
void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled);

View File

@ -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<UserEntity, User> imple
}
@Override
public PageData<User> findUsersByCustomerIds(UUID tenantId, List<CustomerId> customerIds, PageLink pageLink) {
return DaoUtil.toPageData(
userRepository
.findTenantAndCustomerUsers(
tenantId,
DaoUtil.toUUIDs(customerIds),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<User> findAll(PageLink pageLink) {
return DaoUtil.toPageData(userRepository.findAll(DaoUtil.toPageable(pageLink)));

View File

@ -44,6 +44,14 @@ public interface UserRepository extends JpaRepository<UserEntity, UUID> {
@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<UserEntity> findTenantAndCustomerUsers(@Param("tenantId") UUID tenantId,
@Param("customerIds") Collection<UUID> 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<UserEntity> findByTenantId(@Param("tenantId") UUID tenantId,

View File

@ -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<User>, TenantEntityDao {
*/
PageData<User> 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<User> findUsersByCustomerIds(UUID tenantId, List<CustomerId> customerIds, PageLink pageLink);
PageData<User> findAll(PageLink pageLink);
PageData<User> findAllByAuthority(Authority authority, PageLink pageLink);

View File

@ -296,6 +296,15 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
return userDao.findCustomerUsers(tenantId.getId(), customerId.getId(), pageLink);
}
@Override
public PageData<User> findUsersByCustomerIds(TenantId tenantId, List<CustomerId> 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);

View File

@ -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<UserEmailInfo> getUsersForAssign(AlarmId alarmId, PageLink pageLink) {
Map<String, String> 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<PageData<UserEmailInfo>>() {
}, params).getBody();
}
public void setUserCredentialsEnabled(UserId userId, boolean userCredentialsEnabled) {
restTemplate.postForLocation(
baseURL + "/api/user/{userId}/userCredentialsEnabled?userCredentialsEnabled={userCredentialsEnabled}",

View File

@ -51,6 +51,12 @@ export class UserService {
defaultHttpOptionsFromConfig(config));
}
public getUsersForAssign(alarmId: string, pageLink: PageLink,
config?: RequestConfig): Observable<PageData<UserEmailInfo>> {
return this.http.get<PageData<UserEmailInfo>>(`/api/users/assign/${alarmId}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getUser(userId: string, config?: RequestConfig): Observable<User> {
return this.http.get<User>(`/api/user/${userId}`, defaultHttpOptionsFromConfig(config));
}

View File

@ -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<UserEmailInfo>())),
map(pageData => {