Commit 8e0eab37a8b5832c304c7475f021653b36e84cf1

Authored by Andrew Shvayka
Committed by GitHub
2 parents 615e56b5 8d28a28b

Merge pull request #5474 from ViacheslavKlimov/sysadmin-entities-search

[3.3.3] Entities search within all tenants
... ... @@ -17,9 +17,10 @@ package org.thingsboard.server.controller;
17 17
18 18 import io.swagger.annotations.ApiOperation;
19 19 import io.swagger.annotations.ApiParam;
20   -import org.springframework.beans.factory.annotation.Autowired;
  20 +import lombok.RequiredArgsConstructor;
21 21 import org.springframework.http.ResponseEntity;
22 22 import org.springframework.security.access.prepost.PreAuthorize;
  23 +import org.springframework.web.bind.annotation.PostMapping;
23 24 import org.springframework.web.bind.annotation.RequestBody;
24 25 import org.springframework.web.bind.annotation.RequestMapping;
25 26 import org.springframework.web.bind.annotation.RequestMethod;
... ... @@ -36,20 +37,30 @@ import org.thingsboard.server.common.data.query.EntityCountQuery;
36 37 import org.thingsboard.server.common.data.query.EntityData;
37 38 import org.thingsboard.server.common.data.query.EntityDataPageLink;
38 39 import org.thingsboard.server.common.data.query.EntityDataQuery;
  40 +import org.thingsboard.server.data.search.EntitiesSearchRequest;
  41 +import org.thingsboard.server.data.search.EntitySearchResult;
39 42 import org.thingsboard.server.queue.util.TbCoreComponent;
  43 +import org.thingsboard.server.service.query.EntitiesSearchService;
40 44 import org.thingsboard.server.service.query.EntityQueryService;
41 45
42 46 import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION;
43 47 import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION;
44 48 import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION;
  49 +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
  50 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  51 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  52 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  53 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  54 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
45 55
46 56 @RestController
47 57 @TbCoreComponent
48 58 @RequestMapping("/api")
  59 +@RequiredArgsConstructor
49 60 public class EntityQueryController extends BaseController {
50 61
51   - @Autowired
52   - private EntityQueryService entityQueryService;
  62 + private final EntityQueryService entityQueryService;
  63 + private final EntitiesSearchService entitiesSearchService;
53 64
54 65 private static final int MAX_PAGE_SIZE = 100;
55 66
... ... @@ -123,4 +134,70 @@ public class EntityQueryController extends BaseController {
123 134 }
124 135 }
125 136
  137 + @ApiOperation(value = "Search entities (searchEntities)", notes = "Search entities with specified entity type by id or name within the whole platform. " +
  138 + "Searchable entity types are: CUSTOMER, USER, DEVICE, DEVICE_PROFILE, ASSET, ENTITY_VIEW, DASHBOARD, " +
  139 + "RULE_CHAIN, EDGE, OTA_PACKAGE, TB_RESOURCE, WIDGETS_BUNDLE, TENANT, TENANT_PROFILE." + NEW_LINE +
  140 + "The platform will search for entities, where a name contains the search text (case-insensitively), " +
  141 + "or if the search query is a valid UUID (e.g. 128e4d40-26b3-11ec-aaeb-c7661c54701e) then " +
  142 + "it will also search for an entity where id fully matches the query. If search query is empty " +
  143 + "then all entities will be returned (according to page number, page size and sorting)." + NEW_LINE +
  144 + "The returned result is a page of EntitySearchResult, which contains: " +
  145 + "entity id, entity fields represented as strings, tenant info and owner info. " +
  146 + "Returned entity fields are: name, type (will be present for USER, DEVICE, ASSET, ENTITY_VIEW, RULE_CHAIN, " +
  147 + "EDGE, OTA_PACKAGE, TB_RESOURCE entity types; in case of USER - the type is its authority), " +
  148 + "createdTime and lastActivityTime (will only be present for DEVICE and USER; for USER it is its last login time). " +
  149 + "Tenant info contains tenant's id and title; owner info contains the same info for an entity's owner " +
  150 + "(its customer, or if it is not a customer's entity - tenant)." + NEW_LINE +
  151 + "Example response value:\n" +
  152 + "{\n" +
  153 + " \"data\": [\n" +
  154 + " {\n" +
  155 + " \"entityId\": {\n" +
  156 + " \"entityType\": \"DEVICE\",\n" +
  157 + " \"id\": \"48be0670-25c9-11ec-a618-8165eb6b112a\"\n" +
  158 + " },\n" +
  159 + " \"fields\": {\n" +
  160 + " \"name\": \"Thermostat T1\",\n" +
  161 + " \"createdTime\": \"1633430698071\",\n" +
  162 + " \"lastActivityTime\": \"1635761085285\",\n" +
  163 + " \"type\": \"thermostat\"\n" +
  164 + " },\n" +
  165 + " \"tenantInfo\": {\n" +
  166 + " \"id\": {\n" +
  167 + " \"entityType\": \"TENANT\",\n" +
  168 + " \"id\": \"2ddd6120-25c9-11ec-a618-8165eb6b112a\"\n" +
  169 + " },\n" +
  170 + " \"name\": \"Tenant\"\n" +
  171 + " },\n" +
  172 + " \"ownerInfo\": {\n" +
  173 + " \"id\": {\n" +
  174 + " \"entityType\": \"CUSTOMER\",\n" +
  175 + " \"id\": \"26cba800-eee3-11eb-9e2c-fb031bd4619c\"\n" +
  176 + " },\n" +
  177 + " \"name\": \"Customer A\"\n" +
  178 + " }\n" +
  179 + " }\n" +
  180 + " ],\n" +
  181 + " \"totalPages\": 1,\n" +
  182 + " \"totalElements\": 1,\n" +
  183 + " \"hasNext\": false\n" +
  184 + "}")
  185 + @PostMapping("/entities/search")
  186 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
  187 + public PageData<EntitySearchResult> searchEntities(@RequestBody EntitiesSearchRequest request,
  188 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
  189 + @RequestParam int page,
  190 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
  191 + @RequestParam int pageSize,
  192 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "name, type, createdTime, lastActivityTime, createdTime, tenantId, customerId", required = false)
  193 + @RequestParam(required = false) String sortProperty,
  194 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES, required = false)
  195 + @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  196 + try {
  197 + return entitiesSearchService.searchEntities(getCurrentUser(), request, createPageLink(pageSize, page, null, sortProperty, sortOrder));
  198 + } catch (Exception e) {
  199 + throw handleException(e);
  200 + }
  201 + }
  202 +
