Browse Source

Password policy setting. Login/Logout audit log.

pull/1868/head
Igor Kulikov 7 years ago
parent
commit
8d5d8b2c23
  1. 8
      application/pom.xml
  2. 30
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  3. 98
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  4. 37
      application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java
  5. 12
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  6. 54
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java
  7. 29
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java
  8. 99
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
  10. 33
      application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java
  11. 25
      application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java
  12. 31
      application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java
  13. 155
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  14. 34
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  15. 3
      application/src/main/resources/thingsboard.yml
  16. 12
      application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java
  17. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  18. 4
      common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
  19. 1
      common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java
  20. 11
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
  21. 6
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  22. 5
      dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
  23. 19
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  24. 9
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
  25. 2
      msa/js-executor/package-lock.json
  26. 2
      msa/web-ui/package-lock.json
  27. 12
      pom.xml
  28. 2
      ui/package-lock.json
  29. 19
      ui/src/app/admin/admin.routes.js
  30. 2
      ui/src/app/admin/index.js
  31. 42
      ui/src/app/admin/security-settings.controller.js
  32. 125
      ui/src/app/admin/security-settings.tpl.html
  33. 4
      ui/src/app/admin/settings-card.scss
  34. 24
      ui/src/app/api/admin.service.js
  35. 6
      ui/src/app/api/user.service.js
  36. 7
      ui/src/app/common/types.constant.js
  37. 2
      ui/src/app/global-interceptor.service.js
  38. 1
      ui/src/app/help/help-links.constant.js
  39. 20
      ui/src/app/locale/locale.constant-en_US.json
  40. 9
      ui/src/app/login/login.controller.js
  41. 14
      ui/src/app/login/login.routes.js
  42. 4
      ui/src/app/login/reset-password.controller.js
  43. 1
      ui/src/app/login/reset-password.tpl.html
  44. 13
      ui/src/app/services/menu.service.js

8
application/pom.xml

@ -272,6 +272,14 @@
<groupId>io.springfox.ui</groupId>
<artifactId>springfox-swagger-ui-rfc6570</artifactId>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
</dependency>
<dependency>
<groupId>com.github.ua-parser</groupId>
<artifactId>uap-java</artifactId>
</dependency>
</dependencies>
<build>

30
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -28,8 +28,10 @@ import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.security.model.SecuritySettings;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.service.update.UpdateService;
import org.thingsboard.server.service.update.model.UpdateMessage;
@ -43,6 +45,9 @@ public class AdminController extends BaseController {
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private SystemSecurityService systemSecurityService;
@Autowired
private UpdateService updateService;
@ -74,6 +79,31 @@ public class AdminController extends BaseController {
}
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.GET)
@ResponseBody
public SecuritySettings getSecuritySettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.POST)
@ResponseBody
public SecuritySettings saveSecuritySettings(@RequestBody SecuritySettings securitySettings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(TenantId.SYS_TENANT_ID, securitySettings));
return securitySettings;
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testMail", method = RequestMethod.POST)
public void sendTestMail(@RequestBody AdminSettings adminSettings) throws ThingsboardException {

98
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -24,6 +24,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -34,15 +35,24 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.SecuritySettings;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPasswordPolicy;
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 org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import ua_parser.Client;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
@ -65,6 +75,12 @@ public class AuthController extends BaseController {
@Autowired
private MailService mailService;
@Autowired
private SystemSecurityService systemSecurityService;
@Autowired
private AuditLogService auditLogService;
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET)
public @ResponseBody User getUser() throws ThingsboardException {
@ -76,6 +92,13 @@ public class AuthController extends BaseController {
}
}
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/auth/logout", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void logout(HttpServletRequest request) throws ThingsboardException {
logLogoutAction(request);
}
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ -89,8 +112,24 @@ public class AuthController extends BaseController {
if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
systemSecurityService.validatePassword(securityUser.getTenantId(), newPassword);
if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) {
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
userCredentials.setPassword(passwordEncoder.encode(newPassword));
userService.saveUserCredentials(securityUser.getTenantId(), userCredentials);
userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials);
} catch (Exception e) {
throw handleException(e);
}
}
@RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET)
@ResponseBody
public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException {
try {
SecuritySettings securitySettings =
checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID));
return securitySettings.getPasswordPolicy();
} catch (Exception e) {
throw handleException(e);
}
@ -167,6 +206,7 @@ public class AuthController extends BaseController {
try {
String activateToken = activateRequest.get("activateToken").asText();
String password = activateRequest.get("password").asText();
systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password);
String encodedPassword = passwordEncoder.encode(password);
UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
@ -206,10 +246,14 @@ public class AuthController extends BaseController {
String password = resetPasswordRequest.get("password").asText();
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password);
if (passwordEncoder.matches(password, userCredentials.getPassword())) {
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
String encodedPassword = passwordEncoder.encode(password);
userCredentials.setPassword(encodedPassword);
userCredentials.setResetToken(null);
userCredentials = userService.saveUserCredentials(TenantId.SYS_TENANT_ID, userCredentials);
userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
@ -234,4 +278,54 @@ public class AuthController extends BaseController {
}
}
private void logLogoutAction(HttpServletRequest request) throws ThingsboardException {
try {
SecurityUser user = getCurrentUser();
RestAuthenticationDetails details = new RestAuthenticationDetails(request);
String clientAddress = details.getClientAddress();
String browser = "Unknown";
String os = "Unknown";
String device = "Unknown";
if (details.getUserAgent() != null) {
Client userAgent = details.getUserAgent();
if (userAgent.userAgent != null) {
browser = userAgent.userAgent.family;
if (userAgent.userAgent.major != null) {
browser += " " + userAgent.userAgent.major;
if (userAgent.userAgent.minor != null) {
browser += "." + userAgent.userAgent.minor;
if (userAgent.userAgent.patch != null) {
browser += "." + userAgent.userAgent.patch;
}
}
}
}
if (userAgent.os != null) {
os = userAgent.os.family;
if (userAgent.os.major != null) {
os += " " + userAgent.os.major;
if (userAgent.os.minor != null) {
os += "." + userAgent.os.minor;
if (userAgent.os.patch != null) {
os += "." + userAgent.os.patch;
if (userAgent.os.patchMinor != null) {
os += "." + userAgent.os.patchMinor;
}
}
}
}
}
if (userAgent.device != null) {
device = userAgent.device.family;
}
}
auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device);
} catch (Exception e) {
throw handleException(e);
}
}
}

