diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index d5398e8edb..fce56b509e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -246,6 +246,28 @@ public class UserController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/users", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getUsers( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + SecurityUser currentUser = getCurrentUser(); + if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { + return checkNotNull(userService.findUsersByTenantId(currentUser.getTenantId(), pageLink)); + } else { + return checkNotNull(userService.findCustomerUsers(currentUser.getTenantId(), currentUser.getCustomerId(), pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 9579561df6..b08ec2625b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; @@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.controller.HttpValidationCallback; @@ -172,6 +174,9 @@ public class AccessValidator { case TENANT: validateTenant(currentUser, operation, entityId, callback); return; + case USER: + validateUser(currentUser, operation, entityId, callback); + return; case ENTITY_VIEW: validateEntityView(currentUser, operation, entityId, callback); return; @@ -308,6 +313,22 @@ public class AccessValidator { } } + private void validateUser(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + ListenableFuture userFuture = userService.findUserByIdAsync(currentUser.getTenantId(), new UserId(entityId.getId())); + Futures.addCallback(userFuture, getCallback(callback, user -> { + if (user == null) { + return ValidationResult.entityNotFound("User with requested id wasn't found!"); + } + try { + accessControlService.checkPermission(currentUser, Resource.USER, operation, entityId, user); + } catch (ThingsboardException e) { + return ValidationResult.accessDenied(e.getMessage()); + } + return ValidationResult.ok(user); + + }), executor); + } + private void validateEntityView(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { if (currentUser.isSystemAdmin()) { callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index 1aa5a43547..494e233299 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -52,8 +52,10 @@ public interface UserService { UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); void deleteUser(TenantId tenantId, UserId userId); - - PageData findTenantAdmins(TenantId tenantId, PageLink pageLink); + + PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findTenantAdmins(TenantId tenantId, PageLink pageLink); void deleteTenantAdmins(TenantId tenantId); 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 4fcdf0f777..01e4a4edf3 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 @@ -86,7 +86,7 @@ public class EntityKeyMapping { allowedEntityFieldMap.get(EntityType.TENANT).add(REGION); allowedEntityFieldMap.put(EntityType.CUSTOMER, new HashSet<>(contactBasedEntityFields)); - allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(FIRST_NAME, LAST_NAME, EMAIL))); + allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(CREATED_TIME, FIRST_NAME, LAST_NAME, EMAIL))); allowedEntityFieldMap.put(EntityType.DASHBOARD, new HashSet<>(commonEntityFields)); allowedEntityFieldMap.put(EntityType.RULE_CHAIN, new HashSet<>(commonEntityFields)); @@ -377,28 +377,30 @@ public class EntityKeyMapping { } private String buildSimplePredicateQuery(EntityQueryContext ctx, String alias, EntityKey key, KeyFilterPredicate predicate) { - if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { - if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) { - String column = entityFieldColumnMap.get(key.getKey()); - return this.buildNumericPredicateQuery(ctx, alias + "." + column, (NumericFilterPredicate) predicate); - } else { - String longQuery = this.buildNumericPredicateQuery(ctx, alias + ".long_v", (NumericFilterPredicate) predicate); - String doubleQuery = this.buildNumericPredicateQuery(ctx, alias + ".dbl_v", (NumericFilterPredicate) predicate); - return String.format("(%s or %s)", longQuery, doubleQuery); - } - } else { - String column; - if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) { - column = entityFieldColumnMap.get(key.getKey()); - } else { - column = predicate.getType().equals(FilterPredicateType.STRING) ? "str_v" : "bool_v"; - } + if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) { + String column = entityFieldColumnMap.get(key.getKey()); String field = alias + "." + column; - if (predicate.getType().equals(FilterPredicateType.STRING)) { + if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { + return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate); + } else if (predicate.getType().equals(FilterPredicateType.STRING)) { return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate); } else { return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate); } + } else { + if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { + String longQuery = this.buildNumericPredicateQuery(ctx, alias + ".long_v", (NumericFilterPredicate) predicate); + String doubleQuery = this.buildNumericPredicateQuery(ctx, alias + ".dbl_v", (NumericFilterPredicate) predicate); + return String.format("(%s or %s)", longQuery, doubleQuery); + } else { + String column = predicate.getType().equals(FilterPredicateType.STRING) ? "str_v" : "bool_v"; + String field = alias + "." + column; + if (predicate.getType().equals(FilterPredicateType.STRING)) { + return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate); + } else { + return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate); + } + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 5108332bd6..b0805c1a52 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -59,6 +59,16 @@ public class JpaUserDao extends JpaAbstractSearchTextDao imple return DaoUtil.getData(userRepository.findByEmail(email)); } + @Override + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + userRepository + .findByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + @Override public PageData findTenantAdmins(UUID tenantId, PageLink pageLink) { return DaoUtil.toPageData( diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 265748c6b8..5bae8fbe68 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -43,4 +43,10 @@ public interface UserRepository extends PagingAndSortingRepository findByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index d212034ce7..12bebe9cec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -40,6 +40,15 @@ public interface UserDao extends Dao { * @return the user entity */ User findByEmail(TenantId tenantId, String email); + + /** + * Find users by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of user entities + */ + PageData findByTenantId(UUID tenantId, PageLink pageLink); /** * Find tenant admin users by tenantId and page link. diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 19d993b82c..4da1fb7028 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -219,6 +219,14 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userDao.removeById(tenantId, userId.getId()); } + @Override + public PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findUsersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return userDao.findByTenantId(tenantId.getId(), pageLink); + } + @Override public PageData findTenantAdmins(TenantId tenantId, PageLink pageLink) { log.trace("Executing findTenantAdmins, tenantId [{}], pageLink [{}]", tenantId, pageLink); diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index e0fe50fd3d..3040738c9a 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -310,7 +310,8 @@ export class EntityService { } break; case EntityType.USER: - console.error('Get User Entities is not implemented!'); + pageLink.sortOrder.property = 'email'; + entitiesObservable = this.userService.getUsers(pageLink); break; case EntityType.ALARM: console.error('Get Alarm Entities is not implemented!'); @@ -548,6 +549,7 @@ export class EntityService { entityTypes.push(EntityType.ENTITY_VIEW); entityTypes.push(EntityType.TENANT); entityTypes.push(EntityType.CUSTOMER); + entityTypes.push(EntityType.USER); entityTypes.push(EntityType.DASHBOARD); if (useAliasEntityTypes) { entityTypes.push(AliasEntityType.CURRENT_CUSTOMER); @@ -559,12 +561,16 @@ export class EntityService { entityTypes.push(EntityType.ASSET); entityTypes.push(EntityType.ENTITY_VIEW); entityTypes.push(EntityType.CUSTOMER); + entityTypes.push(EntityType.USER); entityTypes.push(EntityType.DASHBOARD); if (useAliasEntityTypes) { entityTypes.push(AliasEntityType.CURRENT_CUSTOMER); } break; } + if (useAliasEntityTypes) { + entityTypes.push(AliasEntityType.CURRENT_USER); + } if (allowedEntityTypes && allowedEntityTypes.length) { for (let index = entityTypes.length - 1; index >= 0; index--) { if (allowedEntityTypes.indexOf(entityTypes[index]) === -1) { @@ -961,6 +967,10 @@ export class EntityService { const authUser = getCurrentAuthUser(this.store); entityId.entityType = EntityType.TENANT; entityId.id = authUser.tenantId; + } else if (entityType === AliasEntityType.CURRENT_USER){ + const authUser = getCurrentAuthUser(this.store); + entityId.entityType = EntityType.USER; + entityId.id = authUser.userId; } return entityId; } diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts index 5beb8017f9..c84443a2df 100644 --- a/ui-ngx/src/app/core/http/user.service.ts +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -32,6 +32,12 @@ export class UserService { private http: HttpClient ) { } + public getUsers(pageLink: PageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/users${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + public getTenantAdmins(tenantId: string, pageLink: PageLink, config?: RequestConfig): Observable> { return this.http.get>(`/api/tenant/${tenantId}/users${pageLink.toQuery()}`, diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts index 2852450d7d..a9a7fc026a 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts @@ -28,6 +28,7 @@ import { AliasesEntitySelectPanelData } from './aliases-entity-select-panel.component'; import { deepClone } from '@core/utils'; +import { AliasFilterType } from '@shared/models/alias.models'; @Component({ selector: 'tb-aliases-entity-select', @@ -178,7 +179,7 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { for (const aliasId of Object.keys(allEntityAliases)) { const aliasInfo = this.aliasController.getInstantAliasInfo(aliasId); if (aliasInfo && !aliasInfo.resolveMultiple && aliasInfo.currentEntity - && aliasInfo.entityFilter) { + && aliasInfo.entityFilter && aliasInfo.entityFilter.type !== AliasFilterType.singleEntity) { this.entityAliasesInfo[aliasId] = deepClone(aliasInfo); this.hasSelectableAliasEntities = true; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 3656b8ee80..083852a077 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -84,6 +84,9 @@ import { KeyFilter } from '@shared/models/query/query.models'; import { sortItems } from '@shared/models/page/page-link'; +import { entityFields } from '@shared/models/entity.models'; +import { alarmFields } from '@shared/models/alarm.models'; +import { DatePipe } from '@angular/common'; interface EntitiesTableWidgetSettings extends TableWidgetSettings { entitiesTitle: string; @@ -153,6 +156,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni private overlay: Overlay, private viewContainerRef: ViewContainerRef, private utils: UtilsService, + private datePipe: DatePipe, private translate: TranslateService, private domSanitizer: DomSanitizer) { super(store); @@ -511,9 +515,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni content = '' + value; } } else { - const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; - const units = contentInfo.units || this.ctx.widgetConfig.units; - content = this.ctx.utils.formatValue(value, decimals, units, true); + content = this.defaultContent(key, contentInfo, value); } return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; } else { @@ -521,6 +523,22 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } } + private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { + if (isDefined(value)) { + const entityField = entityFields[key.name]; + if (entityField) { + if (entityField.time) { + return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss'); + } + } + const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; + const units = contentInfo.units || this.ctx.widgetConfig.units; + return this.ctx.utils.formatValue(value, decimals, units, true); + } else { + return ''; + } + } + public onRowClick($event: Event, entity: EntityData, isDouble?: boolean) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html index 997bfae799..9d5533c2f6 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html @@ -15,6 +15,23 @@ limitations under the License. --> + + + + + + + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 893d746afc..8d76118bcf 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -175,6 +175,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit this.entityRequiredText = 'customer.customer-required'; break; case EntityType.USER: + case AliasEntityType.CURRENT_USER: this.entityText = 'user.user'; this.noEntitiesMatchingText = 'user.no-users-matching'; this.entityRequiredText = 'user.user-required'; @@ -324,6 +325,8 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit return EntityType.CUSTOMER; } else if (entityType === AliasEntityType.CURRENT_TENANT) { return EntityType.TENANT; + } else if (entityType === AliasEntityType.CURRENT_USER) { + return EntityType.USER; } return entityType; } diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index ffc11e3735..8b75e1c00b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -27,7 +27,8 @@ diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index 670b105f9a..0383d83c00 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -97,7 +97,7 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte ngOnInit() { this.entitySelectFormGroup.get('entityType').valueChanges.subscribe( (value) => { - if(value === AliasEntityType.CURRENT_TENANT){ + if(value === AliasEntityType.CURRENT_TENANT || value === AliasEntityType.CURRENT_USER) { this.modelValue.id = NULL_UUID; } this.updateView(value, this.modelValue.id); @@ -145,7 +145,9 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte entityType, id: this.modelValue.entityType !== entityType ? null : entityId }; - if (this.modelValue.entityType && (this.modelValue.id || this.modelValue.entityType === AliasEntityType.CURRENT_TENANT)) { + if (this.modelValue.entityType && (this.modelValue.id || + this.modelValue.entityType === AliasEntityType.CURRENT_TENANT || + this.modelValue.entityType === AliasEntityType.CURRENT_USER)) { this.propagateChange(this.modelValue); } else { this.propagateChange(null); diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 83b9bcc1fe..05e240581d 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -50,7 +50,8 @@ export enum EntityType { export enum AliasEntityType { CURRENT_CUSTOMER = 'CURRENT_CUSTOMER', - CURRENT_TENANT = 'CURRENT_TENANT' + CURRENT_TENANT = 'CURRENT_TENANT', + CURRENT_USER = 'CURRENT_USER' } export interface EntityTypeTranslation { @@ -229,6 +230,13 @@ export const entityTypeTranslations = new Map