126 203 }
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.data.search;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.common.data.EntityType;
  20 +
  21 +@Data
  22 +public class EntitiesSearchRequest {
  23 + private EntityType entityType;
  24 + private String searchQuery;
  25 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.data.search;
  17 +
  18 +import lombok.AllArgsConstructor;
  19 +import lombok.Data;
  20 +import lombok.NoArgsConstructor;
  21 +import org.thingsboard.server.common.data.id.EntityId;
  22 +import org.thingsboard.server.common.data.id.TenantId;
  23 +
  24 +import java.util.Map;
  25 +
  26 +@Data
  27 +public class EntitySearchResult {
  28 + private EntityId entityId;
  29 + private Map<String, String> fields;
  30 +
  31 + private EntityTenantInfo tenantInfo;
  32 + private EntityOwnerInfo ownerInfo;
  33 +
  34 + @Data
  35 + @AllArgsConstructor
  36 + @NoArgsConstructor
  37 + public static final class EntityTenantInfo {
  38 + private TenantId id;
  39 + private String name;
  40 + }
  41 +
  42 + @Data
  43 + @AllArgsConstructor
  44 + @NoArgsConstructor
  45 + public static final class EntityOwnerInfo {
  46 + private EntityId id;
  47 + private String name;
  48 + }
  49 +
  50 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.query;
  17 +
  18 +import org.thingsboard.server.common.data.page.PageData;
  19 +import org.thingsboard.server.common.data.page.PageLink;
  20 +import org.thingsboard.server.data.search.EntitiesSearchRequest;
  21 +import org.thingsboard.server.data.search.EntitySearchResult;
  22 +import org.thingsboard.server.service.security.model.SecurityUser;
  23 +
  24 +public interface EntitiesSearchService {
  25 + PageData<EntitySearchResult> searchEntities(SecurityUser user, EntitiesSearchRequest request, PageLink pageLink);
  26 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.query;
  17 +
  18 +import com.google.common.base.Strings;
  19 +import lombok.RequiredArgsConstructor;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +import org.apache.commons.lang3.StringUtils;
  22 +import org.springframework.stereotype.Service;
  23 +import org.thingsboard.server.common.data.ContactBased;
  24 +import org.thingsboard.server.common.data.Customer;
  25 +import org.thingsboard.server.common.data.EntityType;
  26 +import org.thingsboard.server.common.data.Tenant;
  27 +import org.thingsboard.server.common.data.id.CustomerId;
  28 +import org.thingsboard.server.common.data.id.EntityId;
  29 +import org.thingsboard.server.common.data.id.TenantId;
  30 +import org.thingsboard.server.common.data.page.PageData;
  31 +import org.thingsboard.server.common.data.page.PageLink;
  32 +import org.thingsboard.server.common.data.page.SortOrder;
  33 +import org.thingsboard.server.common.data.query.EntityData;
  34 +import org.thingsboard.server.common.data.query.EntityDataPageLink;
  35 +import org.thingsboard.server.common.data.query.EntityDataQuery;
  36 +import org.thingsboard.server.common.data.query.EntityDataSortOrder;
  37 +import org.thingsboard.server.common.data.query.EntityKey;
  38 +import org.thingsboard.server.common.data.query.EntityKeyType;
  39 +import org.thingsboard.server.common.data.query.EntityNameOrIdFilter;
  40 +import org.thingsboard.server.dao.customer.CustomerService;
  41 +import org.thingsboard.server.dao.tenant.TenantService;
  42 +import org.thingsboard.server.data.search.EntitiesSearchRequest;
  43 +import org.thingsboard.server.data.search.EntitySearchResult;
  44 +import org.thingsboard.server.queue.util.TbCoreComponent;
  45 +import org.thingsboard.server.service.security.model.SecurityUser;
  46 +import org.thingsboard.server.service.state.DefaultDeviceStateService;
  47 +
  48 +import java.util.ArrayList;
  49 +import java.util.Collections;
  50 +import java.util.EnumSet;
  51 +import java.util.HashMap;
  52 +import java.util.List;
  53 +import java.util.Map;
  54 +import java.util.Optional;
  55 +import java.util.Set;
  56 +import java.util.UUID;
  57 +import java.util.stream.Collectors;
  58 +import java.util.stream.Stream;
  59 +
  60 +import static org.thingsboard.server.common.data.EntityType.ASSET;
  61 +import static org.thingsboard.server.common.data.EntityType.CUSTOMER;
  62 +import static org.thingsboard.server.common.data.EntityType.DASHBOARD;
  63 +import static org.thingsboard.server.common.data.EntityType.DEVICE;
  64 +import static org.thingsboard.server.common.data.EntityType.DEVICE_PROFILE;
  65 +import static org.thingsboard.server.common.data.EntityType.EDGE;
  66 +import static org.thingsboard.server.common.data.EntityType.ENTITY_VIEW;
  67 +import static org.thingsboard.server.common.data.EntityType.OTA_PACKAGE;
  68 +import static org.thingsboard.server.common.data.EntityType.RULE_CHAIN;
  69 +import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE;
  70 +import static org.thingsboard.server.common.data.EntityType.TENANT;
  71 +import static org.thingsboard.server.common.data.EntityType.TENANT_PROFILE;
  72 +import static org.thingsboard.server.common.data.EntityType.USER;
  73 +import static org.thingsboard.server.common.data.EntityType.WIDGETS_BUNDLE;
  74 +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.CREATED_TIME;
  75 +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.CUSTOMER_ID;
  76 +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.LAST_ACTIVITY_TIME;
  77 +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.NAME;
  78 +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.TENANT_ID;
  79 +import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.TYPE;
  80 +
  81 +@Service
  82 +@TbCoreComponent
  83 +@RequiredArgsConstructor
  84 +@Slf4j
  85 +public class EntitiesSearchServiceImpl implements EntitiesSearchService {
  86 + private final EntityQueryService entityQueryService;
  87 +
  88 + private final TenantService tenantService;
  89 + private final CustomerService customerService;
  90 + private final DefaultDeviceStateService deviceStateService;
  91 +
  92 + private static final List<EntityKey> entityResponseFields = Stream.of(CREATED_TIME, NAME, TYPE, TENANT_ID, CUSTOMER_ID)
  93 + .map(field -> new EntityKey(EntityKeyType.ENTITY_FIELD, field))
  94 + .collect(Collectors.toList());
  95 +
  96 + private static final Set<EntityType> searchableEntityTypes = EnumSet.of(
  97 + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, RULE_CHAIN, ENTITY_VIEW,
  98 + WIDGETS_BUNDLE, TENANT_PROFILE, DEVICE_PROFILE, TB_RESOURCE, OTA_PACKAGE, EDGE
  99 + );
  100 +
  101 + @Override
  102 + public PageData<EntitySearchResult> searchEntities(SecurityUser user, EntitiesSearchRequest request, PageLink pageLink) {
  103 + EntityType entityType = request.getEntityType();
  104 + if (!searchableEntityTypes.contains(entityType)) {
  105 + return new PageData<>();
  106 + }
  107 +
  108 + EntityDataQuery query = createSearchQuery(request.getSearchQuery(), entityType, pageLink);
  109 + PageData<EntityData> resultPage = entityQueryService.findEntityDataByQuery(user, query);
  110 +
  111 + Map<EntityId, ContactBased<? extends EntityId>> localOwnersCache = new HashMap<>();
  112 + return resultPage.mapData(entityData -> {
  113 + Map<String, String> fields = new HashMap<>();
  114 + entityData.getLatest().values().stream()
  115 + .flatMap(values -> values.entrySet().stream())
  116 + .forEach(entry -> fields.put(entry.getKey(), Strings.emptyToNull(entry.getValue().getValue())));
  117 +
  118 + EntitySearchResult entitySearchResult = new EntitySearchResult();
  119 +
  120 + entitySearchResult.setEntityId(entityData.getEntityId());
  121 + entitySearchResult.setFields(fields);
  122 + setOwnerInfo(entitySearchResult, localOwnersCache);
  123 +
  124 + return entitySearchResult;
  125 + });
  126 + }
  127 +
  128 + private EntityDataQuery createSearchQuery(String searchQuery, EntityType entityType, PageLink pageLink) {
  129 + EntityDataPageLink entityDataPageLink = new EntityDataPageLink();
  130 + entityDataPageLink.setPageSize(pageLink.getPageSize());
  131 + entityDataPageLink.setPage(pageLink.getPage());
  132 + if (pageLink.getSortOrder() != null && StringUtils.isNotEmpty(pageLink.getSortOrder().getProperty())) {
  133 + entityDataPageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, pageLink.getSortOrder().getProperty()),
  134 + EntityDataSortOrder.Direction.valueOf(Optional.ofNullable(pageLink.getSortOrder().getDirection()).orElse(SortOrder.Direction.ASC).name())));
  135 + }
  136 +
  137 + EntityNameOrIdFilter filter = new EntityNameOrIdFilter();
  138 + filter.setEntityType(entityType);
  139 + filter.setNameOrId(searchQuery);
  140 +
  141 + List<EntityKey> entityFields = entityResponseFields;
  142 + List<EntityKey> latestValues = Collections.emptyList();
  143 +
  144 + if (entityType == USER) {
  145 + entityFields = new ArrayList<>(entityFields);
  146 + entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, LAST_ACTIVITY_TIME));
  147 + } else if (entityType == DEVICE) {
  148 + EntityKey lastActivityTimeKey;
  149 + if (deviceStateService.isPersistToTelemetry()) {
  150 + lastActivityTimeKey = new EntityKey(EntityKeyType.TIME_SERIES, LAST_ACTIVITY_TIME);
  151 + } else {
  152 + lastActivityTimeKey = new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_ACTIVITY_TIME);
  153 + }
  154 + latestValues = List.of(lastActivityTimeKey);
  155 + if (entityDataPageLink.getSortOrder() != null && entityDataPageLink.getSortOrder().getKey().getKey().equals(LAST_ACTIVITY_TIME)) {
  156 + entityDataPageLink.getSortOrder().setKey(lastActivityTimeKey);
  157 + }
  158 + }
  159 +
  160 + return new EntityDataQuery(filter, entityDataPageLink, entityFields, latestValues, Collections.emptyList());
  161 + }
  162 +
  163 + private void setOwnerInfo(EntitySearchResult entitySearchResult, Map<EntityId, ContactBased<? extends EntityId>> localOwnersCache) {
  164 + Map<String, String> fields = entitySearchResult.getFields();
  165 +
  166 + UUID tenantUuid = toUuid(fields.remove(TENANT_ID));
  167 + UUID customerUuid = toUuid(fields.remove(CUSTOMER_ID));
  168 +
  169 + Tenant tenant = null;
  170 + if (tenantUuid != null) {
  171 + tenant = getTenant(new TenantId(tenantUuid), localOwnersCache);
  172 + }
  173 +
  174 + ContactBased<? extends EntityId> owner;
  175 + if (customerUuid != null) {
  176 + owner = getCustomer(new CustomerId(customerUuid), localOwnersCache);
  177 + } else {
  178 + owner = tenant;
  179 + }
  180 +
  181 + if (tenant != null) {
  182 + entitySearchResult.setTenantInfo(new EntitySearchResult.EntityTenantInfo(tenant.getId(), tenant.getName()));
  183 + }
  184 + if (owner != null) {
  185 + entitySearchResult.setOwnerInfo(new EntitySearchResult.EntityOwnerInfo(owner.getId(), owner.getName()));
  186 + }
  187 + }
  188 +
  189 + private Tenant getTenant(TenantId tenantId, Map<EntityId, ContactBased<? extends EntityId>> localOwnersCache) {
  190 + return (Tenant) localOwnersCache.computeIfAbsent(tenantId, id -> tenantService.findTenantById(tenantId));
  191 + }
  192 +
  193 + private Customer getCustomer(CustomerId customerId, Map<EntityId, ContactBased<? extends EntityId>> localOwnersCache) {
  194 + return (Customer) localOwnersCache.computeIfAbsent(customerId, id -> customerService.findCustomerById(TenantId.SYS_TENANT_ID, customerId));
  195 + }
  196 +
  197 + private UUID toUuid(String uuid) {
  198 + try {
  199 + UUID id = UUID.fromString(uuid);
  200 + if (!id.equals(EntityId.NULL_UUID)) {
  201 + return id;
  202 + }
  203 + } catch (Exception ignored) {}
  204 +
  205 + return null;
  206 + }
  207 +
  208 +}