37
application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.exception;
import org.springframework.http.HttpStatus;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse {
private final String resetToken;
protected ThingsboardCredentialsExpiredResponse(String message, String resetToken) {
super(message, ThingsboardErrorCode.CREDENTIALS_EXPIRED, HttpStatus.UNAUTHORIZED);
this.resetToken = resetToken;
}
public static ThingsboardCredentialsExpiredResponse of(final String message, final String resetToken) {
return new ThingsboardCredentialsExpiredResponse(message, resetToken);
}
public String getResetToken() {
return resetToken;
}
}

12
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -18,10 +18,12 @@ package org.thingsboard.server.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
@ -31,11 +33,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@Component
@Slf4j
@ -141,8 +146,13 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
} else if (authenticationException instanceof AuthMethodNotSupportedException) {
mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
} else if (authenticationException instanceof UserPasswordExpiredException) {
UserPasswordExpiredException expiredException = (UserPasswordExpiredException)authenticationException;
String resetToken = expiredException.getResetToken();
mapper.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken));
} else {
mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
}
mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
}
}

54
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.rest;
import lombok.Data;
import ua_parser.Client;
import ua_parser.Parser;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
@Data
public class RestAuthenticationDetails implements Serializable {
private final String clientAddress;
private final Client userAgent;
public RestAuthenticationDetails(HttpServletRequest request) {
this.clientAddress = getClientIP(request);
this.userAgent = getUserAgent(request);
}
private static String getClientIP(HttpServletRequest request) {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader == null) {
return request.getRemoteAddr();
}
return xfHeader.split(",")[0];
}
private static Client getUserAgent(HttpServletRequest request) {
try {
Parser uaParser = new Parser();
return uaParser.parse(request.getHeader("User-Agent"));
} catch (IOException e) {
return new Client(null, null, null);
}
}
}

29
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.rest;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import javax.servlet.http.HttpServletRequest;
public class RestAuthenticationDetailsSource implements
AuthenticationDetailsSource<HttpServletRequest, RestAuthenticationDetails> {
public RestAuthenticationDetails buildDetails(HttpServletRequest context) {
return new RestAuthenticationDetails(context);
}
}

