Commit 796294f33faf87ac31f14b320210600cabb89bc3

Authored by Igor Kulikov
1 parent 8efb250f

Login As User feature.

@@ -15,7 +15,12 @@ @@ -15,7 +15,12 @@
15 */ 15 */
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import com.fasterxml.jackson.databind.ObjectMapper;
  20 +import com.fasterxml.jackson.databind.node.ObjectNode;
  21 +import lombok.Getter;
18 import org.springframework.beans.factory.annotation.Autowired; 22 import org.springframework.beans.factory.annotation.Autowired;
  23 +import org.springframework.beans.factory.annotation.Value;
19 import org.springframework.http.HttpStatus; 24 import org.springframework.http.HttpStatus;
20 import org.springframework.security.access.prepost.PreAuthorize; 25 import org.springframework.security.access.prepost.PreAuthorize;
21 import org.springframework.web.bind.annotation.PathVariable; 26 import org.springframework.web.bind.annotation.PathVariable;
@@ -39,7 +44,11 @@ import org.thingsboard.server.common.data.page.TextPageData; @@ -39,7 +44,11 @@ import org.thingsboard.server.common.data.page.TextPageData;
39 import org.thingsboard.server.common.data.page.TextPageLink; 44 import org.thingsboard.server.common.data.page.TextPageLink;
40 import org.thingsboard.server.common.data.security.Authority; 45 import org.thingsboard.server.common.data.security.Authority;
41 import org.thingsboard.server.common.data.security.UserCredentials; 46 import org.thingsboard.server.common.data.security.UserCredentials;
  47 +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
42 import org.thingsboard.server.service.security.model.SecurityUser; 48 import org.thingsboard.server.service.security.model.SecurityUser;
  49 +import org.thingsboard.server.service.security.model.UserPrincipal;
  50 +import org.thingsboard.server.service.security.model.token.JwtToken;
  51 +import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
43 52
44 import javax.servlet.http.HttpServletRequest; 53 import javax.servlet.http.HttpServletRequest;
45 54
@@ -50,9 +59,21 @@ public class UserController extends BaseController { @@ -50,9 +59,21 @@ public class UserController extends BaseController {
50 public static final String USER_ID = "userId"; 59 public static final String USER_ID = "userId";
51 public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; 60 public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
52 public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s"; 61 public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s";
  62 +
  63 + @Value("${security.user_token_access_enabled}")
  64 + @Getter
  65 + private boolean userTokenAccessEnabled;
  66 +
53 @Autowired 67 @Autowired
54 private MailService mailService; 68 private MailService mailService;
55 69
  70 + @Autowired
  71 + private JwtTokenFactory tokenFactory;
  72 +
  73 + @Autowired
  74 + private RefreshTokenRepository refreshTokenRepository;
  75 +
  76 +
56 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") 77 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
57 @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) 78 @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET)
58 @ResponseBody 79 @ResponseBody
@@ -71,6 +92,42 @@ public class UserController extends BaseController { @@ -71,6 +92,42 @@ public class UserController extends BaseController {
71 } 92 }
72 } 93 }
73 94
  95 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
  96 + @RequestMapping(value = "/user/tokenAccessEnabled", method = RequestMethod.GET)
  97 + @ResponseBody
  98 + public boolean isUserTokenAccessEnabled() {
  99 + return userTokenAccessEnabled;
  100 + }
  101 +
  102 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
  103 + @RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET)
  104 + @ResponseBody
  105 + public JsonNode getUserToken(@PathVariable(USER_ID) String strUserId) throws ThingsboardException {
  106 + checkParameter(USER_ID, strUserId);
  107 + try {
  108 + UserId userId = new UserId(toUUID(strUserId));
  109 + SecurityUser authUser = getCurrentUser();
  110 + User user = userService.findUserById(userId);
  111 + if (!userTokenAccessEnabled || (authUser.getAuthority() == Authority.SYS_ADMIN && user.getAuthority() != Authority.TENANT_ADMIN)
  112 + || (authUser.getAuthority() == Authority.TENANT_ADMIN && !authUser.getTenantId().equals(user.getTenantId()))) {
  113 + throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
  114 + ThingsboardErrorCode.PERMISSION_DENIED);
  115 + }
  116 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
  117 + UserCredentials credentials = userService.findUserCredentialsByUserId(userId);
  118 + SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
  119 + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
  120 + JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
  121 + ObjectMapper objectMapper = new ObjectMapper();
  122 + ObjectNode tokenObject = objectMapper.createObjectNode();
  123 + tokenObject.put("token", accessToken.getToken());
  124 + tokenObject.put("refreshToken", refreshToken.getToken());
  125 + return tokenObject;
  126 + } catch (Exception e) {
  127 + throw handleException(e);
  128 + }
  129 + }
  130 +