... ...
... ... @@ -69,5 +69,6 @@ public interface PermissionChecker<I extends EntityId, T extends HasTenantId> {
69 69 }
70 70 };
71 71
  72 + PermissionChecker allowReadPermissionChecker = new GenericPermissionChecker(Operation.READ, Operation.READ_TELEMETRY, Operation.READ_ATTRIBUTES);
72 73
73 74 }
... ...
... ... @@ -17,10 +17,7 @@ package org.thingsboard.server.service.security.permission;
17 17
18 18 import org.springframework.stereotype.Component;
19 19 import org.thingsboard.server.common.data.HasTenantId;
20   -import org.thingsboard.server.common.data.User;
21 20 import org.thingsboard.server.common.data.id.EntityId;
22   -import org.thingsboard.server.common.data.id.UserId;
23   -import org.thingsboard.server.common.data.security.Authority;
24 21 import org.thingsboard.server.service.security.model.SecurityUser;
25 22
26 23 @Component(value="sysAdminPermissions")
... ... @@ -29,23 +26,29 @@ public class SysAdminPermissions extends AbstractPermissions {
29 26 public SysAdminPermissions() {
30 27 super();
31 28 put(Resource.ADMIN_SETTINGS, PermissionChecker.allowAllPermissionChecker);
32   - put(Resource.DASHBOARD, new PermissionChecker.GenericPermissionChecker(Operation.READ));
  29 + put(Resource.DASHBOARD, PermissionChecker.allowReadPermissionChecker);
33 30 put(Resource.TENANT, PermissionChecker.allowAllPermissionChecker);
34   - put(Resource.RULE_CHAIN, systemEntityPermissionChecker);
35   - put(Resource.USER, userPermissionChecker);
  31 + put(Resource.RULE_CHAIN, PermissionChecker.allowReadPermissionChecker);
  32 + put(Resource.USER, PermissionChecker.allowAllPermissionChecker);
36 33 put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker);
37 34 put(Resource.WIDGET_TYPE, systemEntityPermissionChecker);
38 35 put(Resource.OAUTH2_CONFIGURATION_INFO, PermissionChecker.allowAllPermissionChecker);
39 36 put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker);
40 37 put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker);
41   - put(Resource.TB_RESOURCE, systemEntityPermissionChecker);
  38 + put(Resource.TB_RESOURCE, PermissionChecker.allowAllPermissionChecker);
  39 + put(Resource.CUSTOMER, PermissionChecker.allowReadPermissionChecker);
  40 + put(Resource.ASSET, PermissionChecker.allowReadPermissionChecker);
  41 + put(Resource.DEVICE, PermissionChecker.allowReadPermissionChecker);
  42 + put(Resource.ENTITY_VIEW, PermissionChecker.allowReadPermissionChecker);
  43 + put(Resource.DEVICE_PROFILE, PermissionChecker.allowReadPermissionChecker);
  44 + put(Resource.OTA_PACKAGE, PermissionChecker.allowReadPermissionChecker);
  45 + put(Resource.EDGE, PermissionChecker.allowReadPermissionChecker);