99
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.security.auth.rest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
@ -29,31 +30,41 @@ import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import ua_parser.Client;
import java.util.UUID;
@Component
@Slf4j
public class RestAuthenticationProvider implements AuthenticationProvider {
private final BCryptPasswordEncoder encoder;
private final SystemSecurityService systemSecurityService;
private final UserService userService;
private final CustomerService customerService;
private final AuditLogService auditLogService;
@Autowired
public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) {
public RestAuthenticationProvider(final UserService userService,
final CustomerService customerService,
final SystemSecurityService systemSecurityService,
final AuditLogService auditLogService) {
this.userService = userService;
this.customerService = customerService;
this.encoder = encoder;
this.systemSecurityService = systemSecurityService;
this.auditLogService = auditLogService;
}
@Override
@ -69,37 +80,40 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
String username = userPrincipal.getValue();
String password = (String) authentication.getCredentials();
return authenticateByUsernameAndPassword(userPrincipal, username, password);
return authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
} else {
String publicId = userPrincipal.getValue();
return authenticateByPublicId(userPrincipal, publicId);
}
}
private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) {
private Authentication authenticateByUsernameAndPassword(Authentication authentication, UserPrincipal userPrincipal, String username, String password) {
User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId());
if (userCredentials == null) {
throw new UsernameNotFoundException("User credentials not found");
}
try {
if (!userCredentials.isEnabled()) {
throw new DisabledException("User is not active");
}
UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId());
if (userCredentials == null) {
throw new UsernameNotFoundException("User credentials not found");
}
if (!encoder.matches(password, userCredentials.getPassword())) {
throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
}
systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, password);
if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
if (user.getAuthority() == null)
throw new InsufficientAuthenticationException("User has no authority assigned");
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
logLoginAction(user, authentication, null);
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
} catch (Exception e) {
logLoginAction(user, authentication, e);
throw e;
}
}
private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
@ -133,4 +147,53 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
private void logLoginAction(User user, Authentication authentication, Exception e) {
String clientAddress = "Unknown";
String browser = "Unknown";
String os = "Unknown";
String device = "Unknown";
if (authentication != null && authentication.getDetails() != null) {
if (authentication.getDetails() instanceof RestAuthenticationDetails) {
RestAuthenticationDetails details = (RestAuthenticationDetails)authentication.getDetails();
clientAddress = details.getClientAddress();
if (details.getUserAgent() != null) {
Client userAgent = details.getUserAgent();
if (userAgent.userAgent != null) {
browser = userAgent.userAgent.family;
if (userAgent.userAgent.major != null) {
browser += " " + userAgent.userAgent.major;
if (userAgent.userAgent.minor != null) {
browser += "." + userAgent.userAgent.minor;
if (userAgent.userAgent.patch != null) {
browser += "." + userAgent.userAgent.patch;
}
}
}
}
if (userAgent.os != null) {
os = userAgent.os.family;
if (userAgent.os.major != null) {
os += " " + userAgent.os.major;
if (userAgent.os.minor != null) {
os += "." + userAgent.os.minor;
if (userAgent.os.patch != null) {
os += "." + userAgent.os.patch;
if (userAgent.os.patchMinor != null) {
os += "." + userAgent.os.patchMinor;
}
}
}
}
}
if (userAgent.device != null) {
device = userAgent.device.family;
}
}
}
}
auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, ActionType.LOGIN, e, clientAddress, browser, os, device);
}
}

6
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@ -27,6 +28,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -39,6 +41,8 @@ import java.io.IOException;
@Slf4j
public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new RestAuthenticationDetailsSource();
private final AuthenticationSuccessHandler successHandler;
private final AuthenticationFailureHandler failureHandler;
@ -76,7 +80,7 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword());
token.setDetails(authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(token);
}

33
application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.exception;
import org.springframework.security.authentication.CredentialsExpiredException;
public class UserPasswordExpiredException extends CredentialsExpiredException {
private final String resetToken;
public UserPasswordExpiredException(String msg, String resetToken) {
super(msg);
this.resetToken = resetToken;
}
public String getResetToken() {
return resetToken;
}
}

25
application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.model;
import lombok.Data;
@Data
public class SecuritySettings {
private UserPasswordPolicy passwordPolicy;
}

31
application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.model;
import lombok.Data;
@Data
public class UserPasswordPolicy {
private Integer minimumLength;
private Integer minimumUppercaseLetters;
private Integer minimumLowercaseLetters;
private Integer minimumDigits;
private Integer minimumSpecialCharacters;
private Integer passwordExpirationPeriodDays;
}

