Commit 7446e012a6d8158fbf3e09d49c8f2f60d2eddec8

Authored by Igor Kulikov
1 parent c6f7862c

Introduce Current user alias

... ... @@ -246,6 +246,28 @@ public class UserController extends BaseController {
246 246 }
247 247 }
248 248
  249 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
  250 + @RequestMapping(value = "/users", params = {"pageSize", "page"}, method = RequestMethod.GET)
  251 + @ResponseBody
  252 + public PageData<User> getUsers(
  253 + @RequestParam int pageSize,
  254 + @RequestParam int page,
  255 + @RequestParam(required = false) String textSearch,
  256 + @RequestParam(required = false) String sortProperty,
  257 + @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  258 + try {
  259 + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
  260 + SecurityUser currentUser = getCurrentUser();
  261 + if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) {
  262 + return checkNotNull(userService.findUsersByTenantId(currentUser.getTenantId(), pageLink));
  263 + } else {
  264 + return checkNotNull(userService.findCustomerUsers(currentUser.getTenantId(), currentUser.getCustomerId(), pageLink));
  265 + }
  266 + } catch (Exception e) {
  267 + throw handleException(e);
  268 + }
  269 + }
  270 +
249 271 @PreAuthorize("hasAuthority('SYS_ADMIN')")
250 272 @RequestMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET)
251 273 @ResponseBody
... ...
... ... @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.Customer;
29 29 import org.thingsboard.server.common.data.Device;
30 30 import org.thingsboard.server.common.data.EntityView;
31 31 import org.thingsboard.server.common.data.Tenant;
  32 +import org.thingsboard.server.common.data.User;
32 33 import org.thingsboard.server.common.data.asset.Asset;
33 34 import org.thingsboard.server.common.data.exception.ThingsboardException;
34 35 import org.thingsboard.server.common.data.id.AssetId;
... ... @@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.id.EntityViewId;
40 41 import org.thingsboard.server.common.data.id.RuleChainId;
41 42 import org.thingsboard.server.common.data.id.RuleNodeId;
42 43 import org.thingsboard.server.common.data.id.TenantId;
  44 +import org.thingsboard.server.common.data.id.UserId;
43 45 import org.thingsboard.server.common.data.rule.RuleChain;
44 46 import org.thingsboard.server.common.data.rule.RuleNode;
45 47 import org.thingsboard.server.controller.HttpValidationCallback;
... ... @@ -172,6 +174,9 @@ public class AccessValidator {
172 174 case TENANT:
173 175 validateTenant(currentUser, operation, entityId, callback);
174 176 return;
  177 + case USER:
  178 + validateUser(currentUser, operation, entityId, callback);
  179 + return;
175 180 case ENTITY_VIEW:
176 181 validateEntityView(currentUser, operation, entityId, callback);
177 182 return;
... ... @@ -308,6 +313,22 @@ public class AccessValidator {
308 313 }
309 314 }
310 315
  316 + private void validateUser(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
  317 + ListenableFuture<User> userFuture = userService.findUserByIdAsync(currentUser.getTenantId(), new UserId(entityId.getId()));
  318 + Futures.addCallback(userFuture, getCallback(callback, user -> {
  319 + if (user == null) {
  320 + return ValidationResult.entityNotFound("User with requested id wasn't found!");
  321 + }
  322 + try {
  323 + accessControlService.checkPermission(currentUser, Resource.USER, operation, entityId, user);
  324 + } catch (ThingsboardException e) {
  325 + return ValidationResult.accessDenied(e.getMessage());
  326 + }
  327 + return ValidationResult.ok(user);
  328 +
  329 + }), executor);
  330 + }
  331 +