42 46 }
43 47
44 48 private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() {
45 49
46 50 @Override
47 51 public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) {
48   -
49 52 if (entity.getTenantId() != null && !entity.getTenantId().isNullUid()) {
50 53 return false;
51 54 }
... ... @@ -53,16 +56,4 @@ public class SysAdminPermissions extends AbstractPermissions {
53 56 }
54 57 };
55 58
56   - private static final PermissionChecker userPermissionChecker = new PermissionChecker<UserId, User>() {
57   -
58   - @Override
59   - public boolean hasPermission(SecurityUser user, Operation operation, UserId userId, User userEntity) {
60   - if (Authority.CUSTOMER_USER.equals(userEntity.getAuthority())) {
61   - return false;
62   - }
63   - return true;
64   - }
65   -
66   - };
67   -
68 59 }
... ...
... ... @@ -29,7 +29,8 @@ public enum EntityFilterType {
29 29 DEVICE_SEARCH_QUERY("deviceSearchQuery"),
30 30 ENTITY_VIEW_SEARCH_QUERY("entityViewSearchQuery"),
31 31 EDGE_SEARCH_QUERY("edgeSearchQuery"),
32   - API_USAGE_STATE("apiUsageState");
  32 + API_USAGE_STATE("apiUsageState"),
  33 + ENTITY_NAME_OR_ID("entityNameOrId");
33 34
34 35 private final String label;
35 36
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.query;
  17 +
  18 +import lombok.Data;
  19 +import lombok.EqualsAndHashCode;
  20 +import org.thingsboard.server.common.data.EntityType;
  21 +
  22 +@EqualsAndHashCode(callSuper = true)
  23 +@Data
  24 +public class EntityNameOrIdFilter extends EntitySearchQueryFilter {
  25 + private String nameOrId;
  26 + private EntityType entityType;
  27 +
  28 + @Override
  29 + public EntityFilterType getType() {
  30 + return EntityFilterType.ENTITY_NAME_OR_ID;
  31 + }
  32 +}
... ...
... ... @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.query.EntityFilterType;
44 44 import org.thingsboard.server.common.data.query.EntityKeyType;
45 45 import org.thingsboard.server.common.data.query.EntityListFilter;
46 46 import org.thingsboard.server.common.data.query.EntityNameFilter;
  47 +import org.thingsboard.server.common.data.query.EntityNameOrIdFilter;
47 48 import org.thingsboard.server.common.data.query.EntitySearchQueryFilter;
48 49 import org.thingsboard.server.common.data.query.EntityTypeFilter;
49 50 import org.thingsboard.server.common.data.query.EntityViewSearchQueryFilter;
... ... @@ -236,6 +237,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
236 237 entityTableMap.put(EntityType.TENANT, "tenant");
237 238 entityTableMap.put(EntityType.API_USAGE_STATE, SELECT_API_USAGE_STATE);
238 239 entityTableMap.put(EntityType.EDGE, "edge");
  240 + entityTableMap.put(EntityType.RULE_CHAIN, "rule_chain");
  241 + entityTableMap.put(EntityType.WIDGETS_BUNDLE, "widgets_bundle");
  242 + entityTableMap.put(EntityType.TENANT_PROFILE, "tenant_profile");
  243 + entityTableMap.put(EntityType.DEVICE_PROFILE, "device_profile");
  244 + entityTableMap.put(EntityType.TB_RESOURCE, "resource");
  245 + entityTableMap.put(EntityType.OTA_PACKAGE, "ota_package");
239 246 }
240 247
241 248 public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{
... ... @@ -441,7 +448,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
441 448 Optional<EntityKeyMapping> sortOrderMappingOpt = mappings.stream().filter(EntityKeyMapping::isSortOrder).findFirst();
442 449 if (sortOrderMappingOpt.isPresent()) {
443 450 EntityKeyMapping sortOrderMapping = sortOrderMappingOpt.get();
444   - String direction = sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC ? "asc" : "desc";
  451 + String direction = sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC ? "asc" : "desc nulls last";
445 452 if (sortOrderMapping.getEntityKey().getType() == EntityKeyType.ENTITY_FIELD) {
446 453 dataQuery = String.format("%s order by %s %s, result.id %s", dataQuery, sortOrderMapping.getValueAlias(), direction, direction);
447 454 } else {
... ... @@ -471,15 +478,26 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
471 478 String entityFieldsQuery = EntityKeyMapping.buildQuery(ctx, entityFieldsFilters, entityFilter.getType());
472 479 String result = permissionQuery;
473 480 if (!entityFilterQuery.isEmpty()) {
474   - result += " and (" + entityFilterQuery + ")";
  481 + if (!result.isEmpty()) {
  482 + result += " and (" + entityFilterQuery + ")";
  483 + } else {
  484 + result = "(" + entityFilterQuery + ")";
  485 + }
475 486 }
476 487 if (!entityFieldsQuery.isEmpty()) {
477   - result += " and (" + entityFieldsQuery + ")";
  488 + if (!result.isEmpty()) {
  489 + result += " and (" + entityFieldsQuery + ")";
  490 + } else {
  491 + result = "(" + entityFieldsQuery + ")";
  492 + }
478 493 }
479 494 return result;
480 495 }
481 496
482 497 private String buildPermissionQuery(QueryContext ctx, EntityFilter entityFilter) {
  498 + if (ctx.getTenantId().equals(TenantId.SYS_TENANT_ID)) {
  499 + return "";
  500 + }
483 501 switch (entityFilter.getType()) {
484 502 case RELATIONS_QUERY:
485 503 case DEVICE_SEARCH_QUERY:
... ... @@ -548,6 +566,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
548 566 case API_USAGE_STATE:
549 567 case ENTITY_TYPE:
550 568 return "";
  569 + case ENTITY_NAME_OR_ID:
  570 + return entityNameOrIdQuery(ctx, (EntityNameOrIdFilter) entityFilter);
551 571 default:
552 572 throw new RuntimeException("Not implemented!");
553 573 }
... ... @@ -759,7 +779,27 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
759 779
760 780 private String entityNameQuery(QueryContext ctx, EntityNameFilter filter) {
761 781 ctx.addStringParameter("entity_filter_name_filter", filter.getEntityNameFilter());
762   - return "lower(e.search_text) like lower(concat(:entity_filter_name_filter, '%%'))";
  782 + return "lower(e.search_text) like lower(concat('%', :entity_filter_name_filter, '%'))";
  783 + }
  784 +
  785 + private String entityNameOrIdQuery(QueryContext ctx, EntityNameOrIdFilter filter) {
  786 + String nameOrId = filter.getNameOrId();
  787 + if (StringUtils.isNotEmpty(nameOrId)) {
  788 + nameOrId = nameOrId.replaceAll("%", "\\\\%").replaceAll("_", "\\\\_");
  789 + ctx.addStringParameter("entity_id_or_search_text_filter", nameOrId);
  790 + String query = "";
  791 +
  792 + String searchTextField = EntityKeyMapping.searchTextFields.get(filter.getEntityType());
  793 + query += "lower(e." + searchTextField + ") like lower(concat('%', :entity_id_or_search_text_filter, '%'))";
  794 +
  795 + try {
  796 + UUID.fromString(nameOrId);
  797 + query += " or e.id = :entity_id_or_search_text_filter::uuid";
  798 + } catch (Exception ignored) {}
  799 +
  800 + return query;
  801 + }
  802 + return "true";
763 803 }
764 804
765 805 private String typeQuery(QueryContext ctx, EntityFilter filter) {
... ... @@ -787,7 +827,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
787 827 }
788 828 ctx.addStringParameter("entity_filter_type_query_type", type);
789 829 ctx.addStringParameter("entity_filter_type_query_name", name);
790   - return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))";
  830 + return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(concat('%', :entity_filter_type_query_name, '%'))";
791 831 }
792 832
793 833 private EntityType resolveEntityType(EntityFilter entityFilter) {
... ... @@ -816,6 +856,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
816 856 return ((RelationsQueryFilter) entityFilter).getRootEntity().getEntityType();
817 857 case API_USAGE_STATE:
818 858 return EntityType.API_USAGE_STATE;
  859 + case ENTITY_NAME_OR_ID:
  860 + return ((EntityNameOrIdFilter) entityFilter).getEntityType();
819 861 default:
820 862 throw new RuntimeException("Not implemented!");
821 863 }
... ...
... ... @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.model.ModelConstants;
38 38 import java.util.ArrayList;
39 39 import java.util.Arrays;
40 40 import java.util.Collections;
  41 +import java.util.EnumMap;