155
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -0,0 +1,155 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.system;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.passay.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.model.SecuritySettings;
import org.thingsboard.server.service.security.model.UserPasswordPolicy;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
@Service
@Slf4j
public class DefaultSystemSecurityService implements SystemSecurityService {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private BCryptPasswordEncoder encoder;
@Autowired
private UserService userService;
@Resource
private SystemSecurityService self;
@Cacheable(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
@Override
public SecuritySettings getSecuritySettings(TenantId tenantId) {
SecuritySettings securitySettings = null;
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings");
if (adminSettings != null) {
try {
securitySettings = objectMapper.treeToValue(adminSettings.getJsonValue(), SecuritySettings.class);
} catch (Exception e) {
throw new RuntimeException("Failed to load security settings!", e);
}
} else {
securitySettings = new SecuritySettings();
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6);
}
return securitySettings;
}
@CacheEvict(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
@Override
public SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings) {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings");
if (adminSettings == null) {
adminSettings = new AdminSettings();
adminSettings.setKey("securitySettings");
}
adminSettings.setJsonValue(objectMapper.valueToTree(securitySettings));
AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(tenantId, adminSettings);
try {
return objectMapper.treeToValue(savedAdminSettings.getJsonValue(), SecuritySettings.class);
} catch (Exception e) {
throw new RuntimeException("Failed to load security settings!", e);
}
}
@Override
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String password) throws AuthenticationException {
if (!encoder.matches(password, userCredentials.getPassword())) {
throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
}
if (!userCredentials.isEnabled()) {
throw new DisabledException("User is not active");
}
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
if ((userCredentials.getCreatedTime()
+ TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays()))
< System.currentTimeMillis()) {
userCredentials = userService.requestExpiredPasswordReset(tenantId, userCredentials.getId());
throw new UserPasswordExpiredException("User password expired!", userCredentials.getResetToken());
}
}
}
@Override
public void validatePassword(TenantId tenantId, String password) throws DataValidationException {
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
List<Rule> passwordRules = new ArrayList<>();
passwordRules.add(new LengthRule(passwordPolicy.getMinimumLength(), Integer.MAX_VALUE));
if (isPositiveInteger(passwordPolicy.getMinimumUppercaseLetters())) {
passwordRules.add(new CharacterRule(EnglishCharacterData.UpperCase, passwordPolicy.getMinimumUppercaseLetters()));
}
if (isPositiveInteger(passwordPolicy.getMinimumLowercaseLetters())) {
passwordRules.add(new CharacterRule(EnglishCharacterData.LowerCase, passwordPolicy.getMinimumLowercaseLetters()));
}
if (isPositiveInteger(passwordPolicy.getMinimumDigits())) {
passwordRules.add(new CharacterRule(EnglishCharacterData.Digit, passwordPolicy.getMinimumDigits()));
}
if (isPositiveInteger(passwordPolicy.getMinimumSpecialCharacters())) {
passwordRules.add(new CharacterRule(EnglishCharacterData.Special, passwordPolicy.getMinimumSpecialCharacters()));
}
PasswordValidator validator = new PasswordValidator(passwordRules);
PasswordData passwordData = new PasswordData(password);
RuleResult result = validator.validate(passwordData);
if (!result.isValid()) {
String message = String.join("\n", validator.getMessages(result));
throw new DataValidationException(message);
}
}
private static boolean isPositiveInteger(Integer val) {
return val != null && val.intValue() > 0;
}
}

34
application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.system;
import org.springframework.security.core.AuthenticationException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.service.security.model.SecuritySettings;
public interface SystemSecurityService {
SecuritySettings getSecuritySettings(TenantId tenantId);
SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings);
void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String password) throws AuthenticationException;
void validatePassword(TenantId tenantId, String password) throws DataValidationException;
}

3
application/src/main/resources/thingsboard.yml

@ -269,6 +269,9 @@ caffeine:
claimDevices:
timeToLiveInMinutes: 1
maxSize: 100000
securitySettings:
timeToLiveInMinutes: 1440
maxSize: 1
redis:
# standalone or cluster

12
application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java