74 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") 131 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
75 @RequestMapping(value = "/user", method = RequestMethod.POST) 132 @RequestMapping(value = "/user", method = RequestMethod.POST)
76 @ResponseBody 133 @ResponseBody
@@ -66,12 +66,16 @@ plugins: @@ -66,12 +66,16 @@ plugins:
66 # Comma seperated package list used during classpath scanning for plugins 66 # Comma seperated package list used during classpath scanning for plugins
67 scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}" 67 scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}"
68 68
69 -# JWT Token parameters  
70 -security.jwt:  
71 - tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins)  
72 - refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour)  
73 - tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"  
74 - tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" 69 +# Security parameters
  70 +security:
  71 + # JWT Token parameters
  72 + jwt:
  73 + tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins)
  74 + refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour)
  75 + tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
  76 + tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
  77 + # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator
  78 + user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}"
75 79
76 # Device communication protocol parameters 80 # Device communication protocol parameters
77 http: 81 http:
@@ -27,6 +27,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -27,6 +27,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
27 currentUserDetails = null, 27 currentUserDetails = null,
28 lastPublicDashboardId = null, 28 lastPublicDashboardId = null,
29 allowedDashboardIds = [], 29 allowedDashboardIds = [],
  30 + userTokenAccessEnabled = false,
30 userLoaded = false; 31 userLoaded = false;
31 32
32 var refreshTokenQueue = []; 33 var refreshTokenQueue = [];
@@ -59,7 +60,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -59,7 +60,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
59 forceDefaultPlace: forceDefaultPlace, 60 forceDefaultPlace: forceDefaultPlace,
60 updateLastPublicDashboardId: updateLastPublicDashboardId, 61 updateLastPublicDashboardId: updateLastPublicDashboardId,
61 logout: logout, 62 logout: logout,
62 - reloadUser: reloadUser 63 + reloadUser: reloadUser,
  64 + isUserTokenAccessEnabled: isUserTokenAccessEnabled,
  65 + loginAsUser: loginAsUser
63 } 66 }
64 67
65 reloadUser(); 68 reloadUser();
@@ -105,6 +108,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -105,6 +108,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
105 currentUser = null; 108 currentUser = null;
106 currentUserDetails = null; 109 currentUserDetails = null;
107 lastPublicDashboardId = null; 110 lastPublicDashboardId = null;
  111 + userTokenAccessEnabled = false;
108 allowedDashboardIds = []; 112 allowedDashboardIds = [];
109 if (!jwtToken) { 113 if (!jwtToken) {
110 clearTokenData(); 114 clearTokenData();
@@ -299,24 +303,36 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -299,24 +303,36 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
299 } else if (currentUser) { 303 } else if (currentUser) {
300 currentUser.authority = "ANONYMOUS"; 304 currentUser.authority = "ANONYMOUS";
301 } 305 }
  306 + var sysParamsPromise = loadSystemParams();