41 42 import java.util.HashMap;
42 43 import java.util.HashSet;
43 44 import java.util.List;
... ... @@ -53,6 +54,8 @@ public class EntityKeyMapping {
53 54 private static final Map<EntityType, Set<String>> allowedEntityFieldMap = new HashMap<>();
54 55 private static final Map<String, String> entityFieldColumnMap = new HashMap<>();
55 56 private static final Map<EntityType, Map<String, String>> aliases = new HashMap<>();
  57 + private static final Map<EntityType, Map<String, String>> propertiesFunctions = new EnumMap<>(EntityType.class);
  58 + public static final Map<EntityType, String> searchTextFields = new EnumMap<>(EntityType.class);
56 59
57 60 public static final String CREATED_TIME = "createdTime";
58 61 public static final String ENTITY_TYPE = "entityType";
... ... @@ -72,35 +75,51 @@ public class EntityKeyMapping {
72 75 public static final String ZIP = "zip";
73 76 public static final String PHONE = "phone";
74 77 public static final String ADDITIONAL_INFO = "additionalInfo";
  78 + public static final String TENANT_ID = "tenantId";
  79 + public static final String CUSTOMER_ID = "customerId";
  80 + public static final String AUTHORITY = "authority";
  81 + public static final String RESOURCE_TYPE = "resourceType";
  82 + public static final String LAST_ACTIVITY_TIME = "lastActivityTime";
75 83
76   - public static final List<String> typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO);
77   - public static final List<String> widgetEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME);
  84 + public static final List<String> typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO, TENANT_ID);
