From 8d28a28b08bf9b16794345ccaa908f051d66716a Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 28 Oct 2021 18:08:17 +0300 Subject: [PATCH] Implement entities search within all tenants --- .../controller/EntityQueryController.java | 83 ++++++- .../data/search/EntitiesSearchRequest.java | 25 +++ .../data/search/EntitySearchResult.java | 50 +++++ .../service/query/EntitiesSearchService.java | 26 +++ .../query/EntitiesSearchServiceImpl.java | 208 ++++++++++++++++++ .../permission/PermissionChecker.java | 1 + .../permission/SysAdminPermissions.java | 31 +-- .../common/data/query/EntityFilterType.java | 3 +- .../data/query/EntityNameOrIdFilter.java | 32 +++ .../query/DefaultEntityQueryRepository.java | 52 ++++- .../dao/sql/query/EntityKeyMapping.java | 60 ++++- 11 files changed, 532 insertions(+), 39 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/data/search/EntitiesSearchRequest.java create mode 100644 application/src/main/java/org/thingsboard/server/data/search/EntitySearchResult.java create mode 100644 application/src/main/java/org/thingsboard/server/service/query/EntitiesSearchService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/query/EntitiesSearchServiceImpl.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/query/EntityNameOrIdFilter.java diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 019066c636..e5467c8ae9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -17,9 +17,10 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -36,20 +37,30 @@ import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.data.search.EntitiesSearchRequest; +import org.thingsboard.server.data.search.EntitySearchResult; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.query.EntitiesSearchService; import org.thingsboard.server.service.query.EntityQueryService; import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; @RestController @TbCoreComponent @RequestMapping("/api") +@RequiredArgsConstructor public class EntityQueryController extends BaseController { - @Autowired - private EntityQueryService entityQueryService; + private final EntityQueryService entityQueryService; + private final EntitiesSearchService entitiesSearchService; private static final int MAX_PAGE_SIZE = 100; @@ -123,4 +134,70 @@ public class EntityQueryController extends BaseController { } } + @ApiOperation(value = "Search entities (searchEntities)", notes = "Search entities with specified entity type by id or name within the whole platform. " + + "Searchable entity types are: CUSTOMER, USER, DEVICE, DEVICE_PROFILE, ASSET, ENTITY_VIEW, DASHBOARD, " + + "RULE_CHAIN, EDGE, OTA_PACKAGE, TB_RESOURCE, WIDGETS_BUNDLE, TENANT, TENANT_PROFILE." + NEW_LINE + + "The platform will search for entities, where a name contains the search text (case-insensitively), " + + "or if the search query is a valid UUID (e.g. 128e4d40-26b3-11ec-aaeb-c7661c54701e) then " + + "it will also search for an entity where id fully matches the query. If search query is empty " + + "then all entities will be returned (according to page number, page size and sorting)." + NEW_LINE + + "The returned result is a page of EntitySearchResult, which contains: " + + "entity id, entity fields represented as strings, tenant info and owner info. " + + "Returned entity fields are: name, type (will be present for USER, DEVICE, ASSET, ENTITY_VIEW, RULE_CHAIN, " + + "EDGE, OTA_PACKAGE, TB_RESOURCE entity types; in case of USER - the type is its authority), " + + "createdTime and lastActivityTime (will only be present for DEVICE and USER; for USER it is its last login time). " + + "Tenant info contains tenant's id and title; owner info contains the same info for an entity's owner " + + "(its customer, or if it is not a customer's entity - tenant)." + NEW_LINE + + "Example response value:\n" + + "{\n" + + " \"data\": [\n" + + " {\n" + + " \"entityId\": {\n" + + " \"entityType\": \"DEVICE\",\n" + + " \"id\": \"48be0670-25c9-11ec-a618-8165eb6b112a\"\n" + + " },\n" + + " \"fields\": {\n" + + " \"name\": \"Thermostat T1\",\n" + + " \"createdTime\": \"1633430698071\",\n" + + " \"lastActivityTime\": \"1635761085285\",\n" + + " \"type\": \"thermostat\"\n" + + " },\n" + + " \"tenantInfo\": {\n" + + " \"id\": {\n" + + " \"entityType\": \"TENANT\",\n" + + " \"id\": \"2ddd6120-25c9-11ec-a618-8165eb6b112a\"\n" + + " },\n" + + " \"name\": \"Tenant\"\n" + + " },\n" + + " \"ownerInfo\": {\n" + + " \"id\": {\n" + + " \"entityType\": \"CUSTOMER\",\n" + + " \"id\": \"26cba800-eee3-11eb-9e2c-fb031bd4619c\"\n" + + " },\n" + + " \"name\": \"Customer A\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"totalPages\": 1,\n" + + " \"totalElements\": 1,\n" + + " \"hasNext\": false\n" + + "}") + @PostMapping("/entities/search") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + public PageData searchEntities(@RequestBody EntitiesSearchRequest request, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "name, type, createdTime, lastActivityTime, createdTime, tenantId, customerId", required = false) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES, required = false) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + return entitiesSearchService.searchEntities(getCurrentUser(), request, createPageLink(pageSize, page, null, sortProperty, sortOrder)); + } catch (Exception e) { + throw handleException(e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/data/search/EntitiesSearchRequest.java b/application/src/main/java/org/thingsboard/server/data/search/EntitiesSearchRequest.java new file mode 100644 index 0000000000..48f8966107 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/data/search/EntitiesSearchRequest.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed 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 org.thingsboard.server.data.search; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; + +@Data +public class EntitiesSearchRequest { + private EntityType entityType; + private String searchQuery; +} diff --git a/application/src/main/java/org/thingsboard/server/data/search/EntitySearchResult.java b/application/src/main/java/org/thingsboard/server/data/search/EntitySearchResult.java new file mode 100644 index 0000000000..a26d580b7d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/data/search/EntitySearchResult.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed 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 org.thingsboard.server.data.search; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Map; + +@Data +public class EntitySearchResult { + private EntityId entityId; + private Map fields; + + private EntityTenantInfo tenantInfo; + private EntityOwnerInfo ownerInfo; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static final class EntityTenantInfo { + private TenantId id; + private String name; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static final class EntityOwnerInfo { + private EntityId id; + private String name; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntitiesSearchService.java b/application/src/main/java/org/thingsboard/server/service/query/EntitiesSearchService.java new file mode 100644 index 0000000000..36a34f89e7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/query/EntitiesSearchService.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed 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 org.thingsboard.server.service.query; + +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.data.search.EntitiesSearchRequest; +import org.thingsboard.server.data.search.EntitySearchResult; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface EntitiesSearchService { + PageData searchEntities(SecurityUser user, EntitiesSearchRequest request, PageLink pageLink); +} diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntitiesSearchServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/query/EntitiesSearchServiceImpl.java new file mode 100644 index 0000000000..1753a49c0d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/query/EntitiesSearchServiceImpl.java @@ -0,0 +1,208 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed 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 org.thingsboard.server.service.query; + +import com.google.common.base.Strings; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ContactBased; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityNameOrIdFilter; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.data.search.EntitiesSearchRequest; +import org.thingsboard.server.data.search.EntitySearchResult; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.state.DefaultDeviceStateService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.thingsboard.server.common.data.EntityType.ASSET; +import static org.thingsboard.server.common.data.EntityType.CUSTOMER; +import static org.thingsboard.server.common.data.EntityType.DASHBOARD; +import static org.thingsboard.server.common.data.EntityType.DEVICE; +import static org.thingsboard.server.common.data.EntityType.DEVICE_PROFILE; +import static org.thingsboard.server.common.data.EntityType.EDGE; +import static org.thingsboard.server.common.data.EntityType.ENTITY_VIEW; +import static org.thingsboard.server.common.data.EntityType.OTA_PACKAGE; +import static org.thingsboard.server.common.data.EntityType.RULE_CHAIN; +import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE; +import static org.thingsboard.server.common.data.EntityType.TENANT; +import static org.thingsboard.server.common.data.EntityType.TENANT_PROFILE; +import static org.thingsboard.server.common.data.EntityType.USER; +import static org.thingsboard.server.common.data.EntityType.WIDGETS_BUNDLE; +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.CREATED_TIME; +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.CUSTOMER_ID; +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.LAST_ACTIVITY_TIME; +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.NAME; +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.TENANT_ID; +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.TYPE; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +@Slf4j +public class EntitiesSearchServiceImpl implements EntitiesSearchService { + private final EntityQueryService entityQueryService; + + private final TenantService tenantService; + private final CustomerService customerService; + private final DefaultDeviceStateService deviceStateService; + + private static final List entityResponseFields = Stream.of(CREATED_TIME, NAME, TYPE, TENANT_ID, CUSTOMER_ID) + .map(field -> new EntityKey(EntityKeyType.ENTITY_FIELD, field)) + .collect(Collectors.toList()); + + private static final Set searchableEntityTypes = EnumSet.of( + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, RULE_CHAIN, ENTITY_VIEW, + WIDGETS_BUNDLE, TENANT_PROFILE, DEVICE_PROFILE, TB_RESOURCE, OTA_PACKAGE, EDGE + ); + + @Override + public PageData searchEntities(SecurityUser user, EntitiesSearchRequest request, PageLink pageLink) { + EntityType entityType = request.getEntityType(); + if (!searchableEntityTypes.contains(entityType)) { + return new PageData<>(); + } + + EntityDataQuery query = createSearchQuery(request.getSearchQuery(), entityType, pageLink); + PageData resultPage = entityQueryService.findEntityDataByQuery(user, query); + + Map> localOwnersCache = new HashMap<>(); + return resultPage.mapData(entityData -> { + Map fields = new HashMap<>(); + entityData.getLatest().values().stream() + .flatMap(values -> values.entrySet().stream()) + .forEach(entry -> fields.put(entry.getKey(), Strings.emptyToNull(entry.getValue().getValue()))); + + EntitySearchResult entitySearchResult = new EntitySearchResult(); + + entitySearchResult.setEntityId(entityData.getEntityId()); + entitySearchResult.setFields(fields); + setOwnerInfo(entitySearchResult, localOwnersCache); + + return entitySearchResult; + }); + } + + private EntityDataQuery createSearchQuery(String searchQuery, EntityType entityType, PageLink pageLink) { + EntityDataPageLink entityDataPageLink = new EntityDataPageLink(); + entityDataPageLink.setPageSize(pageLink.getPageSize()); + entityDataPageLink.setPage(pageLink.getPage()); + if (pageLink.getSortOrder() != null && StringUtils.isNotEmpty(pageLink.getSortOrder().getProperty())) { + entityDataPageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, pageLink.getSortOrder().getProperty()), + EntityDataSortOrder.Direction.valueOf(Optional.ofNullable(pageLink.getSortOrder().getDirection()).orElse(SortOrder.Direction.ASC).name()))); + } + + EntityNameOrIdFilter filter = new EntityNameOrIdFilter(); + filter.setEntityType(entityType); + filter.setNameOrId(searchQuery); + + List entityFields = entityResponseFields; + List latestValues = Collections.emptyList(); + + if (entityType == USER) { + entityFields = new ArrayList<>(entityFields); + entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, LAST_ACTIVITY_TIME)); + } else if (entityType == DEVICE) { + EntityKey lastActivityTimeKey; + if (deviceStateService.isPersistToTelemetry()) { + lastActivityTimeKey = new EntityKey(EntityKeyType.TIME_SERIES, LAST_ACTIVITY_TIME); + } else { + lastActivityTimeKey = new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_ACTIVITY_TIME); + } + latestValues = List.of(lastActivityTimeKey); + if (entityDataPageLink.getSortOrder() != null && entityDataPageLink.getSortOrder().getKey().getKey().equals(LAST_ACTIVITY_TIME)) { + entityDataPageLink.getSortOrder().setKey(lastActivityTimeKey); + } + } + + return new EntityDataQuery(filter, entityDataPageLink, entityFields, latestValues, Collections.emptyList()); + } + + private void setOwnerInfo(EntitySearchResult entitySearchResult, Map> localOwnersCache) { + Map fields = entitySearchResult.getFields(); + + UUID tenantUuid = toUuid(fields.remove(TENANT_ID)); + UUID customerUuid = toUuid(fields.remove(CUSTOMER_ID)); + + Tenant tenant = null; + if (tenantUuid != null) { + tenant = getTenant(new TenantId(tenantUuid), localOwnersCache); + } + + ContactBased owner; + if (customerUuid != null) { + owner = getCustomer(new CustomerId(customerUuid), localOwnersCache); + } else { + owner = tenant; + } + + if (tenant != null) { + entitySearchResult.setTenantInfo(new EntitySearchResult.EntityTenantInfo(tenant.getId(), tenant.getName())); + } + if (owner != null) { + entitySearchResult.setOwnerInfo(new EntitySearchResult.EntityOwnerInfo(owner.getId(), owner.getName())); + } + } + + private Tenant getTenant(TenantId tenantId, Map> localOwnersCache) { + return (Tenant) localOwnersCache.computeIfAbsent(tenantId, id -> tenantService.findTenantById(tenantId)); + } + + private Customer getCustomer(CustomerId customerId, Map> localOwnersCache) { + return (Customer) localOwnersCache.computeIfAbsent(customerId, id -> customerService.findCustomerById(TenantId.SYS_TENANT_ID, customerId)); + } + + private UUID toUuid(String uuid) { + try { + UUID id = UUID.fromString(uuid); + if (!id.equals(EntityId.NULL_UUID)) { + return id; + } + } catch (Exception ignored) {} + + return null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java b/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java index 3f6e16d753..45a43876e6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java @@ -69,5 +69,6 @@ public interface PermissionChecker { } }; + PermissionChecker allowReadPermissionChecker = new GenericPermissionChecker(Operation.READ, Operation.READ_TELEMETRY, Operation.READ_ATTRIBUTES); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index f703102e18..8f9648dc93 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -17,10 +17,7 @@ package org.thingsboard.server.service.security.permission; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; @Component(value="sysAdminPermissions") @@ -29,23 +26,29 @@ public class SysAdminPermissions extends AbstractPermissions { public SysAdminPermissions() { super(); put(Resource.ADMIN_SETTINGS, PermissionChecker.allowAllPermissionChecker); - put(Resource.DASHBOARD, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.DASHBOARD, PermissionChecker.allowReadPermissionChecker); put(Resource.TENANT, PermissionChecker.allowAllPermissionChecker); - put(Resource.RULE_CHAIN, systemEntityPermissionChecker); - put(Resource.USER, userPermissionChecker); + put(Resource.RULE_CHAIN, PermissionChecker.allowReadPermissionChecker); + put(Resource.USER, PermissionChecker.allowAllPermissionChecker); put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker); put(Resource.WIDGET_TYPE, systemEntityPermissionChecker); put(Resource.OAUTH2_CONFIGURATION_INFO, PermissionChecker.allowAllPermissionChecker); put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); - put(Resource.TB_RESOURCE, systemEntityPermissionChecker); + put(Resource.TB_RESOURCE, PermissionChecker.allowAllPermissionChecker); + put(Resource.CUSTOMER, PermissionChecker.allowReadPermissionChecker); + put(Resource.ASSET, PermissionChecker.allowReadPermissionChecker); + put(Resource.DEVICE, PermissionChecker.allowReadPermissionChecker); + put(Resource.ENTITY_VIEW, PermissionChecker.allowReadPermissionChecker); + put(Resource.DEVICE_PROFILE, PermissionChecker.allowReadPermissionChecker); + put(Resource.OTA_PACKAGE, PermissionChecker.allowReadPermissionChecker); + put(Resource.EDGE, PermissionChecker.allowReadPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { @Override public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { - if (entity.getTenantId() != null && !entity.getTenantId().isNullUid()) { return false; } @@ -53,16 +56,4 @@ public class SysAdminPermissions extends AbstractPermissions { } }; - private static final PermissionChecker userPermissionChecker = new PermissionChecker() { - - @Override - public boolean hasPermission(SecurityUser user, Operation operation, UserId userId, User userEntity) { - if (Authority.CUSTOMER_USER.equals(userEntity.getAuthority())) { - return false; - } - return true; - } - - }; - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java index 1a843c7b0f..c75d6bf4df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java @@ -29,7 +29,8 @@ public enum EntityFilterType { DEVICE_SEARCH_QUERY("deviceSearchQuery"), ENTITY_VIEW_SEARCH_QUERY("entityViewSearchQuery"), EDGE_SEARCH_QUERY("edgeSearchQuery"), - API_USAGE_STATE("apiUsageState"); + API_USAGE_STATE("apiUsageState"), + ENTITY_NAME_OR_ID("entityNameOrId"); private final String label; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityNameOrIdFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityNameOrIdFilter.java new file mode 100644 index 0000000000..1fcb9e9350 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityNameOrIdFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed 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 org.thingsboard.server.common.data.query; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityType; + +@EqualsAndHashCode(callSuper = true) +@Data +public class EntityNameOrIdFilter extends EntitySearchQueryFilter { + private String nameOrId; + private EntityType entityType; + + @Override + public EntityFilterType getType() { + return EntityFilterType.ENTITY_NAME_OR_ID; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 4b331f78f2..cbecc96f59 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.query.EntityFilterType; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.query.EntityNameFilter; +import org.thingsboard.server.common.data.query.EntityNameOrIdFilter; import org.thingsboard.server.common.data.query.EntitySearchQueryFilter; import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.EntityViewSearchQueryFilter; @@ -236,6 +237,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { entityTableMap.put(EntityType.TENANT, "tenant"); entityTableMap.put(EntityType.API_USAGE_STATE, SELECT_API_USAGE_STATE); entityTableMap.put(EntityType.EDGE, "edge"); + entityTableMap.put(EntityType.RULE_CHAIN, "rule_chain"); + entityTableMap.put(EntityType.WIDGETS_BUNDLE, "widgets_bundle"); + entityTableMap.put(EntityType.TENANT_PROFILE, "tenant_profile"); + entityTableMap.put(EntityType.DEVICE_PROFILE, "device_profile"); + entityTableMap.put(EntityType.TB_RESOURCE, "resource"); + entityTableMap.put(EntityType.OTA_PACKAGE, "ota_package"); } public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{ @@ -441,7 +448,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { Optional sortOrderMappingOpt = mappings.stream().filter(EntityKeyMapping::isSortOrder).findFirst(); if (sortOrderMappingOpt.isPresent()) { EntityKeyMapping sortOrderMapping = sortOrderMappingOpt.get(); - String direction = sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC ? "asc" : "desc"; + String direction = sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC ? "asc" : "desc nulls last"; if (sortOrderMapping.getEntityKey().getType() == EntityKeyType.ENTITY_FIELD) { dataQuery = String.format("%s order by %s %s, result.id %s", dataQuery, sortOrderMapping.getValueAlias(), direction, direction); } else { @@ -471,15 +478,26 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { String entityFieldsQuery = EntityKeyMapping.buildQuery(ctx, entityFieldsFilters, entityFilter.getType()); String result = permissionQuery; if (!entityFilterQuery.isEmpty()) { - result += " and (" + entityFilterQuery + ")"; + if (!result.isEmpty()) { + result += " and (" + entityFilterQuery + ")"; + } else { + result = "(" + entityFilterQuery + ")"; + } } if (!entityFieldsQuery.isEmpty()) { - result += " and (" + entityFieldsQuery + ")"; + if (!result.isEmpty()) { + result += " and (" + entityFieldsQuery + ")"; + } else { + result = "(" + entityFieldsQuery + ")"; + } } return result; } private String buildPermissionQuery(QueryContext ctx, EntityFilter entityFilter) { + if (ctx.getTenantId().equals(TenantId.SYS_TENANT_ID)) { + return ""; + } switch (entityFilter.getType()) { case RELATIONS_QUERY: case DEVICE_SEARCH_QUERY: @@ -548,6 +566,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case API_USAGE_STATE: case ENTITY_TYPE: return ""; + case ENTITY_NAME_OR_ID: + return entityNameOrIdQuery(ctx, (EntityNameOrIdFilter) entityFilter); default: throw new RuntimeException("Not implemented!"); } @@ -759,7 +779,27 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { private String entityNameQuery(QueryContext ctx, EntityNameFilter filter) { ctx.addStringParameter("entity_filter_name_filter", filter.getEntityNameFilter()); - return "lower(e.search_text) like lower(concat(:entity_filter_name_filter, '%%'))"; + return "lower(e.search_text) like lower(concat('%', :entity_filter_name_filter, '%'))"; + } + + private String entityNameOrIdQuery(QueryContext ctx, EntityNameOrIdFilter filter) { + String nameOrId = filter.getNameOrId(); + if (StringUtils.isNotEmpty(nameOrId)) { + nameOrId = nameOrId.replaceAll("%", "\\\\%").replaceAll("_", "\\\\_"); + ctx.addStringParameter("entity_id_or_search_text_filter", nameOrId); + String query = ""; + + String searchTextField = EntityKeyMapping.searchTextFields.get(filter.getEntityType()); + query += "lower(e." + searchTextField + ") like lower(concat('%', :entity_id_or_search_text_filter, '%'))"; + + try { + UUID.fromString(nameOrId); + query += " or e.id = :entity_id_or_search_text_filter::uuid"; + } catch (Exception ignored) {} + + return query; + } + return "true"; } private String typeQuery(QueryContext ctx, EntityFilter filter) { @@ -787,7 +827,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } ctx.addStringParameter("entity_filter_type_query_type", type); ctx.addStringParameter("entity_filter_type_query_name", name); - return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))"; + return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(concat('%', :entity_filter_type_query_name, '%'))"; } private EntityType resolveEntityType(EntityFilter entityFilter) { @@ -816,6 +856,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return ((RelationsQueryFilter) entityFilter).getRootEntity().getEntityType(); case API_USAGE_STATE: return EntityType.API_USAGE_STATE; + case ENTITY_NAME_OR_ID: + return ((EntityNameOrIdFilter) entityFilter).getEntityType(); default: throw new RuntimeException("Not implemented!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index caed48758a..3d40156329 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.model.ModelConstants; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -53,6 +54,8 @@ public class EntityKeyMapping { private static final Map> allowedEntityFieldMap = new HashMap<>(); private static final Map entityFieldColumnMap = new HashMap<>(); private static final Map> aliases = new HashMap<>(); + private static final Map> propertiesFunctions = new EnumMap<>(EntityType.class); + public static final Map searchTextFields = new EnumMap<>(EntityType.class); public static final String CREATED_TIME = "createdTime"; public static final String ENTITY_TYPE = "entityType"; @@ -72,35 +75,51 @@ public class EntityKeyMapping { public static final String ZIP = "zip"; public static final String PHONE = "phone"; public static final String ADDITIONAL_INFO = "additionalInfo"; + public static final String TENANT_ID = "tenantId"; + public static final String CUSTOMER_ID = "customerId"; + public static final String AUTHORITY = "authority"; + public static final String RESOURCE_TYPE = "resourceType"; + public static final String LAST_ACTIVITY_TIME = "lastActivityTime"; - public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); - public static final List widgetEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME); + public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO, TENANT_ID); public static final List commonEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, ADDITIONAL_INFO); - public static final List dashboardEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, TITLE); - public static final List labeledEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, ADDITIONAL_INFO); + + public static final List dashboardEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, TITLE, TENANT_ID); + public static final List labeledEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, ADDITIONAL_INFO, TENANT_ID, CUSTOMER_ID); public static final List contactBasedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, EMAIL, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO); - public static final Set apiUsageStateEntityFields = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME)); + public static final Set apiUsageStateEntityFields = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME)); public static final Set commonEntityFieldsSet = new HashSet<>(commonEntityFields); public static final Set relationQueryEntityFieldsSet = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, FIRST_NAME, LAST_NAME, EMAIL, REGION, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO)); static { allowedEntityFieldMap.put(EntityType.DEVICE, new HashSet<>(labeledEntityFields)); allowedEntityFieldMap.put(EntityType.ASSET, new HashSet<>(labeledEntityFields)); + allowedEntityFieldMap.put(EntityType.EDGE, new HashSet<>(labeledEntityFields)); allowedEntityFieldMap.put(EntityType.ENTITY_VIEW, new HashSet<>(typedEntityFields)); + allowedEntityFieldMap.get(EntityType.ENTITY_VIEW).add(CUSTOMER_ID); allowedEntityFieldMap.put(EntityType.TENANT, new HashSet<>(contactBasedEntityFields)); allowedEntityFieldMap.get(EntityType.TENANT).add(REGION); allowedEntityFieldMap.put(EntityType.CUSTOMER, new HashSet<>(contactBasedEntityFields)); + allowedEntityFieldMap.get(EntityType.CUSTOMER).add(TENANT_ID); + + allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(CREATED_TIME, FIRST_NAME, LAST_NAME, EMAIL, + ADDITIONAL_INFO, AUTHORITY, TENANT_ID, CUSTOMER_ID))); - allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(CREATED_TIME, FIRST_NAME, LAST_NAME, EMAIL, ADDITIONAL_INFO))); + allowedEntityFieldMap.put(EntityType.DEVICE_PROFILE, new HashSet<>(commonEntityFields)); + allowedEntityFieldMap.get(EntityType.DEVICE_PROFILE).add(TENANT_ID); allowedEntityFieldMap.put(EntityType.DASHBOARD, new HashSet<>(dashboardEntityFields)); allowedEntityFieldMap.put(EntityType.RULE_CHAIN, new HashSet<>(commonEntityFields)); + allowedEntityFieldMap.get(EntityType.RULE_CHAIN).add(TENANT_ID); + allowedEntityFieldMap.get(EntityType.RULE_CHAIN).add(TYPE); allowedEntityFieldMap.put(EntityType.RULE_NODE, new HashSet<>(commonEntityFields)); - allowedEntityFieldMap.put(EntityType.WIDGET_TYPE, new HashSet<>(widgetEntityFields)); - allowedEntityFieldMap.put(EntityType.WIDGETS_BUNDLE, new HashSet<>(widgetEntityFields)); + allowedEntityFieldMap.put(EntityType.WIDGET_TYPE, new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TENANT_ID))); + allowedEntityFieldMap.put(EntityType.WIDGETS_BUNDLE, new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, TITLE, TENANT_ID))); allowedEntityFieldMap.put(EntityType.API_USAGE_STATE, apiUsageStateEntityFields); + allowedEntityFieldMap.put(EntityType.TB_RESOURCE, Set.of(CREATED_TIME, ENTITY_TYPE, RESOURCE_TYPE, TITLE, TENANT_ID)); + allowedEntityFieldMap.put(EntityType.OTA_PACKAGE, Set.of(CREATED_TIME, ENTITY_TYPE, TYPE, TITLE, TENANT_ID)); entityFieldColumnMap.put(CREATED_TIME, ModelConstants.CREATED_TIME_PROPERTY); entityFieldColumnMap.put(ENTITY_TYPE, ModelConstants.ENTITY_TYPE_PROPERTY); @@ -120,25 +139,42 @@ public class EntityKeyMapping { entityFieldColumnMap.put(ZIP, ModelConstants.ZIP_PROPERTY); entityFieldColumnMap.put(PHONE, ModelConstants.PHONE_PROPERTY); entityFieldColumnMap.put(ADDITIONAL_INFO, ModelConstants.ADDITIONAL_INFO_PROPERTY); + entityFieldColumnMap.put(TENANT_ID, ModelConstants.TENANT_ID_PROPERTY); + entityFieldColumnMap.put(CUSTOMER_ID, ModelConstants.CUSTOMER_ID_PROPERTY); + entityFieldColumnMap.put(AUTHORITY, ModelConstants.USER_AUTHORITY_PROPERTY); + entityFieldColumnMap.put(RESOURCE_TYPE, ModelConstants.RESOURCE_TYPE_COLUMN); Map contactBasedAliases = new HashMap<>(); contactBasedAliases.put(NAME, TITLE); contactBasedAliases.put(LABEL, TITLE); aliases.put(EntityType.TENANT, contactBasedAliases); - aliases.put(EntityType.CUSTOMER, contactBasedAliases); + aliases.put(EntityType.CUSTOMER, new HashMap<>(contactBasedAliases)); aliases.put(EntityType.DASHBOARD, contactBasedAliases); Map commonEntityAliases = new HashMap<>(); commonEntityAliases.put(TITLE, NAME); aliases.put(EntityType.DEVICE, commonEntityAliases); aliases.put(EntityType.ASSET, commonEntityAliases); aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases); - aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases); + aliases.put(EntityType.EDGE, commonEntityAliases); + aliases.put(EntityType.WIDGETS_BUNDLE, new HashMap<>(commonEntityAliases)); + aliases.get(EntityType.WIDGETS_BUNDLE).put(NAME, TITLE); Map userEntityAliases = new HashMap<>(); userEntityAliases.put(TITLE, EMAIL); userEntityAliases.put(LABEL, EMAIL); userEntityAliases.put(NAME, EMAIL); + userEntityAliases.put(TYPE, AUTHORITY); aliases.put(EntityType.USER, userEntityAliases); + aliases.put(EntityType.TB_RESOURCE, Map.of(NAME, TITLE, TYPE, RESOURCE_TYPE)); + aliases.put(EntityType.OTA_PACKAGE, Map.of(NAME, TITLE)); + + propertiesFunctions.put(EntityType.USER, Map.of( + LAST_ACTIVITY_TIME, "cast(e.additional_info::json ->> 'lastLoginTs' as bigint)" + )); + + Arrays.stream(EntityType.values()).forEach(entityType -> { + searchTextFields.put(entityType, ModelConstants.SEARCH_TEXT_PROPERTY); + }); } private int index; @@ -179,6 +215,10 @@ public class EntityKeyMapping { String column = entityFieldColumnMap.get(alias); return String.format("cast(e.%s as varchar) as %s", column, getValueAlias()); } else { + Map entityPropertiesFunctions = propertiesFunctions.get(entityType); + if (entityPropertiesFunctions != null && entityPropertiesFunctions.containsKey(alias)) { + return String.format("%s as %s", entityPropertiesFunctions.get(alias), getValueAlias()); + } return String.format("'' as %s", getValueAlias()); } }