311 332 private void validateEntityView(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
312 333 if (currentUser.isSystemAdmin()) {
313 334 callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
... ...
... ... @@ -52,8 +52,10 @@ public interface UserService {
52 52 UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
53 53
54 54 void deleteUser(TenantId tenantId, UserId userId);
55   -
56   - PageData<User> findTenantAdmins(TenantId tenantId, PageLink pageLink);
  55 +
  56 + PageData<User> findUsersByTenantId(TenantId tenantId, PageLink pageLink);
  57 +
  58 + PageData<User> findTenantAdmins(TenantId tenantId, PageLink pageLink);
57 59
58 60 void deleteTenantAdmins(TenantId tenantId);
59 61
... ...
... ... @@ -86,7 +86,7 @@ public class EntityKeyMapping {
86 86 allowedEntityFieldMap.get(EntityType.TENANT).add(REGION);
87 87 allowedEntityFieldMap.put(EntityType.CUSTOMER, new HashSet<>(contactBasedEntityFields));
88 88
89   - allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(FIRST_NAME, LAST_NAME, EMAIL)));
  89 + allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(CREATED_TIME, FIRST_NAME, LAST_NAME, EMAIL)));
90 90
91 91 allowedEntityFieldMap.put(EntityType.DASHBOARD, new HashSet<>(commonEntityFields));
92 92 allowedEntityFieldMap.put(EntityType.RULE_CHAIN, new HashSet<>(commonEntityFields));
... ... @@ -377,27 +377,29 @@ public class EntityKeyMapping {
377 377 }
378 378
379 379 private String buildSimplePredicateQuery(EntityQueryContext ctx, String alias, EntityKey key, KeyFilterPredicate predicate) {
380   - if (predicate.getType().equals(FilterPredicateType.NUMERIC)) {
381   - if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) {
382   - String column = entityFieldColumnMap.get(key.getKey());
383   - return this.buildNumericPredicateQuery(ctx, alias + "." + column, (NumericFilterPredicate) predicate);
  380 + if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) {
  381 + String column = entityFieldColumnMap.get(key.getKey());
  382 + String field = alias + "." + column;
  383 + if (predicate.getType().equals(FilterPredicateType.NUMERIC)) {
  384 + return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate);
  385 + } else if (predicate.getType().equals(FilterPredicateType.STRING)) {
  386 + return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate);
384 387 } else {
  388 + return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate);
  389 + }
  390 + } else {
  391 + if (predicate.getType().equals(FilterPredicateType.NUMERIC)) {
385 392 String longQuery = this.buildNumericPredicateQuery(ctx, alias + ".long_v", (NumericFilterPredicate) predicate);
386 393 String doubleQuery = this.buildNumericPredicateQuery(ctx, alias + ".dbl_v", (NumericFilterPredicate) predicate);
387 394 return String.format("(%s or %s)", longQuery, doubleQuery);
388   - }
389   - } else {
390   - String column;
391   - if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) {
392   - column = entityFieldColumnMap.get(key.getKey());
393 395 } else {
394   - column = predicate.getType().equals(FilterPredicateType.STRING) ? "str_v" : "bool_v";
395   - }
396   - String field = alias + "." + column;
397   - if (predicate.getType().equals(FilterPredicateType.STRING)) {
398   - return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate);
399   - } else {
400   - return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate);
  396 + String column = predicate.getType().equals(FilterPredicateType.STRING) ? "str_v" : "bool_v";
  397 + String field = alias + "." + column;
  398 + if (predicate.getType().equals(FilterPredicateType.STRING)) {
  399 + return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate);
  400 + } else {
  401 + return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate);
  402 + }
401 403 }
402 404 }
403 405 }
... ...
... ... @@ -60,6 +60,16 @@ public class JpaUserDao extends JpaAbstractSearchTextDao<UserEntity, User> imple
60 60 }
61 61
62 62 @Override
  63 + public PageData<User> findByTenantId(UUID tenantId, PageLink pageLink) {
  64 + return DaoUtil.toPageData(
  65 + userRepository
  66 + .findByTenantId(
  67 + tenantId,
  68 + Objects.toString(pageLink.getTextSearch(), ""),
  69 + DaoUtil.toPageable(pageLink)));
  70 + }
  71 +
  72 + @Override