78 85 public static final List<String> commonEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, ADDITIONAL_INFO);
79   - public static final List<String> dashboardEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, TITLE);
80   - public static final List<String> labeledEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, ADDITIONAL_INFO);
  86 +
  87 + public static final List<String> dashboardEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, TITLE, TENANT_ID);
  88 + public static final List<String> labeledEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, ADDITIONAL_INFO, TENANT_ID, CUSTOMER_ID);
81 89 public static final List<String> contactBasedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, EMAIL, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO);
82 90
83   - public static final Set<String> apiUsageStateEntityFields = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME));
  91 + public static final Set<String> apiUsageStateEntityFields = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME));
84 92 public static final Set<String> commonEntityFieldsSet = new HashSet<>(commonEntityFields);
85 93 public static final Set<String> 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));
86 94
87 95 static {
88 96 allowedEntityFieldMap.put(EntityType.DEVICE, new HashSet<>(labeledEntityFields));
89 97 allowedEntityFieldMap.put(EntityType.ASSET, new HashSet<>(labeledEntityFields));
  98 + allowedEntityFieldMap.put(EntityType.EDGE, new HashSet<>(labeledEntityFields));
90 99 allowedEntityFieldMap.put(EntityType.ENTITY_VIEW, new HashSet<>(typedEntityFields));
  100 + allowedEntityFieldMap.get(EntityType.ENTITY_VIEW).add(CUSTOMER_ID);
91 101
92 102 allowedEntityFieldMap.put(EntityType.TENANT, new HashSet<>(contactBasedEntityFields));
93 103 allowedEntityFieldMap.get(EntityType.TENANT).add(REGION);
94 104 allowedEntityFieldMap.put(EntityType.CUSTOMER, new HashSet<>(contactBasedEntityFields));
  105 + allowedEntityFieldMap.get(EntityType.CUSTOMER).add(TENANT_ID);
  106 +
  107 + allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(CREATED_TIME, FIRST_NAME, LAST_NAME, EMAIL,
  108 + ADDITIONAL_INFO, AUTHORITY, TENANT_ID, CUSTOMER_ID)));