302 if (currentUser.isPublic) { 307 if (currentUser.isPublic) {
303 $rootScope.forceFullscreen = true; 308 $rootScope.forceFullscreen = true;
304 - fetchAllowedDashboardIds(); 309 + sysParamsPromise.then(
  310 + () => { fetchAllowedDashboardIds(); },
  311 + () => { deferred.reject(); }
  312 + );
305 } else if (currentUser.userId) { 313 } else if (currentUser.userId) {
306 getUser(currentUser.userId, true).then( 314 getUser(currentUser.userId, true).then(
307 function success(user) { 315 function success(user) {
308 - currentUserDetails = user;  
309 - updateUserLang();  
310 - $rootScope.forceFullscreen = false;  
311 - if (userForceFullscreen()) {  
312 - $rootScope.forceFullscreen = true;  
313 - }  
314 - if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' ||  
315 - currentUser.authority === 'CUSTOMER_USER')) {  
316 - fetchAllowedDashboardIds();  
317 - } else {  
318 - deferred.resolve();  
319 - } 316 + sysParamsPromise.then(
  317 + () => {
  318 + currentUserDetails = user;
  319 + updateUserLang();
  320 + $rootScope.forceFullscreen = false;
  321 + if (userForceFullscreen()) {
  322 + $rootScope.forceFullscreen = true;
  323 + }
  324 + if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' ||
  325 + currentUser.authority === 'CUSTOMER_USER')) {
  326 + fetchAllowedDashboardIds();
  327 + } else {
  328 + deferred.resolve();
  329 + }
  330 + },
  331 + () => {
  332 + deferred.reject();
  333 + logout();
  334 + }
  335 + );
320 }, 336 },
321 function fail() { 337 function fail() {
322 deferred.reject(); 338 deferred.reject();
@@ -353,6 +369,30 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -353,6 +369,30 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
353 return deferred.promise; 369 return deferred.promise;
354 } 370 }
355 371
  372 + function loadIsUserTokenAccessEnabled() {
  373 + var deferred = $q.defer();
  374 + if (currentUser.authority === 'SYS_ADMIN' || currentUser.authority === 'TENANT_ADMIN') {
  375 + var url = '/api/user/tokenAccessEnabled';
  376 + $http.get(url).then(function success(response) {
  377 + userTokenAccessEnabled = response.data;
  378 + deferred.resolve(response.data);
  379 + }, function fail() {
  380 + userTokenAccessEnabled = false;
  381 + deferred.reject();
  382 + });
  383 + } else {
  384 + userTokenAccessEnabled = false;
  385 + deferred.resolve(false);
  386 + }
  387 + return deferred.promise;
  388 + }
  389 +
  390 + function loadSystemParams() {
  391 + var promises = [];
  392 + promises.push(loadIsUserTokenAccessEnabled());
  393 + return $q.all(promises);
  394 + }
  395 +
356 function notifyUserLoaded() { 396 function notifyUserLoaded() {
357 if (!userLoaded) { 397 if (!userLoaded) {
358 userLoaded = true; 398 userLoaded = true;
@@ -520,7 +560,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -520,7 +560,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
520 } 560 }
521 ); 561 );
522 } 562 }
523 - $state.go(place, params); 563 + $state.go(place, params, {reload: true});
524 } else { 564 } else {
525 $state.go('login', params); 565 $state.go('login', params);
526 } 566 }
@@ -549,4 +589,18 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -549,4 +589,18 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
549 } 589 }
550 } 590 }
551 591
  592 + function isUserTokenAccessEnabled() {
  593 + return userTokenAccessEnabled;
  594 + }
  595 +
  596 + function loginAsUser(userId) {
  597 + var url = '/api/user/' + userId + '/token';
  598 + $http.get(url).then(function success(response) {
  599 + var token = response.data.token;
  600 + var refreshToken = response.data.refreshToken;
  601 + setUserFromJwtToken(token, refreshToken, true);
  602 + }, function fail() {
  603 + });
  604 + }
  605 +
552 } 606 }
@@ -1270,7 +1270,9 @@ @@ -1270,7 +1270,9 @@
1270 "activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :", 1270 "activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :",
1271 "copy-activation-link": "Copy activation link", 1271 "copy-activation-link": "Copy activation link",
1272 "activation-link-copied-message": "User activation link has been copied to clipboard", 1272 "activation-link-copied-message": "User activation link has been copied to clipboard",
1273 - "details": "Details" 1273 + "details": "Details",
  1274 + "login-as-tenant-admin": "Login as Tenant Admin",
  1275 + "login-as-customer-user": "Login as Customer User"