63 73 public PageData<User> findTenantAdmins(UUID tenantId, PageLink pageLink) {
64 74 return DaoUtil.toPageData(
65 75 userRepository
... ...
... ... @@ -43,4 +43,10 @@ public interface UserRepository extends PagingAndSortingRepository<UserEntity, U
43 43 @Param("authority") Authority authority,
44 44 Pageable pageable);
45 45
  46 + @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " +
  47 + "AND LOWER(u.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
  48 + Page<UserEntity> findByTenantId(@Param("tenantId") UUID tenantId,
  49 + @Param("searchText") String searchText,
  50 + Pageable pageable);
  51 +
46 52 }
... ...
... ... @@ -40,6 +40,15 @@ public interface UserDao extends Dao<User> {
40 40 * @return the user entity
41 41 */
42 42 User findByEmail(TenantId tenantId, String email);
  43 +
  44 + /**
  45 + * Find users by tenantId and page link.
  46 + *
  47 + * @param tenantId the tenantId
  48 + * @param pageLink the page link
  49 + * @return the list of user entities
  50 + */
  51 + PageData<User> findByTenantId(UUID tenantId, PageLink pageLink);
43 52
44 53 /**
45 54 * Find tenant admin users by tenantId and page link.
... ...
... ... @@ -220,6 +220,14 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
220 220 }
221 221
222 222 @Override
  223 + public PageData<User> findUsersByTenantId(TenantId tenantId, PageLink pageLink) {
  224 + log.trace("Executing findUsersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
  225 + validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
  226 + validatePageLink(pageLink);
  227 + return userDao.findByTenantId(tenantId.getId(), pageLink);
  228 + }
  229 +
  230 + @Override
223 231 public PageData<User> findTenantAdmins(TenantId tenantId, PageLink pageLink) {
224 232 log.trace("Executing findTenantAdmins, tenantId [{}], pageLink [{}]", tenantId, pageLink);
225 233 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
... ...
... ... @@ -310,7 +310,8 @@ export class EntityService {
310 310 }
311 311 break;
312 312 case EntityType.USER:
313   - console.error('Get User Entities is not implemented!');
  313 + pageLink.sortOrder.property = 'email';
  314 + entitiesObservable = this.userService.getUsers(pageLink);
314 315 break;
315 316 case EntityType.ALARM:
316 317 console.error('Get Alarm Entities is not implemented!');
... ... @@ -548,6 +549,7 @@ export class EntityService {
548 549 entityTypes.push(EntityType.ENTITY_VIEW);
549 550 entityTypes.push(EntityType.TENANT);
550 551 entityTypes.push(EntityType.CUSTOMER);
  552 + entityTypes.push(EntityType.USER);
551 553 entityTypes.push(EntityType.DASHBOARD);
552 554 if (useAliasEntityTypes) {
553 555 entityTypes.push(AliasEntityType.CURRENT_CUSTOMER);
... ... @@ -559,12 +561,16 @@ export class EntityService {
559 561 entityTypes.push(EntityType.ASSET);
560 562 entityTypes.push(EntityType.ENTITY_VIEW);
561 563 entityTypes.push(EntityType.CUSTOMER);
  564 + entityTypes.push(EntityType.USER);
562 565 entityTypes.push(EntityType.DASHBOARD);
563 566 if (useAliasEntityTypes) {
564 567 entityTypes.push(AliasEntityType.CURRENT_CUSTOMER);
565 568 }
566 569 break;
567 570 }
  571 + if (useAliasEntityTypes) {
  572 + entityTypes.push(AliasEntityType.CURRENT_USER);
  573 + }
568 574 if (allowedEntityTypes && allowedEntityTypes.length) {
569 575 for (let index = entityTypes.length - 1; index >= 0; index--) {
570 576 if (allowedEntityTypes.indexOf(entityTypes[index]) === -1) {
... ... @@ -961,6 +967,10 @@ export class EntityService {
961 967 const authUser = getCurrentAuthUser(this.store);
962 968 entityId.entityType = EntityType.TENANT;
963 969 entityId.id = authUser.tenantId;
  970 + } else if (entityType === AliasEntityType.CURRENT_USER){
  971 + const authUser = getCurrentAuthUser(this.store);
  972 + entityId.entityType = EntityType.USER;
  973 + entityId.id = authUser.userId;
964 974 }
965 975 return entityId;
966 976 }
... ...
... ... @@ -32,6 +32,12 @@ export class UserService {
32 32 private http: HttpClient
33 33 ) { }
34 34
  35 + public getUsers(pageLink: PageLink,
  36 + config?: RequestConfig): Observable<PageData<User>> {
  37 + return this.http.get<PageData<User>>(`/api/users${pageLink.toQuery()}`,
  38 + defaultHttpOptionsFromConfig(config));
  39 + }
  40 +
35 41 public getTenantAdmins(tenantId: string, pageLink: PageLink,
36 42 config?: RequestConfig): Observable<PageData<User>> {
37 43 return this.http.get<PageData<User>>(`/api/tenant/${tenantId}/users${pageLink.toQuery()}`,
... ...
... ... @@ -28,6 +28,7 @@ import {
28 28 AliasesEntitySelectPanelData
29 29 } from './aliases-entity-select-panel.component';
30 30 import { deepClone } from '@core/utils';
  31 +import { AliasFilterType } from '@shared/models/alias.models';
31 32
32 33 @Component({
33 34 selector: 'tb-aliases-entity-select',
... ... @@ -178,7 +179,7 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy {
178 179 for (const aliasId of Object.keys(allEntityAliases)) {
179 180 const aliasInfo = this.aliasController.getInstantAliasInfo(aliasId);
180 181 if (aliasInfo && !aliasInfo.resolveMultiple && aliasInfo.currentEntity
181   - && aliasInfo.entityFilter) {
  182 + && aliasInfo.entityFilter && aliasInfo.entityFilter.type !== AliasFilterType.singleEntity) {
182 183 this.entityAliasesInfo[aliasId] = deepClone(aliasInfo);
183 184 this.hasSelectableAliasEntities = true;
184 185 }
... ...
... ... @@ -84,6 +84,9 @@ import {
84 84 KeyFilter
85 85 } from '@shared/models/query/query.models';
86 86 import { sortItems } from '@shared/models/page/page-link';
  87 +import { entityFields } from '@shared/models/entity.models';
  88 +import { alarmFields } from '@shared/models/alarm.models';
  89 +import { DatePipe } from '@angular/common';
87 90
88 91 interface EntitiesTableWidgetSettings extends TableWidgetSettings {
89 92 entitiesTitle: string;
... ... @@ -153,6 +156,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
153 156 private overlay: Overlay,
154 157 private viewContainerRef: ViewContainerRef,
155 158 private utils: UtilsService,
  159 + private datePipe: DatePipe,
156 160 private translate: TranslateService,
157 161 private domSanitizer: DomSanitizer) {
158 162 super(store);
... ... @@ -511,9 +515,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
511 515 content = '' + value;
512 516 }
513 517 } else {
514   - const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals;
515   - const units = contentInfo.units || this.ctx.widgetConfig.units;
516   - content = this.ctx.utils.formatValue(value, decimals, units, true);
  518 + content = this.defaultContent(key, contentInfo, value);
517 519 }
518 520 return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : '';
519 521 } else {
... ... @@ -521,6 +523,22 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
521 523 }
522 524 }
523 525
  526 + private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any {
  527 + if (isDefined(value)) {
  528 + const entityField = entityFields[key.name];
  529 + if (entityField) {
  530 + if (entityField.time) {
  531 + return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss');
  532 + }
  533 + }
  534 + const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals;
  535 + const units = contentInfo.units || this.ctx.widgetConfig.units;
  536 + return this.ctx.utils.formatValue(value, decimals, units, true);
  537 + } else {
  538 + return '';
  539 + }
  540 + }
  541 +
524 542 public onRowClick($event: Event, entity: EntityData, isDouble?: boolean) {
525 543 if ($event) {
526 544 $event.stopPropagation();
... ...
... ... @@ -15,6 +15,23 @@
15 15 limitations under the License.
16 16
17 17 -->
  18 +<mat-tab *ngIf="entity"
  19 + label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
  20 + <tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
  21 + [active]="attributesTab.isActive"
  22 + [entityId]="entity.id"
  23 + [entityName]="entity.name">
  24 + </tb-attribute-table>
  25 +</mat-tab>
  26 +<mat-tab *ngIf="entity"
  27 + label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
  28 + <tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
  29 + disableAttributeScopeSelection
  30 + [active]="telemetryTab.isActive"
  31 + [entityId]="entity.id"
  32 + [entityName]="entity.name">
  33 + </tb-attribute-table>
  34 +</mat-tab>
18 35 <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
19 36 label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
20 37 <tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.USER" [userId]="entity.id" detailsMode="true"></tb-audit-log-table>
... ...
... ... @@ -175,6 +175,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
175 175 this.entityRequiredText = 'customer.customer-required';
176 176 break;
177 177 case EntityType.USER:
  178 + case AliasEntityType.CURRENT_USER:
178 179 this.entityText = 'user.user';
179 180 this.noEntitiesMatchingText = 'user.no-users-matching';
180 181 this.entityRequiredText = 'user.user-required';
... ... @@ -324,6 +325,8 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
324 325 return EntityType.CUSTOMER;
325 326 } else if (entityType === AliasEntityType.CURRENT_TENANT) {
326 327 return EntityType.TENANT;
  328 + } else if (entityType === AliasEntityType.CURRENT_USER) {
  329 + return EntityType.USER;
327 330 }
328 331 return entityType;
329 332 }
... ...
... ... @@ -27,7 +27,8 @@
27 27 </tb-entity-type-select>
28 28 <tb-entity-autocomplete
29 29 fxFlex
30   - *ngIf="modelValue.entityType && modelValue.entityType !== AliasEntityType.CURRENT_TENANT"
  30 + *ngIf="modelValue.entityType && modelValue.entityType !== AliasEntityType.CURRENT_TENANT
  31 + && modelValue.entityType !== AliasEntityType.CURRENT_USER"
31 32 [required]="required"
32 33 [entityType]="modelValue.entityType"
33 34 formControlName="entityId">
... ...
... ... @@ -97,7 +97,7 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
97 97 ngOnInit() {
98 98 this.entitySelectFormGroup.get('entityType').valueChanges.subscribe(
99 99 (value) => {
100   - if(value === AliasEntityType.CURRENT_TENANT){
  100 + if(value === AliasEntityType.CURRENT_TENANT || value === AliasEntityType.CURRENT_USER) {
101 101 this.modelValue.id = NULL_UUID;
102 102 }
103 103 this.updateView(value, this.modelValue.id);
... ... @@ -145,7 +145,9 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
145 145 entityType,
146 146 id: this.modelValue.entityType !== entityType ? null : entityId
147 147 };
148   - if (this.modelValue.entityType && (this.modelValue.id || this.modelValue.entityType === AliasEntityType.CURRENT_TENANT)) {
  148 + if (this.modelValue.entityType && (this.modelValue.id ||
  149 + this.modelValue.entityType === AliasEntityType.CURRENT_TENANT ||
  150 + this.modelValue.entityType === AliasEntityType.CURRENT_USER)) {
149 151 this.propagateChange(this.modelValue);
150 152 } else {
151 153 this.propagateChange(null);
... ...
... ... @@ -50,7 +50,8 @@ export enum EntityType {
50 50
51 51 export enum AliasEntityType {
52 52 CURRENT_CUSTOMER = 'CURRENT_CUSTOMER',
53   - CURRENT_TENANT = 'CURRENT_TENANT'
  53 + CURRENT_TENANT = 'CURRENT_TENANT',
  54 + CURRENT_USER = 'CURRENT_USER'
54 55 }
55 56
56 57 export interface EntityTypeTranslation {
... ... @@ -229,6 +230,13 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
229 230 type: 'entity.type-current-tenant',
230 231 list: 'entity.type-current-tenant'
231 232 }
  233 + ],
  234 + [
  235 + AliasEntityType.CURRENT_USER,
  236 + {
  237 + type: 'entity.type-current-user',
  238 + list: 'entity.type-current-user'
  239 + }
232 240 ]
233 241 ]
234 242 );
... ...
... ... @@ -841,6 +841,7 @@
841 841 "rulenode-name-starts-with": "Rule nodes whose names start with '{{prefix}}'",
842 842 "type-current-customer": "Current Customer",
843 843 "type-current-tenant": "Current Tenant",
  844 + "type-current-user": "Current User",
844 845 "search": "Search entities",
845 846 "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected",
846 847 "entity-name": "Entity name",
... ...