95 109
96   - allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(CREATED_TIME, FIRST_NAME, LAST_NAME, EMAIL, ADDITIONAL_INFO)));
  110 + allowedEntityFieldMap.put(EntityType.DEVICE_PROFILE, new HashSet<>(commonEntityFields));
  111 + allowedEntityFieldMap.get(EntityType.DEVICE_PROFILE).add(TENANT_ID);
97 112
98 113 allowedEntityFieldMap.put(EntityType.DASHBOARD, new HashSet<>(dashboardEntityFields));
99 114 allowedEntityFieldMap.put(EntityType.RULE_CHAIN, new HashSet<>(commonEntityFields));
  115 + allowedEntityFieldMap.get(EntityType.RULE_CHAIN).add(TENANT_ID);
  116 + allowedEntityFieldMap.get(EntityType.RULE_CHAIN).add(TYPE);
100 117 allowedEntityFieldMap.put(EntityType.RULE_NODE, new HashSet<>(commonEntityFields));
101   - allowedEntityFieldMap.put(EntityType.WIDGET_TYPE, new HashSet<>(widgetEntityFields));
102   - allowedEntityFieldMap.put(EntityType.WIDGETS_BUNDLE, new HashSet<>(widgetEntityFields));
  118 + allowedEntityFieldMap.put(EntityType.WIDGET_TYPE, new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TENANT_ID)));
  119 + allowedEntityFieldMap.put(EntityType.WIDGETS_BUNDLE, new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, TITLE, TENANT_ID)));