1274 }, 1276 },
1275 "value": { 1277 "value": {
1276 "type": "Value type", 1278 "type": "Value type",
@@ -21,6 +21,9 @@ @@ -21,6 +21,9 @@
21 <md-button ng-click="onResendActivation({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 21 <md-button ng-click="onResendActivation({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{
22 'user.resend-activation' | translate }} 22 'user.resend-activation' | translate }}
23 </md-button> 23 </md-button>
  24 +<md-button ng-click="onLoginAsUser({event: $event})" ng-show="!isEdit && loginAsUserEnabled" class="md-raised md-primary">{{
  25 + (isTenantAdmin() ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') | translate }}
  26 +</md-button>
24 <md-button ng-click="onDeleteUser({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'user.delete' | 27 <md-button ng-click="onDeleteUser({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'user.delete' |
25 translate }} 28 translate }}
26 </md-button> 29 </md-button>
@@ -32,6 +32,17 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d @@ -32,6 +32,17 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
32 var userActionsList = [ 32 var userActionsList = [
33 { 33 {
34 onAction: function ($event, item) { 34 onAction: function ($event, item) {
  35 + loginAsUser(item);
  36 + },
  37 + name: function() { return $translate.instant('login.login') },
  38 + details: function() { return $translate.instant(usersType === 'tenant' ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') },
  39 + icon: "login",
  40 + isEnabled: function() {
  41 + return userService.isUserTokenAccessEnabled();
  42 + }
  43 + },
  44 + {
  45 + onAction: function ($event, item) {
35 vm.grid.deleteItem($event, item); 46 vm.grid.deleteItem($event, item);
36 }, 47 },
37 name: function() { return $translate.instant('action.delete') }, 48 name: function() { return $translate.instant('action.delete') },
@@ -78,6 +89,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d @@ -78,6 +89,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
78 89
79 vm.displayActivationLink = displayActivationLink; 90 vm.displayActivationLink = displayActivationLink;
80 vm.resendActivation = resendActivation; 91 vm.resendActivation = resendActivation;
  92 + vm.loginAsUser = loginAsUser;
81 93
82 initController(); 94 initController();
83 95
@@ -184,4 +196,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d @@ -184,4 +196,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
184 toast.showSuccess($translate.instant('user.activation-email-sent-message')); 196 toast.showSuccess($translate.instant('user.activation-email-sent-message'));
185 }); 197 });
186 } 198 }
  199 +
  200 + function loginAsUser(user) {
  201 + userService.loginAsUser(user.id.id);
  202 + }
187 } 203 }
@@ -22,18 +22,20 @@ import userFieldsetTemplate from './user-fieldset.tpl.html'; @@ -22,18 +22,20 @@ import userFieldsetTemplate from './user-fieldset.tpl.html';
22 /* eslint-enable import/no-unresolved, import/default */ 22 /* eslint-enable import/no-unresolved, import/default */
23 23
24 /*@ngInject*/ 24 /*@ngInject*/
25 -export default function UserDirective($compile, $templateCache/*, dashboardService*/) { 25 +export default function UserDirective($compile, $templateCache, userService) {
26 var linker = function (scope, element) { 26 var linker = function (scope, element) {
27 var template = $templateCache.get(userFieldsetTemplate); 27 var template = $templateCache.get(userFieldsetTemplate);
28 element.html(template); 28 element.html(template);
29 29
30 scope.isTenantAdmin = function() { 30 scope.isTenantAdmin = function() {
31 return scope.user && scope.user.authority === 'TENANT_ADMIN'; 31 return scope.user && scope.user.authority === 'TENANT_ADMIN';
32 - } 32 + };
33 33
34 scope.isCustomerUser = function() { 34 scope.isCustomerUser = function() {
35 return scope.user && scope.user.authority === 'CUSTOMER_USER'; 35 return scope.user && scope.user.authority === 'CUSTOMER_USER';
36 - } 36 + };
  37 +
  38 + scope.loginAsUserEnabled = userService.isUserTokenAccessEnabled();
37 39
38 $compile(element.contents())(scope); 40 $compile(element.contents())(scope);
39 } 41 }
@@ -46,6 +48,7 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi @@ -46,6 +48,7 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi
46 theForm: '=', 48 theForm: '=',
47 onDisplayActivationLink: '&', 49 onDisplayActivationLink: '&',
48 onResendActivation: '&', 50 onResendActivation: '&',
  51 + onLoginAsUser: '&',
49 onDeleteUser: '&' 52 onDeleteUser: '&'
50 } 53 }
51 }; 54 };
@@ -27,6 +27,7 @@ @@ -27,6 +27,7 @@
27 the-form="vm.grid.detailsForm" 27 the-form="vm.grid.detailsForm"
28 on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)" 28 on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
29 on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)" 29 on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
  30 + on-login-as-user="vm.loginAsUser(vm.grid.detailsConfig.currentItem)"
30 on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user> 31 on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
31 </md-tab> 32 </md-tab>
32 <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}"> 33 <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">