Showing
8 changed files
with
165 additions
and
25 deletions
... | ... | @@ -15,7 +15,12 @@ |
15 | 15 | */ |
16 | 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 | 22 | import org.springframework.beans.factory.annotation.Autowired; |
23 | +import org.springframework.beans.factory.annotation.Value; | |
19 | 24 | import org.springframework.http.HttpStatus; |
20 | 25 | import org.springframework.security.access.prepost.PreAuthorize; |
21 | 26 | import org.springframework.web.bind.annotation.PathVariable; |
... | ... | @@ -39,7 +44,11 @@ import org.thingsboard.server.common.data.page.TextPageData; |
39 | 44 | import org.thingsboard.server.common.data.page.TextPageLink; |
40 | 45 | import org.thingsboard.server.common.data.security.Authority; |
41 | 46 | import org.thingsboard.server.common.data.security.UserCredentials; |
47 | +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; | |
42 | 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 | 53 | import javax.servlet.http.HttpServletRequest; |
45 | 54 | |
... | ... | @@ -50,9 +59,21 @@ public class UserController extends BaseController { |
50 | 59 | public static final String USER_ID = "userId"; |
51 | 60 | public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; |
52 | 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 | 67 | @Autowired |
54 | 68 | private MailService mailService; |
55 | 69 | |
70 | + @Autowired | |
71 | + private JwtTokenFactory tokenFactory; | |
72 | + | |
73 | + @Autowired | |
74 | + private RefreshTokenRepository refreshTokenRepository; | |
75 | + | |
76 | + | |
56 | 77 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
57 | 78 | @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) |
58 | 79 | @ResponseBody |
... | ... | @@ -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 | 131 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
75 | 132 | @RequestMapping(value = "/user", method = RequestMethod.POST) |
76 | 133 | @ResponseBody | ... | ... |
... | ... | @@ -66,12 +66,16 @@ plugins: |
66 | 66 | # Comma seperated package list used during classpath scanning for plugins |
67 | 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 | 80 | # Device communication protocol parameters |
77 | 81 | http: | ... | ... |
... | ... | @@ -27,6 +27,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi |
27 | 27 | currentUserDetails = null, |
28 | 28 | lastPublicDashboardId = null, |
29 | 29 | allowedDashboardIds = [], |
30 | + userTokenAccessEnabled = false, | |
30 | 31 | userLoaded = false; |
31 | 32 | |
32 | 33 | var refreshTokenQueue = []; |
... | ... | @@ -59,7 +60,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi |
59 | 60 | forceDefaultPlace: forceDefaultPlace, |
60 | 61 | updateLastPublicDashboardId: updateLastPublicDashboardId, |
61 | 62 | logout: logout, |
62 | - reloadUser: reloadUser | |
63 | + reloadUser: reloadUser, | |
64 | + isUserTokenAccessEnabled: isUserTokenAccessEnabled, | |
65 | + loginAsUser: loginAsUser | |
63 | 66 | } |
64 | 67 | |
65 | 68 | reloadUser(); |
... | ... | @@ -105,6 +108,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi |
105 | 108 | currentUser = null; |
106 | 109 | currentUserDetails = null; |
107 | 110 | lastPublicDashboardId = null; |
111 | + userTokenAccessEnabled = false; | |
108 | 112 | allowedDashboardIds = []; |
109 | 113 | if (!jwtToken) { |
110 | 114 | clearTokenData(); |
... | ... | @@ -299,24 +303,36 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi |
299 | 303 | } else if (currentUser) { |
300 | 304 | currentUser.authority = "ANONYMOUS"; |
301 | 305 | } |
306 | + var sysParamsPromise = loadSystemParams(); | |
302 | 307 | if (currentUser.isPublic) { |
303 | 308 | $rootScope.forceFullscreen = true; |
304 | - fetchAllowedDashboardIds(); | |
309 | + sysParamsPromise.then( | |
310 | + () => { fetchAllowedDashboardIds(); }, | |
311 | + () => { deferred.reject(); } | |
312 | + ); | |
305 | 313 | } else if (currentUser.userId) { |
306 | 314 | getUser(currentUser.userId, true).then( |
307 | 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 | 337 | function fail() { |
322 | 338 | deferred.reject(); |
... | ... | @@ -353,6 +369,30 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi |
353 | 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 | 396 | function notifyUserLoaded() { |
357 | 397 | if (!userLoaded) { |
358 | 398 | userLoaded = true; |
... | ... | @@ -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 | 564 | } else { |
525 | 565 | $state.go('login', params); |
526 | 566 | } |
... | ... | @@ -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 | 1270 | "activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :", |
1271 | 1271 | "copy-activation-link": "Copy activation link", |
1272 | 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 | 1277 | "value": { |
1276 | 1278 | "type": "Value type", | ... | ... |
... | ... | @@ -21,6 +21,9 @@ |
21 | 21 | <md-button ng-click="onResendActivation({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ |
22 | 22 | 'user.resend-activation' | translate }} |
23 | 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 | 27 | <md-button ng-click="onDeleteUser({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'user.delete' | |
25 | 28 | translate }} |
26 | 29 | </md-button> | ... | ... |
... | ... | @@ -32,6 +32,17 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d |
32 | 32 | var userActionsList = [ |
33 | 33 | { |
34 | 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 | 46 | vm.grid.deleteItem($event, item); |
36 | 47 | }, |
37 | 48 | name: function() { return $translate.instant('action.delete') }, |
... | ... | @@ -78,6 +89,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d |
78 | 89 | |
79 | 90 | vm.displayActivationLink = displayActivationLink; |
80 | 91 | vm.resendActivation = resendActivation; |
92 | + vm.loginAsUser = loginAsUser; | |
81 | 93 | |
82 | 94 | initController(); |
83 | 95 | |
... | ... | @@ -184,4 +196,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d |
184 | 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 | 22 | /* eslint-enable import/no-unresolved, import/default */ |
23 | 23 | |
24 | 24 | /*@ngInject*/ |
25 | -export default function UserDirective($compile, $templateCache/*, dashboardService*/) { | |
25 | +export default function UserDirective($compile, $templateCache, userService) { | |
26 | 26 | var linker = function (scope, element) { |
27 | 27 | var template = $templateCache.get(userFieldsetTemplate); |
28 | 28 | element.html(template); |
29 | 29 | |
30 | 30 | scope.isTenantAdmin = function() { |
31 | 31 | return scope.user && scope.user.authority === 'TENANT_ADMIN'; |
32 | - } | |
32 | + }; | |
33 | 33 | |
34 | 34 | scope.isCustomerUser = function() { |
35 | 35 | return scope.user && scope.user.authority === 'CUSTOMER_USER'; |
36 | - } | |
36 | + }; | |
37 | + | |
38 | + scope.loginAsUserEnabled = userService.isUserTokenAccessEnabled(); | |
37 | 39 | |
38 | 40 | $compile(element.contents())(scope); |
39 | 41 | } |
... | ... | @@ -46,6 +48,7 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi |
46 | 48 | theForm: '=', |
47 | 49 | onDisplayActivationLink: '&', |
48 | 50 | onResendActivation: '&', |
51 | + onLoginAsUser: '&', | |
49 | 52 | onDeleteUser: '&' |
50 | 53 | } |
51 | 54 | }; | ... | ... |
... | ... | @@ -27,6 +27,7 @@ |
27 | 27 | the-form="vm.grid.detailsForm" |
28 | 28 | on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)" |
29 | 29 | on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)" |
30 | + on-login-as-user="vm.loginAsUser(vm.grid.detailsConfig.currentItem)" | |
30 | 31 | on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user> |
31 | 32 | </md-tab> |
32 | 33 | <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}"> | ... | ... |