103 120 allowedEntityFieldMap.put(EntityType.API_USAGE_STATE, apiUsageStateEntityFields);
  121 + allowedEntityFieldMap.put(EntityType.TB_RESOURCE, Set.of(CREATED_TIME, ENTITY_TYPE, RESOURCE_TYPE, TITLE, TENANT_ID));
  122 + allowedEntityFieldMap.put(EntityType.OTA_PACKAGE, Set.of(CREATED_TIME, ENTITY_TYPE, TYPE, TITLE, TENANT_ID));
104 123
105 124 entityFieldColumnMap.put(CREATED_TIME, ModelConstants.CREATED_TIME_PROPERTY);
106 125 entityFieldColumnMap.put(ENTITY_TYPE, ModelConstants.ENTITY_TYPE_PROPERTY);
... ... @@ -120,25 +139,42 @@ public class EntityKeyMapping {
120 139 entityFieldColumnMap.put(ZIP, ModelConstants.ZIP_PROPERTY);
121 140 entityFieldColumnMap.put(PHONE, ModelConstants.PHONE_PROPERTY);
122 141 entityFieldColumnMap.put(ADDITIONAL_INFO, ModelConstants.ADDITIONAL_INFO_PROPERTY);
  142 + entityFieldColumnMap.put(TENANT_ID, ModelConstants.TENANT_ID_PROPERTY);
  143 + entityFieldColumnMap.put(CUSTOMER_ID, ModelConstants.CUSTOMER_ID_PROPERTY);
  144 + entityFieldColumnMap.put(AUTHORITY, ModelConstants.USER_AUTHORITY_PROPERTY);
  145 + entityFieldColumnMap.put(RESOURCE_TYPE, ModelConstants.RESOURCE_TYPE_COLUMN);
123 146
124 147 Map<String, String> contactBasedAliases = new HashMap<>();
125 148 contactBasedAliases.put(NAME, TITLE);
126 149 contactBasedAliases.put(LABEL, TITLE);
127 150 aliases.put(EntityType.TENANT, contactBasedAliases);
128   - aliases.put(EntityType.CUSTOMER, contactBasedAliases);
  151 + aliases.put(EntityType.CUSTOMER, new HashMap<>(contactBasedAliases));
129 152 aliases.put(EntityType.DASHBOARD, contactBasedAliases);
130 153 Map<String, String> commonEntityAliases = new HashMap<>();
131 154 commonEntityAliases.put(TITLE, NAME);
132 155 aliases.put(EntityType.DEVICE, commonEntityAliases);
133 156 aliases.put(EntityType.ASSET, commonEntityAliases);
134 157 aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases);
135   - aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases);
  158 + aliases.put(EntityType.EDGE, commonEntityAliases);
  159 + aliases.put(EntityType.WIDGETS_BUNDLE, new HashMap<>(commonEntityAliases));
  160 + aliases.get(EntityType.WIDGETS_BUNDLE).put(NAME, TITLE);
136 161
137 162 Map<String, String> userEntityAliases = new HashMap<>();
138 163 userEntityAliases.put(TITLE, EMAIL);
139 164 userEntityAliases.put(LABEL, EMAIL);
140 165 userEntityAliases.put(NAME, EMAIL);
  166 + userEntityAliases.put(TYPE, AUTHORITY);
141 167 aliases.put(EntityType.USER, userEntityAliases);
  168 + aliases.put(EntityType.TB_RESOURCE, Map.of(NAME, TITLE, TYPE, RESOURCE_TYPE));
  169 + aliases.put(EntityType.OTA_PACKAGE, Map.of(NAME, TITLE));
  170 +
  171 + propertiesFunctions.put(EntityType.USER, Map.of(
  172 + LAST_ACTIVITY_TIME, "cast(e.additional_info::json ->> 'lastLoginTs' as bigint)"
  173 + ));
  174 +
  175 + Arrays.stream(EntityType.values()).forEach(entityType -> {
  176 + searchTextFields.put(entityType, ModelConstants.SEARCH_TEXT_PROPERTY);
  177 + });
142 178 }
143 179
144 180 private int index;
... ... @@ -179,6 +215,10 @@ public class EntityKeyMapping {
179 215 String column = entityFieldColumnMap.get(alias);
180 216 return String.format("cast(e.%s as varchar) as %s", column, getValueAlias());
181 217 } else {
  218 + Map<String, String> entityPropertiesFunctions = propertiesFunctions.get(entityType);
  219 + if (entityPropertiesFunctions != null && entityPropertiesFunctions.containsKey(alias)) {
  220 + return String.format("%s as %s", entityPropertiesFunctions.get(alias), getValueAlias());
  221 + }
182 222 return String.format("'' as %s", getValueAlias());
183 223 }
184 224 }
... ...