@ -105,18 +105,6 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString("Provided json structure is different")));
}
@Test
public void testSaveAdminSettingsWithNonTextValue() throws Exception {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
JsonNode json = adminSettings.getJsonValue();
((ObjectNode) json).put("timeout", 10000L);
adminSettings.setJsonValue(json);
doPost("/api/admin/settings", adminSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Provided json structure can't contain non-text values")));
}
@Test
public void testSendTestMail() throws Exception {
loginSysAdmin();

1
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -23,4 +23,5 @@ public class CacheConstants {
public static final String ASSET_CACHE = "assets";
public static final String ENTITY_VIEW_CACHE = "entityViews";
public static final String CLAIM_DEVICES_CACHE = "claimDevices";
public static final String SECURITY_SETTINGS_CACHE = "securitySettings";
}

4
common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java

@ -37,7 +37,9 @@ public enum ActionType {
RELATION_DELETED(false),
RELATIONS_DELETED(false),
ALARM_ACK(false),
ALARM_CLEAR(false);
ALARM_CLEAR(false),
LOGIN(false),
LOGOUT(false);
private final boolean isRead;

1
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java

@ -22,6 +22,7 @@ public enum ThingsboardErrorCode {
GENERAL(2),
AUTHENTICATION(10),
JWT_TOKEN_EXPIRED(11),
CREDENTIALS_EXPIRED(15),
PERMISSION_DENIED(20),
INVALID_ARGUMENTS(30),
BAD_REQUEST_PARAMS(31),

11
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java

@ -248,6 +248,17 @@ public class AuditLogServiceImpl implements AuditLogService {
EntityRelation relation = extractParameter(EntityRelation.class, 0, additionalInfo);
actionData.set("relation", objectMapper.valueToTree(relation));
break;
case LOGIN:
case LOGOUT:
String clientAddress = extractParameter(String.class, 0, additionalInfo);
String browser = extractParameter(String.class, 1, additionalInfo);
String os = extractParameter(String.class, 2, additionalInfo);
String device = extractParameter(String.class, 3, additionalInfo);
actionData.put("clientAddress", clientAddress);
actionData.put("browser", browser);
actionData.put("os", os);
actionData.put("device", device);
break;
}
return actionData;
}

6
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -85,11 +85,5 @@ public abstract class DataValidator<D extends BaseData<?>> {
if (!expectedFields.containsAll(actualFields) || !actualFields.containsAll(expectedFields)) {
throw new DataValidationException("Provided json structure is different from stored one '" + actualNode + "'!");
}
for (String field : actualFields) {
if (!actualNode.get(field).isTextual()) {
throw new DataValidationException("Provided json structure can't contain non-text values '" + actualNode + "'!");
}
}
}
}

5
dao/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
@ -46,6 +47,10 @@ public interface UserService {
UserCredentials requestPasswordReset(TenantId tenantId, String email);
UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId);
UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
void deleteUser(TenantId tenantId, UserId userId);
TextPageData<User> findTenantAdmins(TenantId tenantId, TextPageLink pageLink);

19
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
@ -176,6 +177,24 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
return saveUserCredentials(tenantId, userCredentials);
}
@Override
public UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId) {
UserCredentials userCredentials = userCredentialsDao.findById(tenantId, userCredentialsId.getId());
if (!userCredentials.isEnabled()) {
throw new IncorrectParameterException("Unable to reset password for inactive user");
}
userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
return saveUserCredentials(tenantId, userCredentials);
}
@Override
public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
log.trace("Executing replaceUserCredentials [{}]", userCredentials);
userCredentialsValidator.validate(userCredentials, data -> tenantId);
userCredentialsDao.removeById(tenantId, userCredentials.getUuidId());
userCredentials.setId(null);
return userCredentialsDao.save(tenantId, userCredentials);
}
@Override
public void deleteUser(TenantId tenantId, UserId userId) {

9
dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java

@ -76,13 +76,4 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest {
adminSettings.setJsonValue(json);
adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings);
}
@Test(expected = DataValidationException.class)
public void testSaveAdminSettingsWithNonTextValue() {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(SYSTEM_TENANT_ID, "mail");
JsonNode json = adminSettings.getJsonValue();
((ObjectNode) json).put("timeout", 10000L);
adminSettings.setJsonValue(json);
adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings);
}
}

2
msa/js-executor/package-lock.json

