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 5f6c1cef21..a050fa3260 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -15,7 +15,12 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -39,7 +44,11 @@ import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.security.model.token.JwtToken; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.servlet.http.HttpServletRequest; @@ -50,9 +59,21 @@ public class UserController extends BaseController { public static final String USER_ID = "userId"; public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s"; + + @Value("${security.user_token_access_enabled}") + @Getter + private boolean userTokenAccessEnabled; + @Autowired private MailService mailService; + @Autowired + private JwtTokenFactory tokenFactory; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) @ResponseBody @@ -71,6 +92,42 @@ public class UserController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/user/tokenAccessEnabled", method = RequestMethod.GET) + @ResponseBody + public boolean isUserTokenAccessEnabled() { + return userTokenAccessEnabled; + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET) + @ResponseBody + public JsonNode getUserToken(@PathVariable(USER_ID) String strUserId) throws ThingsboardException { + checkParameter(USER_ID, strUserId); + try { + UserId userId = new UserId(toUUID(strUserId)); + SecurityUser authUser = getCurrentUser(); + User user = userService.findUserById(userId); + if (!userTokenAccessEnabled || (authUser.getAuthority() == Authority.SYS_ADMIN && user.getAuthority() != Authority.TENANT_ADMIN) + || (authUser.getAuthority() == Authority.TENANT_ADMIN && !authUser.getTenantId().equals(user.getTenantId()))) { + throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION, + ThingsboardErrorCode.PERMISSION_DENIED); + } + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + UserCredentials credentials = userService.findUserCredentialsByUserId(userId); + SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); + JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode tokenObject = objectMapper.createObjectNode(); + tokenObject.put("token", accessToken.getToken()); + tokenObject.put("refreshToken", refreshToken.getToken()); + return tokenObject; + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/user", method = RequestMethod.POST) @ResponseBody diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f510ec48a5..3e62453e4f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -66,12 +66,16 @@ plugins: # Comma seperated package list used during classpath scanning for plugins scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}" -# JWT Token parameters -security.jwt: - tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins) - refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour) - tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" - tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" +# Security parameters +security: + # JWT Token parameters + jwt: + tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins) + refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour) + tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" + tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" + # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator + user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}" # Device communication protocol parameters http: diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index 48c811bb8d..fa4d63c8e2 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -27,6 +27,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi currentUserDetails = null, lastPublicDashboardId = null, allowedDashboardIds = [], + userTokenAccessEnabled = false, userLoaded = false; var refreshTokenQueue = []; @@ -59,7 +60,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi forceDefaultPlace: forceDefaultPlace, updateLastPublicDashboardId: updateLastPublicDashboardId, logout: logout, - reloadUser: reloadUser + reloadUser: reloadUser, + isUserTokenAccessEnabled: isUserTokenAccessEnabled, + loginAsUser: loginAsUser } reloadUser(); @@ -105,6 +108,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi currentUser = null; currentUserDetails = null; lastPublicDashboardId = null; + userTokenAccessEnabled = false; allowedDashboardIds = []; if (!jwtToken) { clearTokenData(); @@ -299,24 +303,36 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi } else if (currentUser) { currentUser.authority = "ANONYMOUS"; } + var sysParamsPromise = loadSystemParams(); if (currentUser.isPublic) { $rootScope.forceFullscreen = true; - fetchAllowedDashboardIds(); + sysParamsPromise.then( + () => { fetchAllowedDashboardIds(); }, + () => { deferred.reject(); } + ); } else if (currentUser.userId) { getUser(currentUser.userId, true).then( function success(user) { - currentUserDetails = user; - updateUserLang(); - $rootScope.forceFullscreen = false; - if (userForceFullscreen()) { - $rootScope.forceFullscreen = true; - } - if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' || - currentUser.authority === 'CUSTOMER_USER')) { - fetchAllowedDashboardIds(); - } else { - deferred.resolve(); - } + sysParamsPromise.then( + () => { + currentUserDetails = user; + updateUserLang(); + $rootScope.forceFullscreen = false; + if (userForceFullscreen()) { + $rootScope.forceFullscreen = true; + } + if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' || + currentUser.authority === 'CUSTOMER_USER')) { + fetchAllowedDashboardIds(); + } else { + deferred.resolve(); + } + }, + () => { + deferred.reject(); + logout(); + } + ); }, function fail() { deferred.reject(); @@ -353,6 +369,30 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi return deferred.promise; } + function loadIsUserTokenAccessEnabled() { + var deferred = $q.defer(); + if (currentUser.authority === 'SYS_ADMIN' || currentUser.authority === 'TENANT_ADMIN') { + var url = '/api/user/tokenAccessEnabled'; + $http.get(url).then(function success(response) { + userTokenAccessEnabled = response.data; + deferred.resolve(response.data); + }, function fail() { + userTokenAccessEnabled = false; + deferred.reject(); + }); + } else { + userTokenAccessEnabled = false; + deferred.resolve(false); + } + return deferred.promise; + } + + function loadSystemParams() { + var promises = []; + promises.push(loadIsUserTokenAccessEnabled()); + return $q.all(promises); + } + function notifyUserLoaded() { if (!userLoaded) { userLoaded = true; @@ -520,7 +560,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi } ); } - $state.go(place, params); + $state.go(place, params, {reload: true}); } else { $state.go('login', params); } @@ -549,4 +589,18 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi } } + function isUserTokenAccessEnabled() { + return userTokenAccessEnabled; + } + + function loginAsUser(userId) { + var url = '/api/user/' + userId + '/token'; + $http.get(url).then(function success(response) { + var token = response.data.token; + var refreshToken = response.data.refreshToken; + setUserFromJwtToken(token, refreshToken, true); + }, function fail() { + }); + } + } diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 22aad1a90d..a98733c671 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -1270,7 +1270,9 @@ "activation-link-text": "In order to activate user use the following activation link :", "copy-activation-link": "Copy activation link", "activation-link-copied-message": "User activation link has been copied to clipboard", - "details": "Details" + "details": "Details", + "login-as-tenant-admin": "Login as Tenant Admin", + "login-as-customer-user": "Login as Customer User" }, "value": { "type": "Value type", diff --git a/ui/src/app/user/user-fieldset.tpl.html b/ui/src/app/user/user-fieldset.tpl.html index 15b32ecc80..d3b5111f15 100644 --- a/ui/src/app/user/user-fieldset.tpl.html +++ b/ui/src/app/user/user-fieldset.tpl.html @@ -21,6 +21,9 @@ {{ 'user.resend-activation' | translate }} +{{ + (isTenantAdmin() ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') | translate }} + {{ 'user.delete' | translate }} diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js index 4fd6b25828..8d7f214b9a 100644 --- a/ui/src/app/user/user.controller.js +++ b/ui/src/app/user/user.controller.js @@ -30,6 +30,17 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d var usersType = $state.$current.data.usersType; var userActionsList = [ + { + onAction: function ($event, item) { + loginAsUser(item); + }, + name: function() { return $translate.instant('login.login') }, + details: function() { return $translate.instant(usersType === 'tenant' ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') }, + icon: "login", + isEnabled: function() { + return userService.isUserTokenAccessEnabled(); + } + }, { onAction: function ($event, item) { vm.grid.deleteItem($event, item); @@ -78,6 +89,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d vm.displayActivationLink = displayActivationLink; vm.resendActivation = resendActivation; + vm.loginAsUser = loginAsUser; initController(); @@ -184,4 +196,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d toast.showSuccess($translate.instant('user.activation-email-sent-message')); }); } + + function loginAsUser(user) { + userService.loginAsUser(user.id.id); + } } diff --git a/ui/src/app/user/user.directive.js b/ui/src/app/user/user.directive.js index 1c01c8f4b4..b32d028e89 100644 --- a/ui/src/app/user/user.directive.js +++ b/ui/src/app/user/user.directive.js @@ -22,18 +22,20 @@ import userFieldsetTemplate from './user-fieldset.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function UserDirective($compile, $templateCache/*, dashboardService*/) { +export default function UserDirective($compile, $templateCache, userService) { var linker = function (scope, element) { var template = $templateCache.get(userFieldsetTemplate); element.html(template); scope.isTenantAdmin = function() { return scope.user && scope.user.authority === 'TENANT_ADMIN'; - } + }; scope.isCustomerUser = function() { return scope.user && scope.user.authority === 'CUSTOMER_USER'; - } + }; + + scope.loginAsUserEnabled = userService.isUserTokenAccessEnabled(); $compile(element.contents())(scope); } @@ -46,6 +48,7 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi theForm: '=', onDisplayActivationLink: '&', onResendActivation: '&', + onLoginAsUser: '&', onDeleteUser: '&' } }; diff --git a/ui/src/app/user/users.tpl.html b/ui/src/app/user/users.tpl.html index b73b5a60b9..a311d98a67 100644 --- a/ui/src/app/user/users.tpl.html +++ b/ui/src/app/user/users.tpl.html @@ -27,6 +27,7 @@ the-form="vm.grid.detailsForm" on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)" on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)" + on-login-as-user="vm.loginAsUser(vm.grid.detailsConfig.currentItem)" on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)">