@ -1,6 +1,6 @@
{
"name": "thingsboard-js-executor",
"version": "2.4.0",
"version": "2.4.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

2
msa/web-ui/package-lock.json

@ -1,6 +1,6 @@
{
"name": "thingsboard-web-ui",
"version": "2.4.0",
"version": "2.4.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

12
pom.xml

@ -89,6 +89,8 @@
<fst.version>2.57</fst.version>
<antlr.version>2.7.7</antlr.version>
<snakeyaml.version>1.23</snakeyaml.version>
<passay.version>1.5.0</passay.version>
<ua-parser.version>1.4.3</ua-parser.version>
</properties>
<modules>
@ -840,6 +842,16 @@
<artifactId>jts-core</artifactId>
<version>${jts.version}</version>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>${passay.version}</version>
</dependency>
<dependency>
<groupId>com.github.ua-parser</groupId>
<artifactId>uap-java</artifactId>
<version>${ua-parser.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

2
ui/package-lock.json

@ -1,6 +1,6 @@
{
"name": "thingsboard",
"version": "2.4.0",
"version": "2.4.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

19
ui/src/app/admin/admin.routes.js

@ -17,6 +17,7 @@
import generalSettingsTemplate from '../admin/general-settings.tpl.html';
import outgoingMailSettingsTemplate from '../admin/outgoing-mail-settings.tpl.html';
import securitySettingsTemplate from '../admin/security-settings.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@ -69,5 +70,23 @@ export default function AdminRoutes($stateProvider) {
ncyBreadcrumb: {
label: '{"icon": "mail", "label": "admin.outgoing-mail"}'
}
})
.state('home.settings.security-settings', {
url: '/security-settings',
module: 'private',
auth: ['SYS_ADMIN'],
views: {
"content@home": {
templateUrl: securitySettingsTemplate,
controllerAs: 'vm',
controller: 'SecuritySettingsController'
}
},
data: {
pageTitle: 'admin.security-settings'
},
ncyBreadcrumb: {
label: '{"icon": "security", "label": "admin.security-settings"}'
}
});
}

2
ui/src/app/admin/index.js

@ -22,6 +22,7 @@ import thingsboardToast from '../services/toast';
import AdminRoutes from './admin.routes';
import AdminController from './admin.controller';
import SecuritySettingsController from './security-settings.controller';
export default angular.module('thingsboard.admin', [
uiRouter,
@ -33,4 +34,5 @@ export default angular.module('thingsboard.admin', [
])
.config(AdminRoutes)
.controller('AdminController', AdminController)
.controller('SecuritySettingsController', SecuritySettingsController)
.name;

42
ui/src/app/admin/security-settings.controller.js

@ -0,0 +1,42 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './settings-card.scss';
/*@ngInject*/
export default function SecuritySettingsController(adminService, $mdExpansionPanel) {
var vm = this;
vm.$mdExpansionPanel = $mdExpansionPanel;
vm.save = save;
loadSettings();
function loadSettings() {
adminService.getSecuritySettings().then(function success(securitySettings) {
vm.securitySettings = securitySettings;
});
}
function save() {
adminService.saveSecuritySettings(vm.securitySettings).then(function success(securitySettings) {
vm.securitySettings = securitySettings;
vm.settingsForm.$setPristine();
});
}
}

125
ui/src/app/admin/security-settings.tpl.html

@ -0,0 +1,125 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div tb-help="'securitySettings'" help-container-id="help-container">
<md-card class="settings-card">
<md-card-title>
<md-card-title-text layout="row">
<span translate class="md-headline">admin.security-settings</span>
<span flex></span>
<div id="help-container"></div>
</md-card-title-text>
</md-card-title>
<md-progress-linear md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-card-content>
<form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm">
<fieldset ng-disabled="$root.loading">
<md-expansion-panel-group md-component-id="securitySettingsPanelGroup" auto-expand="true" multiple>
<md-expansion-panel md-component-id="passwordPolicyPanel" id="passwordPolicyPanel">
<md-expansion-panel-collapsed>
<div class="tb-panel-title" translate>admin.password-policy</div>
<md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-collapsed>
<md-expansion-panel-expanded>
<md-expansion-panel-header ng-click="vm.$mdExpansionPanel('passwordPolicyPanel').collapse()">
<div class="tb-panel-title" translate>admin.password-policy</div>
<md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-header>
<md-expansion-panel-content>
<md-input-container class="md-block">
<label translate>admin.minimum-password-length</label>
<input type="number"
step="1"
min="5"
max="50"
required
name="minimumPasswordLength"
ng-model="vm.securitySettings.passwordPolicy.minimumLength">
<div ng-messages="vm.settingsForm.minimumPasswordLength.$error">
<div translate ng-message="required">admin.minimum-password-length-required</div>
<div translate ng-message="min">admin.minimum-password-length-range</div>
<div translate ng-message="max">admin.minimum-password-length-range</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>admin.minimum-uppercase-letters</label>
<input type="number"
step="1"
min="0"
name="minimumUppercaseLetters"
ng-model="vm.securitySettings.passwordPolicy.minimumUppercaseLetters">
<div ng-messages="vm.settingsForm.minimumUppercaseLetters.$error">
<div translate ng-message="min">admin.minimum-uppercase-letters-range</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>admin.minimum-lowercase-letters</label>
<input type="number"
step="1"
min="0"
name="minimumLowercaseLetters"
ng-model="vm.securitySettings.passwordPolicy.minimumLowercaseLetters">
<div ng-messages="vm.settingsForm.minimumLowercaseLetters.$error">
<div translate ng-message="min">admin.minimum-lowercase-letters-range</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>admin.minimum-digits</label>
<input type="number"
step="1"
min="0"
name="minimumDigits"
ng-model="vm.securitySettings.passwordPolicy.minimumDigits">
<div ng-messages="vm.settingsForm.minimumDigits.$error">
<div translate ng-message="min">admin.minimum-digits-range</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>admin.minimum-special-characters</label>
<input type="number"
step="1"
min="0"
name="minimumSpecialCharacters"
ng-model="vm.securitySettings.passwordPolicy.minimumSpecialCharacters">
<div ng-messages="vm.settingsForm.minimumSpecialCharacters.$error">
<div translate ng-message="min">admin.minimum-special-characters-range</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>admin.password-expiration-period-days</label>
<input type="number"
step="1"
min="0"
name="passwordExpirationPeriodDays"
ng-model="vm.securitySettings.passwordPolicy.passwordExpirationPeriodDays">
<div ng-messages="vm.settingsForm.passwordExpirationPeriodDays.$error">
<div translate ng-message="min">admin.password-expiration-period-days-range</div>
</div>
</md-input-container>
</md-expansion-panel-content>
</md-expansion-panel-expanded>
</md-expansion-panel>
</md-expansion-panel-group>
<div layout="row" layout-align="end center" width="100%" layout-wrap>
<md-button ng-disabled="$root.loading || vm.settingsForm.$invalid || !vm.settingsForm.$dirty" type="submit" class="md-raised md-primary">{{'action.save' | translate}}</md-button>
</div>
</fieldset>
</form>
</md-card-content>
</md-card>
</div>

4
ui/src/app/admin/settings-card.scss

@ -20,4 +20,8 @@ md-card.settings-card {
@media (min-width: $layout-breakpoint-sm) {
width: 60%;
}
md-icon.md-expansion-panel-icon {
margin-right: 0;
}
}

24
ui/src/app/api/admin.service.js

@ -23,6 +23,8 @@ function AdminService($http, $q) {
var service = {
getAdminSettings: getAdminSettings,
saveAdminSettings: saveAdminSettings,
getSecuritySettings: getSecuritySettings,
saveSecuritySettings: saveSecuritySettings,
sendTestMail: sendTestMail,
checkUpdates: checkUpdates
}
@ -51,6 +53,28 @@ function AdminService($http, $q) {
return deferred.promise;
}
function getSecuritySettings() {
var deferred = $q.defer();
var url = '/api/admin/securitySettings';
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function saveSecuritySettings(securitySettings) {
var deferred = $q.defer();
var url = '/api/admin/securitySettings';
$http.post(url, securitySettings).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function sendTestMail(settings) {
var deferred = $q.defer();
var url = '/api/admin/settings/testMail';

6
ui/src/app/api/user.service.js

@ -141,7 +141,11 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
}
function logout() {
clearJwtToken(true);
$http.post('/api/auth/logout', null, {ignoreErrors: true}).then(function success() {
clearJwtToken(true);
}, function fail() {
clearJwtToken(true);
});
}
function clearJwtToken(doLogout) {

7
ui/src/app/common/types.constant.js

@ -20,6 +20,7 @@ export default angular.module('thingsboard.types', [])
general: 2,
authentication: 10,
jwtTokenExpired: 11,
credentialsExpired: 15,
permissionDenied: 20,
invalidArguments: 30,
badRequestParams: 31,
@ -212,6 +213,12 @@ export default angular.module('thingsboard.types', [])
},
"ALARM_CLEAR": {
name: "audit-log.type-alarm-clear"
},
"LOGIN": {
name: "audit-log.type-login"
},
"LOGOUT": {
name: "audit-log.type-logout"
}
},
auditLogActionStatus: {

2
ui/src/app/global-interceptor.service.js

@ -170,7 +170,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
var errorCode = rejectionErrorCode(rejection);
if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
return refreshTokenAndRetry(rejection);
} else {
} else if (errorCode !== getTypes().serverErrorCode.credentialsExpired) {
unhandled = true;
}
} else if (rejection.status === 403) {

1
ui/src/app/help/help-links.constant.js

@ -56,6 +56,7 @@ export default angular.module('thingsboard.help', [])
{
linksMap: {
outgoingMailSettings: helpBaseUrl + "/docs/user-guide/ui/mail-settings",
securitySettings: helpBaseUrl + "/docs/user-guide/ui/security-settings",
ruleEngine: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/overview/",
ruleNodeCheckRelation: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node",
ruleNodeJsFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node",

20
ui/src/app/locale/locale.constant-en_US.json

@ -84,7 +84,22 @@
"timeout-required": "Timeout is required.",
"timeout-invalid": "That doesn't look like a valid timeout.",
"enable-tls": "Enable TLS",
"send-test-mail": "Send test mail"
"send-test-mail": "Send test mail",
"security-settings": "Security settings",
"password-policy": "Password policy",
"minimum-password-length": "Minimum password length",
"minimum-password-length-required": "Minimum password length is required",
"minimum-password-length-range": "Minimum password length should be in a range from 5 to 50",
"minimum-uppercase-letters": "Minimum number of uppercase letters",
"minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative",
"minimum-lowercase-letters": "Minimum number of lowercase letters",
"minimum-lowercase-letters-range": "Minimum number of lowercase letters can't be negative",
"minimum-digits": "Minimum number of digits",
"minimum-digits-range": "Minimum number of digits can't be negative",
"minimum-special-characters": "Minimum number of special characters",
"minimum-special-characters-range": "Minimum number of special characters can't be negative",
"password-expiration-period-days": "Password expiration period in days",
"password-expiration-period-days-range": "Password expiration period in days can't be negative"
},
"alarm": {
"alarm": "Alarm",
@ -306,6 +321,8 @@
"type-relations-delete": "All relation deleted",
"type-alarm-ack": "Acknowledged",
"type-alarm-clear": "Cleared",
"type-login": "Login",
"type-logout": "Logout",
"status-success": "Success",
"status-failure": "Failure",
"audit-log-details": "Audit log details",
@ -1183,6 +1200,7 @@
"remember-me": "Remember me",
"forgot-password": "Forgot Password?",
"password-reset": "Password reset",
"expired-password-reset-message": "Your credentials has been expired! Please create new password.",
"new-password": "New password",
"new-password-again": "New password again",
"password-link-sent-message": "Password reset link was successfully sent!",

9
ui/src/app/login/login.controller.js

@ -20,7 +20,7 @@ import logoSvg from '../../svg/logo_title_white.svg';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function LoginController(toast, loginService, userService/*, $rootScope, $log, $translate*/) {
export default function LoginController(toast, loginService, userService, types, $state/*, $rootScope, $log, $translate*/) {
var vm = this;
vm.logoSvg = logoSvg;
@ -37,7 +37,7 @@ export default function LoginController(toast, loginService, userService/*, $roo
var token = response.data.token;
var refreshToken = response.data.refreshToken;
userService.setUserFromJwtToken(token, refreshToken, true);
}, function fail(/*response*/) {
}, function fail(response) {
/*if (response && response.data && response.data.message) {
toast.showError(response.data.message);
} else if (response && response.statusText) {
@ -45,6 +45,11 @@ export default function LoginController(toast, loginService, userService/*, $roo
} else {
toast.showError($translate.instant('error.unknown-error'));
}*/
if (response && response.data && response.data.errorCode) {
if (response.data.errorCode === types.serverErrorCode.credentialsExpired) {
$state.go('login.resetExpiredPassword', {resetToken: response.data.resetToken});
}
}
});
}

14
ui/src/app/login/login.routes.js

@ -63,6 +63,20 @@ export default function LoginRoutes($stateProvider) {
data: {
pageTitle: 'login.reset-password'
}
}).state('login.resetExpiredPassword', {
url: '/resetExpiredPassword?resetToken',
module: 'public',
views: {
"@": {
controller: 'ResetPasswordController',
controllerAs: 'vm',
templateUrl: resetPasswordTemplate
}
},
data: {
expiredPassword: true,
pageTitle: 'login.reset-password'
}
}).state('login.createPassword', {
url: '/createPassword?activateToken',
module: 'public',

4
ui/src/app/login/reset-password.controller.js

@ -14,7 +14,7 @@
* limitations under the License.
*/
/*@ngInject*/
export default function ResetPasswordController($stateParams, $translate, toast, loginService, userService) {
export default function ResetPasswordController($stateParams, $state, $translate, toast, loginService, userService) {
var vm = this;
vm.newPassword = '';
@ -22,6 +22,8 @@ export default function ResetPasswordController($stateParams, $translate, toast,
vm.resetPassword = resetPassword;
vm.isExpiredPassword = $state.$current.data.expiredPassword === true;
function resetPassword() {
if (vm.newPassword !== vm.newPassword2) {
toast.showError($translate.instant('login.passwords-mismatch-error'));

1
ui/src/app/login/reset-password.tpl.html

@ -20,6 +20,7 @@
<md-card-title>
<md-card-title-text>
<span translate class="md-headline">login.password-reset</span>
<span ng-if="vm.isExpiredPassword" translate class="md-subhead">login.expired-password-reset-message</span>
</md-card-title-text>
</md-card-title>
<md-progress-linear class="md-warn" style="z-index: 1; max-height: 5px; width: inherit; position: absolute"

13
ui/src/app/services/menu.service.js

@ -82,7 +82,7 @@ function Menu(userService, $state, $rootScope) {
name: 'admin.system-settings',
type: 'toggle',
state: 'home.settings',
height: '80px',
height: '120px',
icon: 'settings',
pages: [
{
@ -96,6 +96,12 @@ function Menu(userService, $state, $rootScope) {
type: 'link',
state: 'home.settings.outgoing-mail',
icon: 'mail'
},
{
name: 'admin.security-settings',
type: 'link',
state: 'home.settings.security-settings',
icon: 'security'
}
]
}];
@ -132,6 +138,11 @@ function Menu(userService, $state, $rootScope) {
name: 'admin.outgoing-mail',
icon: 'mail',
state: 'home.settings.outgoing-mail'
},
{
name: 'admin.security-settings',
icon: 'security',
state: 'home.settings.security-settings'
}
]
}];

Loading…
Cancel
Save