Commit 8d5d8b2c234e2f71eeec6a04b6daab572cb50773

Authored by Igor Kulikov
1 parent b482238e

Password policy setting. Login/Logout audit log.

Showing 44 changed files with 973 additions and 60 deletions
@@ -272,6 +272,14 @@ @@ -272,6 +272,14 @@
272 <groupId>io.springfox.ui</groupId> 272 <groupId>io.springfox.ui</groupId>
273 <artifactId>springfox-swagger-ui-rfc6570</artifactId> 273 <artifactId>springfox-swagger-ui-rfc6570</artifactId>
274 </dependency> 274 </dependency>
  275 + <dependency>
  276 + <groupId>org.passay</groupId>
  277 + <artifactId>passay</artifactId>
  278 + </dependency>
  279 + <dependency>
  280 + <groupId>com.github.ua-parser</groupId>
  281 + <artifactId>uap-java</artifactId>
  282 + </dependency>
275 </dependencies> 283 </dependencies>
276 284
277 <build> 285 <build>
@@ -28,8 +28,10 @@ import org.thingsboard.server.common.data.AdminSettings; @@ -28,8 +28,10 @@ import org.thingsboard.server.common.data.AdminSettings;
28 import org.thingsboard.server.common.data.exception.ThingsboardException; 28 import org.thingsboard.server.common.data.exception.ThingsboardException;
29 import org.thingsboard.server.common.data.id.TenantId; 29 import org.thingsboard.server.common.data.id.TenantId;
30 import org.thingsboard.server.dao.settings.AdminSettingsService; 30 import org.thingsboard.server.dao.settings.AdminSettingsService;
  31 +import org.thingsboard.server.service.security.model.SecuritySettings;
31 import org.thingsboard.server.service.security.permission.Operation; 32 import org.thingsboard.server.service.security.permission.Operation;
32 import org.thingsboard.server.service.security.permission.Resource; 33 import org.thingsboard.server.service.security.permission.Resource;
  34 +import org.thingsboard.server.service.security.system.SystemSecurityService;
33 import org.thingsboard.server.service.update.UpdateService; 35 import org.thingsboard.server.service.update.UpdateService;
34 import org.thingsboard.server.service.update.model.UpdateMessage; 36 import org.thingsboard.server.service.update.model.UpdateMessage;
35 37
@@ -44,6 +46,9 @@ public class AdminController extends BaseController { @@ -44,6 +46,9 @@ public class AdminController extends BaseController {
44 private AdminSettingsService adminSettingsService; 46 private AdminSettingsService adminSettingsService;
45 47
46 @Autowired 48 @Autowired
  49 + private SystemSecurityService systemSecurityService;
  50 +
  51 + @Autowired
47 private UpdateService updateService; 52 private UpdateService updateService;
48 53
49 @PreAuthorize("hasAuthority('SYS_ADMIN')") 54 @PreAuthorize("hasAuthority('SYS_ADMIN')")
@@ -75,6 +80,31 @@ public class AdminController extends BaseController { @@ -75,6 +80,31 @@ public class AdminController extends BaseController {
75 } 80 }
76 81
77 @PreAuthorize("hasAuthority('SYS_ADMIN')") 82 @PreAuthorize("hasAuthority('SYS_ADMIN')")
  83 + @RequestMapping(value = "/securitySettings", method = RequestMethod.GET)
  84 + @ResponseBody
  85 + public SecuritySettings getSecuritySettings() throws ThingsboardException {
  86 + try {
  87 + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
  88 + return checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID));
  89 + } catch (Exception e) {
  90 + throw handleException(e);
  91 + }
  92 + }
  93 +
  94 + @PreAuthorize("hasAuthority('SYS_ADMIN')")
  95 + @RequestMapping(value = "/securitySettings", method = RequestMethod.POST)
  96 + @ResponseBody
  97 + public SecuritySettings saveSecuritySettings(@RequestBody SecuritySettings securitySettings) throws ThingsboardException {
  98 + try {
  99 + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
  100 + securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(TenantId.SYS_TENANT_ID, securitySettings));
  101 + return securitySettings;
  102 + } catch (Exception e) {
  103 + throw handleException(e);
  104 + }
  105 + }
  106 +
  107 + @PreAuthorize("hasAuthority('SYS_ADMIN')")
78 @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) 108 @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST)
79 public void sendTestMail(@RequestBody AdminSettings adminSettings) throws ThingsboardException { 109 public void sendTestMail(@RequestBody AdminSettings adminSettings) throws ThingsboardException {
80 try { 110 try {
@@ -24,6 +24,7 @@ import org.springframework.http.HttpHeaders; @@ -24,6 +24,7 @@ import org.springframework.http.HttpHeaders;
24 import org.springframework.http.HttpStatus; 24 import org.springframework.http.HttpStatus;
25 import org.springframework.http.ResponseEntity; 25 import org.springframework.http.ResponseEntity;
26 import org.springframework.security.access.prepost.PreAuthorize; 26 import org.springframework.security.access.prepost.PreAuthorize;
  27 +import org.springframework.security.core.Authentication;
27 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 28 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
28 import org.springframework.web.bind.annotation.RequestBody; 29 import org.springframework.web.bind.annotation.RequestBody;
29 import org.springframework.web.bind.annotation.RequestMapping; 30 import org.springframework.web.bind.annotation.RequestMapping;
@@ -34,15 +35,24 @@ import org.springframework.web.bind.annotation.ResponseStatus; @@ -34,15 +35,24 @@ import org.springframework.web.bind.annotation.ResponseStatus;
34 import org.springframework.web.bind.annotation.RestController; 35 import org.springframework.web.bind.annotation.RestController;
35 import org.thingsboard.rule.engine.api.MailService; 36 import org.thingsboard.rule.engine.api.MailService;
36 import org.thingsboard.server.common.data.User; 37 import org.thingsboard.server.common.data.User;
  38 +import org.thingsboard.server.common.data.audit.ActionType;
37 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 39 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
38 import org.thingsboard.server.common.data.exception.ThingsboardException; 40 import org.thingsboard.server.common.data.exception.ThingsboardException;
39 import org.thingsboard.server.common.data.id.TenantId; 41 import org.thingsboard.server.common.data.id.TenantId;
40 import org.thingsboard.server.common.data.security.UserCredentials; 42 import org.thingsboard.server.common.data.security.UserCredentials;
  43 +import org.thingsboard.server.dao.audit.AuditLogService;
41 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; 44 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
  45 +import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
  46 +import org.thingsboard.server.service.security.model.SecuritySettings;
42 import org.thingsboard.server.service.security.model.SecurityUser; 47 import org.thingsboard.server.service.security.model.SecurityUser;
  48 +import org.thingsboard.server.service.security.model.UserPasswordPolicy;
43 import org.thingsboard.server.service.security.model.UserPrincipal; 49 import org.thingsboard.server.service.security.model.UserPrincipal;
44 import org.thingsboard.server.service.security.model.token.JwtToken; 50 import org.thingsboard.server.service.security.model.token.JwtToken;
45 import org.thingsboard.server.service.security.model.token.JwtTokenFactory; 51 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
  52 +import org.thingsboard.server.service.security.permission.Operation;
  53 +import org.thingsboard.server.service.security.permission.Resource;
  54 +import org.thingsboard.server.service.security.system.SystemSecurityService;
  55 +import ua_parser.Client;
46 56
47 import javax.servlet.http.HttpServletRequest; 57 import javax.servlet.http.HttpServletRequest;
48 import java.net.URI; 58 import java.net.URI;
@@ -65,6 +75,12 @@ public class AuthController extends BaseController { @@ -65,6 +75,12 @@ public class AuthController extends BaseController {
65 @Autowired 75 @Autowired
66 private MailService mailService; 76 private MailService mailService;
67 77
  78 + @Autowired
  79 + private SystemSecurityService systemSecurityService;
  80 +
  81 + @Autowired
  82 + private AuditLogService auditLogService;
  83 +
68 @PreAuthorize("isAuthenticated()") 84 @PreAuthorize("isAuthenticated()")
69 @RequestMapping(value = "/auth/user", method = RequestMethod.GET) 85 @RequestMapping(value = "/auth/user", method = RequestMethod.GET)
70 public @ResponseBody User getUser() throws ThingsboardException { 86 public @ResponseBody User getUser() throws ThingsboardException {
@@ -77,6 +93,13 @@ public class AuthController extends BaseController { @@ -77,6 +93,13 @@ public class AuthController extends BaseController {
77 } 93 }
78 94
79 @PreAuthorize("isAuthenticated()") 95 @PreAuthorize("isAuthenticated()")
  96 + @RequestMapping(value = "/auth/logout", method = RequestMethod.POST)
  97 + @ResponseStatus(value = HttpStatus.OK)
  98 + public void logout(HttpServletRequest request) throws ThingsboardException {
  99 + logLogoutAction(request);
  100 + }
  101 +
  102 + @PreAuthorize("isAuthenticated()")
80 @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) 103 @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
81 @ResponseStatus(value = HttpStatus.OK) 104 @ResponseStatus(value = HttpStatus.OK)
82 public void changePassword ( 105 public void changePassword (
@@ -89,8 +112,24 @@ public class AuthController extends BaseController { @@ -89,8 +112,24 @@ public class AuthController extends BaseController {
89 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { 112 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
90 throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); 113 throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
91 } 114 }
  115 + systemSecurityService.validatePassword(securityUser.getTenantId(), newPassword);
  116 + if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) {
  117 + throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
  118 + }
92 userCredentials.setPassword(passwordEncoder.encode(newPassword)); 119 userCredentials.setPassword(passwordEncoder.encode(newPassword));
93 - userService.saveUserCredentials(securityUser.getTenantId(), userCredentials); 120 + userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials);
  121 + } catch (Exception e) {
  122 + throw handleException(e);
  123 + }
  124 + }
  125 +
  126 + @RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET)
  127 + @ResponseBody
  128 + public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException {
  129 + try {
  130 + SecuritySettings securitySettings =
  131 + checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID));
  132 + return securitySettings.getPasswordPolicy();
94 } catch (Exception e) { 133 } catch (Exception e) {
95 throw handleException(e); 134 throw handleException(e);
96 } 135 }
@@ -167,6 +206,7 @@ public class AuthController extends BaseController { @@ -167,6 +206,7 @@ public class AuthController extends BaseController {
167 try { 206 try {
168 String activateToken = activateRequest.get("activateToken").asText(); 207 String activateToken = activateRequest.get("activateToken").asText();
169 String password = activateRequest.get("password").asText(); 208 String password = activateRequest.get("password").asText();
  209 + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password);
170 String encodedPassword = passwordEncoder.encode(password); 210 String encodedPassword = passwordEncoder.encode(password);
171 UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword); 211 UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword);
172 User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId()); 212 User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
@@ -206,10 +246,14 @@ public class AuthController extends BaseController { @@ -206,10 +246,14 @@ public class AuthController extends BaseController {
206 String password = resetPasswordRequest.get("password").asText(); 246 String password = resetPasswordRequest.get("password").asText();
207 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); 247 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
208 if (userCredentials != null) { 248 if (userCredentials != null) {
  249 + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password);
  250 + if (passwordEncoder.matches(password, userCredentials.getPassword())) {
  251 + throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
  252 + }
209 String encodedPassword = passwordEncoder.encode(password); 253 String encodedPassword = passwordEncoder.encode(password);
210 userCredentials.setPassword(encodedPassword); 254 userCredentials.setPassword(encodedPassword);
211 userCredentials.setResetToken(null); 255 userCredentials.setResetToken(null);
212 - userCredentials = userService.saveUserCredentials(TenantId.SYS_TENANT_ID, userCredentials); 256 + userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials);
213 User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId()); 257 User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
214 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); 258 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
215 SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal); 259 SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
@@ -234,4 +278,54 @@ public class AuthController extends BaseController { @@ -234,4 +278,54 @@ public class AuthController extends BaseController {
234 } 278 }
235 } 279 }
236 280
  281 + private void logLogoutAction(HttpServletRequest request) throws ThingsboardException {
  282 + try {
  283 + SecurityUser user = getCurrentUser();
  284 + RestAuthenticationDetails details = new RestAuthenticationDetails(request);
  285 + String clientAddress = details.getClientAddress();
  286 + String browser = "Unknown";
  287 + String os = "Unknown";
  288 + String device = "Unknown";
  289 + if (details.getUserAgent() != null) {
  290 + Client userAgent = details.getUserAgent();
  291 + if (userAgent.userAgent != null) {
  292 + browser = userAgent.userAgent.family;
  293 + if (userAgent.userAgent.major != null) {
  294 + browser += " " + userAgent.userAgent.major;
  295 + if (userAgent.userAgent.minor != null) {
  296 + browser += "." + userAgent.userAgent.minor;
  297 + if (userAgent.userAgent.patch != null) {
  298 + browser += "." + userAgent.userAgent.patch;
  299 + }
  300 + }
  301 + }
  302 + }
  303 + if (userAgent.os != null) {
  304 + os = userAgent.os.family;
  305 + if (userAgent.os.major != null) {
  306 + os += " " + userAgent.os.major;
  307 + if (userAgent.os.minor != null) {
  308 + os += "." + userAgent.os.minor;
  309 + if (userAgent.os.patch != null) {
  310 + os += "." + userAgent.os.patch;
  311 + if (userAgent.os.patchMinor != null) {
  312 + os += "." + userAgent.os.patchMinor;
  313 + }
  314 + }
  315 + }
  316 + }
  317 + }
  318 + if (userAgent.device != null) {
  319 + device = userAgent.device.family;
  320 + }
  321 + }
  322 + auditLogService.logEntityAction(
  323 + user.getTenantId(), user.getCustomerId(), user.getId(),
  324 + user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device);
  325 +
  326 + } catch (Exception e) {
  327 + throw handleException(e);
  328 + }
  329 + }
  330 +
237 } 331 }
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.exception;
  17 +
  18 +import org.springframework.http.HttpStatus;
  19 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
  20 +
  21 +public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse {
  22 +
  23 + private final String resetToken;
  24 +
  25 + protected ThingsboardCredentialsExpiredResponse(String message, String resetToken) {
  26 + super(message, ThingsboardErrorCode.CREDENTIALS_EXPIRED, HttpStatus.UNAUTHORIZED);
  27 + this.resetToken = resetToken;
  28 + }
  29 +
  30 + public static ThingsboardCredentialsExpiredResponse of(final String message, final String resetToken) {
  31 + return new ThingsboardCredentialsExpiredResponse(message, resetToken);
  32 + }
  33 +
  34 + public String getResetToken() {
  35 + return resetToken;
  36 + }
  37 +}
@@ -18,10 +18,12 @@ package org.thingsboard.server.exception; @@ -18,10 +18,12 @@ package org.thingsboard.server.exception;
18 import com.fasterxml.jackson.databind.ObjectMapper; 18 import com.fasterxml.jackson.databind.ObjectMapper;
19 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
20 import org.springframework.beans.factory.annotation.Autowired; 20 import org.springframework.beans.factory.annotation.Autowired;
  21 +import org.springframework.http.HttpHeaders;
21 import org.springframework.http.HttpStatus; 22 import org.springframework.http.HttpStatus;
22 import org.springframework.http.MediaType; 23 import org.springframework.http.MediaType;
23 import org.springframework.security.access.AccessDeniedException; 24 import org.springframework.security.access.AccessDeniedException;
24 import org.springframework.security.authentication.BadCredentialsException; 25 import org.springframework.security.authentication.BadCredentialsException;
  26 +import org.springframework.security.authentication.CredentialsExpiredException;
25 import org.springframework.security.core.AuthenticationException; 27 import org.springframework.security.core.AuthenticationException;
26 import org.springframework.security.web.access.AccessDeniedHandler; 28 import org.springframework.security.web.access.AccessDeniedHandler;
27 import org.springframework.stereotype.Component; 29 import org.springframework.stereotype.Component;
@@ -31,11 +33,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -31,11 +33,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
31 import org.thingsboard.server.common.msg.tools.TbRateLimitsException; 33 import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
32 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; 34 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
33 import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; 35 import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
  36 +import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
34 37
35 import javax.servlet.ServletException; 38 import javax.servlet.ServletException;
36 import javax.servlet.http.HttpServletRequest; 39 import javax.servlet.http.HttpServletRequest;
37 import javax.servlet.http.HttpServletResponse; 40 import javax.servlet.http.HttpServletResponse;
38 import java.io.IOException; 41 import java.io.IOException;
  42 +import java.net.URI;
  43 +import java.net.URISyntaxException;
39 44
40 @Component 45 @Component
41 @Slf4j 46 @Slf4j
@@ -141,8 +146,13 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { @@ -141,8 +146,13 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
141 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); 146 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
142 } else if (authenticationException instanceof AuthMethodNotSupportedException) { 147 } else if (authenticationException instanceof AuthMethodNotSupportedException) {
143 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 148 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
  149 + } else if (authenticationException instanceof UserPasswordExpiredException) {
  150 + UserPasswordExpiredException expiredException = (UserPasswordExpiredException)authenticationException;
  151 + String resetToken = expiredException.getResetToken();
  152 + mapper.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken));
  153 + } else {
  154 + mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
144 } 155 }
145 - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));  
146 } 156 }
147 157
148 } 158 }
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package org.thingsboard.server.service.security.auth.rest;
  18 +
  19 +import lombok.Data;
  20 +import ua_parser.Client;
  21 +import ua_parser.Parser;
  22 +
  23 +import javax.servlet.http.HttpServletRequest;
  24 +import java.io.IOException;
  25 +import java.io.Serializable;
  26 +
  27 +@Data
  28 +public class RestAuthenticationDetails implements Serializable {
  29 +
  30 + private final String clientAddress;
  31 + private final Client userAgent;
  32 +
  33 + public RestAuthenticationDetails(HttpServletRequest request) {
  34 + this.clientAddress = getClientIP(request);
  35 + this.userAgent = getUserAgent(request);
  36 + }
  37 +
  38 + private static String getClientIP(HttpServletRequest request) {
  39 + String xfHeader = request.getHeader("X-Forwarded-For");
  40 + if (xfHeader == null) {
  41 + return request.getRemoteAddr();
  42 + }
  43 + return xfHeader.split(",")[0];
  44 + }
  45 +
  46 + private static Client getUserAgent(HttpServletRequest request) {
  47 + try {
  48 + Parser uaParser = new Parser();
  49 + return uaParser.parse(request.getHeader("User-Agent"));
  50 + } catch (IOException e) {
  51 + return new Client(null, null, null);
  52 + }
  53 + }
  54 +}
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package org.thingsboard.server.service.security.auth.rest;
  18 +
  19 +import org.springframework.security.authentication.AuthenticationDetailsSource;
  20 +
  21 +import javax.servlet.http.HttpServletRequest;
  22 +
  23 +public class RestAuthenticationDetailsSource implements
  24 + AuthenticationDetailsSource<HttpServletRequest, RestAuthenticationDetails> {
  25 +
  26 + public RestAuthenticationDetails buildDetails(HttpServletRequest context) {
  27 + return new RestAuthenticationDetails(context);
  28 + }
  29 +}
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.service.security.auth.rest; 16 package org.thingsboard.server.service.security.auth.rest;
17 17
  18 +import lombok.extern.slf4j.Slf4j;
18 import org.springframework.beans.factory.annotation.Autowired; 19 import org.springframework.beans.factory.annotation.Autowired;
19 import org.springframework.security.authentication.AuthenticationProvider; 20 import org.springframework.security.authentication.AuthenticationProvider;
20 import org.springframework.security.authentication.BadCredentialsException; 21 import org.springframework.security.authentication.BadCredentialsException;
@@ -29,31 +30,41 @@ import org.springframework.stereotype.Component; @@ -29,31 +30,41 @@ import org.springframework.stereotype.Component;
29 import org.springframework.util.Assert; 30 import org.springframework.util.Assert;
30 import org.thingsboard.server.common.data.Customer; 31 import org.thingsboard.server.common.data.Customer;
31 import org.thingsboard.server.common.data.User; 32 import org.thingsboard.server.common.data.User;
  33 +import org.thingsboard.server.common.data.audit.ActionType;
32 import org.thingsboard.server.common.data.id.CustomerId; 34 import org.thingsboard.server.common.data.id.CustomerId;
33 import org.thingsboard.server.common.data.id.EntityId; 35 import org.thingsboard.server.common.data.id.EntityId;
34 import org.thingsboard.server.common.data.id.TenantId; 36 import org.thingsboard.server.common.data.id.TenantId;
35 import org.thingsboard.server.common.data.id.UserId; 37 import org.thingsboard.server.common.data.id.UserId;
36 import org.thingsboard.server.common.data.security.Authority; 38 import org.thingsboard.server.common.data.security.Authority;
37 import org.thingsboard.server.common.data.security.UserCredentials; 39 import org.thingsboard.server.common.data.security.UserCredentials;
  40 +import org.thingsboard.server.dao.audit.AuditLogService;
38 import org.thingsboard.server.dao.customer.CustomerService; 41 import org.thingsboard.server.dao.customer.CustomerService;
39 import org.thingsboard.server.dao.user.UserService; 42 import org.thingsboard.server.dao.user.UserService;
40 import org.thingsboard.server.service.security.model.SecurityUser; 43 import org.thingsboard.server.service.security.model.SecurityUser;
41 import org.thingsboard.server.service.security.model.UserPrincipal; 44 import org.thingsboard.server.service.security.model.UserPrincipal;
  45 +import org.thingsboard.server.service.security.system.SystemSecurityService;
  46 +import ua_parser.Client;
42 47
43 import java.util.UUID; 48 import java.util.UUID;
44 49
45 @Component 50 @Component
  51 +@Slf4j
46 public class RestAuthenticationProvider implements AuthenticationProvider { 52 public class RestAuthenticationProvider implements AuthenticationProvider {
47 53
48 - private final BCryptPasswordEncoder encoder; 54 + private final SystemSecurityService systemSecurityService;
49 private final UserService userService; 55 private final UserService userService;
50 private final CustomerService customerService; 56 private final CustomerService customerService;
  57 + private final AuditLogService auditLogService;
51 58
52 @Autowired 59 @Autowired
53 - public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) { 60 + public RestAuthenticationProvider(final UserService userService,
  61 + final CustomerService customerService,
  62 + final SystemSecurityService systemSecurityService,
  63 + final AuditLogService auditLogService) {
54 this.userService = userService; 64 this.userService = userService;
55 this.customerService = customerService; 65 this.customerService = customerService;
56 - this.encoder = encoder; 66 + this.systemSecurityService = systemSecurityService;
  67 + this.auditLogService = auditLogService;
57 } 68 }
58 69
59 @Override 70 @Override
@@ -69,37 +80,40 @@ public class RestAuthenticationProvider implements AuthenticationProvider { @@ -69,37 +80,40 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
69 if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { 80 if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
70 String username = userPrincipal.getValue(); 81 String username = userPrincipal.getValue();
71 String password = (String) authentication.getCredentials(); 82 String password = (String) authentication.getCredentials();
72 - return authenticateByUsernameAndPassword(userPrincipal, username, password); 83 + return authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
73 } else { 84 } else {
74 String publicId = userPrincipal.getValue(); 85 String publicId = userPrincipal.getValue();
75 return authenticateByPublicId(userPrincipal, publicId); 86 return authenticateByPublicId(userPrincipal, publicId);
76 } 87 }
77 } 88 }
78 89
79 - private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) { 90 + private Authentication authenticateByUsernameAndPassword(Authentication authentication, UserPrincipal userPrincipal, String username, String password) {
80 User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username); 91 User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username);
81 if (user == null) { 92 if (user == null) {
82 throw new UsernameNotFoundException("User not found: " + username); 93 throw new UsernameNotFoundException("User not found: " + username);
83 } 94 }
84 95
85 - UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId());  
86 - if (userCredentials == null) {  
87 - throw new UsernameNotFoundException("User credentials not found");  
88 - } 96 + try {
89 97
90 - if (!userCredentials.isEnabled()) {  
91 - throw new DisabledException("User is not active");  
92 - } 98 + UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId());
  99 + if (userCredentials == null) {
  100 + throw new UsernameNotFoundException("User credentials not found");
  101 + }
93 102
94 - if (!encoder.matches(password, userCredentials.getPassword())) {  
95 - throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");  
96 - } 103 + systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, password);
97 104
98 - if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); 105 + if (user.getAuthority() == null)
  106 + throw new InsufficientAuthenticationException("User has no authority assigned");
99 107
100 - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); 108 + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
101 109
102 - return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); 110 + logLoginAction(user, authentication, null);
  111 +
  112 + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
  113 + } catch (Exception e) {
  114 + logLoginAction(user, authentication, e);
  115 + throw e;
  116 + }
103 } 117 }
104 118
105 private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) { 119 private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
@@ -133,4 +147,53 @@ public class RestAuthenticationProvider implements AuthenticationProvider { @@ -133,4 +147,53 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
133 public boolean supports(Class<?> authentication) { 147 public boolean supports(Class<?> authentication) {
134 return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); 148 return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
135 } 149 }
  150 +
  151 + private void logLoginAction(User user, Authentication authentication, Exception e) {
  152 + String clientAddress = "Unknown";
  153 + String browser = "Unknown";
  154 + String os = "Unknown";
  155 + String device = "Unknown";
  156 + if (authentication != null && authentication.getDetails() != null) {
  157 + if (authentication.getDetails() instanceof RestAuthenticationDetails) {
  158 + RestAuthenticationDetails details = (RestAuthenticationDetails)authentication.getDetails();
  159 + clientAddress = details.getClientAddress();
  160 + if (details.getUserAgent() != null) {
  161 + Client userAgent = details.getUserAgent();
  162 + if (userAgent.userAgent != null) {
  163 + browser = userAgent.userAgent.family;
  164 + if (userAgent.userAgent.major != null) {
  165 + browser += " " + userAgent.userAgent.major;
  166 + if (userAgent.userAgent.minor != null) {
  167 + browser += "." + userAgent.userAgent.minor;
  168 + if (userAgent.userAgent.patch != null) {
  169 + browser += "." + userAgent.userAgent.patch;
  170 + }
  171 + }
  172 + }
  173 + }
  174 + if (userAgent.os != null) {
  175 + os = userAgent.os.family;
  176 + if (userAgent.os.major != null) {
  177 + os += " " + userAgent.os.major;
  178 + if (userAgent.os.minor != null) {
  179 + os += "." + userAgent.os.minor;
  180 + if (userAgent.os.patch != null) {
  181 + os += "." + userAgent.os.patch;
  182 + if (userAgent.os.patchMinor != null) {
  183 + os += "." + userAgent.os.patchMinor;
  184 + }
  185 + }
  186 + }
  187 + }
  188 + }
  189 + if (userAgent.device != null) {
  190 + device = userAgent.device.family;
  191 + }
  192 + }
  193 + }
  194 + }
  195 + auditLogService.logEntityAction(
  196 + user.getTenantId(), user.getCustomerId(), user.getId(),
  197 + user.getName(), user.getId(), null, ActionType.LOGIN, e, clientAddress, browser, os, device);
  198 + }
136 } 199 }
@@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
19 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
20 import org.apache.commons.lang3.StringUtils; 20 import org.apache.commons.lang3.StringUtils;
21 import org.springframework.http.HttpMethod; 21 import org.springframework.http.HttpMethod;
  22 +import org.springframework.security.authentication.AuthenticationDetailsSource;
22 import org.springframework.security.authentication.AuthenticationServiceException; 23 import org.springframework.security.authentication.AuthenticationServiceException;
23 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 24 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
24 import org.springframework.security.core.Authentication; 25 import org.springframework.security.core.Authentication;
@@ -27,6 +28,7 @@ import org.springframework.security.core.context.SecurityContextHolder; @@ -27,6 +28,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
27 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 28 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
28 import org.springframework.security.web.authentication.AuthenticationFailureHandler; 29 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
29 import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 30 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
  31 +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
30 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; 32 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
31 import org.thingsboard.server.service.security.model.UserPrincipal; 33 import org.thingsboard.server.service.security.model.UserPrincipal;
32 34
@@ -39,6 +41,8 @@ import java.io.IOException; @@ -39,6 +41,8 @@ import java.io.IOException;
39 @Slf4j 41 @Slf4j
40 public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { 42 public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
41 43
  44 + private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new RestAuthenticationDetailsSource();
  45 +
42 private final AuthenticationSuccessHandler successHandler; 46 private final AuthenticationSuccessHandler successHandler;
43 private final AuthenticationFailureHandler failureHandler; 47 private final AuthenticationFailureHandler failureHandler;
44 48
@@ -76,7 +80,7 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF @@ -76,7 +80,7 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF
76 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername()); 80 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername());
77 81
78 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword()); 82 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword());
79 - 83 + token.setDetails(authenticationDetailsSource.buildDetails(request));
80 return this.getAuthenticationManager().authenticate(token); 84 return this.getAuthenticationManager().authenticate(token);
81 } 85 }
82 86
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.exception;
  17 +
  18 +import org.springframework.security.authentication.CredentialsExpiredException;
  19 +
  20 +public class UserPasswordExpiredException extends CredentialsExpiredException {
  21 +
  22 + private final String resetToken;
  23 +
  24 + public UserPasswordExpiredException(String msg, String resetToken) {
  25 + super(msg);
  26 + this.resetToken = resetToken;
  27 + }
  28 +
  29 + public String getResetToken() {
  30 + return resetToken;
  31 + }
  32 +
  33 +}
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.model;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class SecuritySettings {
  22 +
  23 + private UserPasswordPolicy passwordPolicy;
  24 +
  25 +}
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.model;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class UserPasswordPolicy {
  22 +
  23 + private Integer minimumLength;
  24 + private Integer minimumUppercaseLetters;
  25 + private Integer minimumLowercaseLetters;
  26 + private Integer minimumDigits;
  27 + private Integer minimumSpecialCharacters;
  28 +
  29 + private Integer passwordExpirationPeriodDays;
  30 +
  31 +}
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.system;
  17 +
  18 +import com.fasterxml.jackson.databind.ObjectMapper;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.passay.*;
  21 +import org.springframework.beans.factory.annotation.Autowired;
  22 +import org.springframework.cache.annotation.CacheEvict;
  23 +import org.springframework.cache.annotation.Cacheable;
  24 +import org.springframework.security.authentication.BadCredentialsException;
  25 +import org.springframework.security.authentication.CredentialsExpiredException;
  26 +import org.springframework.security.authentication.DisabledException;
  27 +import org.springframework.security.core.AuthenticationException;
  28 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  29 +import org.springframework.stereotype.Service;
  30 +import org.thingsboard.server.common.data.AdminSettings;
  31 +import org.thingsboard.server.common.data.id.TenantId;
  32 +import org.thingsboard.server.common.data.security.UserCredentials;
  33 +import org.thingsboard.server.dao.exception.DataValidationException;
  34 +import org.thingsboard.server.dao.settings.AdminSettingsService;
  35 +import org.thingsboard.server.dao.user.UserService;
  36 +import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
  37 +import org.thingsboard.server.service.security.model.SecuritySettings;
  38 +import org.thingsboard.server.service.security.model.UserPasswordPolicy;
  39 +
  40 +import javax.annotation.Resource;
  41 +import java.util.ArrayList;
  42 +import java.util.List;
  43 +import java.util.concurrent.TimeUnit;
  44 +
  45 +import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;
  46 +import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
  47 +
  48 +@Service
  49 +@Slf4j
  50 +public class DefaultSystemSecurityService implements SystemSecurityService {
  51 +
  52 + private static final ObjectMapper objectMapper = new ObjectMapper();
  53 +
  54 + @Autowired
  55 + private AdminSettingsService adminSettingsService;
  56 +
  57 + @Autowired
  58 + private BCryptPasswordEncoder encoder;
  59 +
  60 + @Autowired
  61 + private UserService userService;
  62 +
  63 + @Resource
  64 + private SystemSecurityService self;
  65 +
  66 + @Cacheable(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
  67 + @Override
  68 + public SecuritySettings getSecuritySettings(TenantId tenantId) {
  69 + SecuritySettings securitySettings = null;
  70 + AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings");
  71 + if (adminSettings != null) {
  72 + try {
  73 + securitySettings = objectMapper.treeToValue(adminSettings.getJsonValue(), SecuritySettings.class);
  74 + } catch (Exception e) {
  75 + throw new RuntimeException("Failed to load security settings!", e);
  76 + }
  77 + } else {
  78 + securitySettings = new SecuritySettings();
  79 + securitySettings.setPasswordPolicy(new UserPasswordPolicy());
  80 + securitySettings.getPasswordPolicy().setMinimumLength(6);
  81 + }
  82 + return securitySettings;
  83 + }
  84 +
  85 + @CacheEvict(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
  86 + @Override
  87 + public SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings) {
  88 + AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings");
  89 + if (adminSettings == null) {
  90 + adminSettings = new AdminSettings();
  91 + adminSettings.setKey("securitySettings");
  92 + }
  93 + adminSettings.setJsonValue(objectMapper.valueToTree(securitySettings));
  94 + AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(tenantId, adminSettings);
  95 + try {
  96 + return objectMapper.treeToValue(savedAdminSettings.getJsonValue(), SecuritySettings.class);
  97 + } catch (Exception e) {
  98 + throw new RuntimeException("Failed to load security settings!", e);
  99 + }
  100 + }
  101 +
  102 + @Override
  103 + public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String password) throws AuthenticationException {
  104 +
  105 + if (!encoder.matches(password, userCredentials.getPassword())) {
  106 + throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
  107 + }
  108 +
  109 + if (!userCredentials.isEnabled()) {
  110 + throw new DisabledException("User is not active");
  111 + }
  112 +
  113 + SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
  114 + if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
  115 + if ((userCredentials.getCreatedTime()
  116 + + TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays()))
  117 + < System.currentTimeMillis()) {
  118 + userCredentials = userService.requestExpiredPasswordReset(tenantId, userCredentials.getId());
  119 + throw new UserPasswordExpiredException("User password expired!", userCredentials.getResetToken());
  120 + }
  121 + }
  122 + }
  123 +
  124 + @Override
  125 + public void validatePassword(TenantId tenantId, String password) throws DataValidationException {
  126 + SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
  127 + UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
  128 +
  129 + List<Rule> passwordRules = new ArrayList<>();
  130 + passwordRules.add(new LengthRule(passwordPolicy.getMinimumLength(), Integer.MAX_VALUE));
  131 + if (isPositiveInteger(passwordPolicy.getMinimumUppercaseLetters())) {
  132 + passwordRules.add(new CharacterRule(EnglishCharacterData.UpperCase, passwordPolicy.getMinimumUppercaseLetters()));
  133 + }
  134 + if (isPositiveInteger(passwordPolicy.getMinimumLowercaseLetters())) {
  135 + passwordRules.add(new CharacterRule(EnglishCharacterData.LowerCase, passwordPolicy.getMinimumLowercaseLetters()));
  136 + }
  137 + if (isPositiveInteger(passwordPolicy.getMinimumDigits())) {
  138 + passwordRules.add(new CharacterRule(EnglishCharacterData.Digit, passwordPolicy.getMinimumDigits()));
  139 + }
  140 + if (isPositiveInteger(passwordPolicy.getMinimumSpecialCharacters())) {
  141 + passwordRules.add(new CharacterRule(EnglishCharacterData.Special, passwordPolicy.getMinimumSpecialCharacters()));
  142 + }
  143 + PasswordValidator validator = new PasswordValidator(passwordRules);
  144 + PasswordData passwordData = new PasswordData(password);
  145 + RuleResult result = validator.validate(passwordData);
  146 + if (!result.isValid()) {
  147 + String message = String.join("\n", validator.getMessages(result));
  148 + throw new DataValidationException(message);
  149 + }
  150 + }
  151 +
  152 + private static boolean isPositiveInteger(Integer val) {
  153 + return val != null && val.intValue() > 0;
  154 + }
  155 +}
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.system;
  17 +
  18 +import org.springframework.security.core.AuthenticationException;
  19 +import org.thingsboard.server.common.data.id.TenantId;
  20 +import org.thingsboard.server.common.data.security.UserCredentials;
  21 +import org.thingsboard.server.dao.exception.DataValidationException;
  22 +import org.thingsboard.server.service.security.model.SecuritySettings;
  23 +
  24 +public interface SystemSecurityService {
  25 +
  26 + SecuritySettings getSecuritySettings(TenantId tenantId);
  27 +
  28 + SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings);
  29 +
  30 + void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String password) throws AuthenticationException;
  31 +
  32 + void validatePassword(TenantId tenantId, String password) throws DataValidationException;
  33 +
  34 +}
@@ -269,6 +269,9 @@ caffeine: @@ -269,6 +269,9 @@ caffeine:
269 claimDevices: 269 claimDevices:
270 timeToLiveInMinutes: 1 270 timeToLiveInMinutes: 1
271 maxSize: 100000 271 maxSize: 100000
  272 + securitySettings:
  273 + timeToLiveInMinutes: 1440
  274 + maxSize: 1
272 275
273 redis: 276 redis:
274 # standalone or cluster 277 # standalone or cluster
@@ -106,18 +106,6 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest { @@ -106,18 +106,6 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
106 } 106 }
107 107
108 @Test 108 @Test
109 - public void testSaveAdminSettingsWithNonTextValue() throws Exception {  
110 - loginSysAdmin();  
111 - AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);  
112 - JsonNode json = adminSettings.getJsonValue();  
113 - ((ObjectNode) json).put("timeout", 10000L);  
114 - adminSettings.setJsonValue(json);  
115 - doPost("/api/admin/settings", adminSettings)  
116 - .andExpect(status().isBadRequest())  
117 - .andExpect(statusReason(containsString("Provided json structure can't contain non-text values")));  
118 - }  
119 -  
120 - @Test  
121 public void testSendTestMail() throws Exception { 109 public void testSendTestMail() throws Exception {
122 loginSysAdmin(); 110 loginSysAdmin();
123 AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); 111 AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
@@ -23,4 +23,5 @@ public class CacheConstants { @@ -23,4 +23,5 @@ public class CacheConstants {
23 public static final String ASSET_CACHE = "assets"; 23 public static final String ASSET_CACHE = "assets";
24 public static final String ENTITY_VIEW_CACHE = "entityViews"; 24 public static final String ENTITY_VIEW_CACHE = "entityViews";
25 public static final String CLAIM_DEVICES_CACHE = "claimDevices"; 25 public static final String CLAIM_DEVICES_CACHE = "claimDevices";
  26 + public static final String SECURITY_SETTINGS_CACHE = "securitySettings";
26 } 27 }
@@ -37,7 +37,9 @@ public enum ActionType { @@ -37,7 +37,9 @@ public enum ActionType {
37 RELATION_DELETED(false), 37 RELATION_DELETED(false),
38 RELATIONS_DELETED(false), 38 RELATIONS_DELETED(false),
39 ALARM_ACK(false), 39 ALARM_ACK(false),
40 - ALARM_CLEAR(false); 40 + ALARM_CLEAR(false),
  41 + LOGIN(false),
  42 + LOGOUT(false);
41 43
42 private final boolean isRead; 44 private final boolean isRead;
43 45
@@ -22,6 +22,7 @@ public enum ThingsboardErrorCode { @@ -22,6 +22,7 @@ public enum ThingsboardErrorCode {
22 GENERAL(2), 22 GENERAL(2),
23 AUTHENTICATION(10), 23 AUTHENTICATION(10),
24 JWT_TOKEN_EXPIRED(11), 24 JWT_TOKEN_EXPIRED(11),
  25 + CREDENTIALS_EXPIRED(15),
25 PERMISSION_DENIED(20), 26 PERMISSION_DENIED(20),
26 INVALID_ARGUMENTS(30), 27 INVALID_ARGUMENTS(30),
27 BAD_REQUEST_PARAMS(31), 28 BAD_REQUEST_PARAMS(31),
@@ -248,6 +248,17 @@ public class AuditLogServiceImpl implements AuditLogService { @@ -248,6 +248,17 @@ public class AuditLogServiceImpl implements AuditLogService {
248 EntityRelation relation = extractParameter(EntityRelation.class, 0, additionalInfo); 248 EntityRelation relation = extractParameter(EntityRelation.class, 0, additionalInfo);
249 actionData.set("relation", objectMapper.valueToTree(relation)); 249 actionData.set("relation", objectMapper.valueToTree(relation));
250 break; 250 break;
  251 + case LOGIN:
  252 + case LOGOUT:
  253 + String clientAddress = extractParameter(String.class, 0, additionalInfo);
  254 + String browser = extractParameter(String.class, 1, additionalInfo);
  255 + String os = extractParameter(String.class, 2, additionalInfo);
  256 + String device = extractParameter(String.class, 3, additionalInfo);
  257 + actionData.put("clientAddress", clientAddress);
  258 + actionData.put("browser", browser);
  259 + actionData.put("os", os);
  260 + actionData.put("device", device);
  261 + break;
251 } 262 }
252 return actionData; 263 return actionData;
253 } 264 }
@@ -85,11 +85,5 @@ public abstract class DataValidator<D extends BaseData<?>> { @@ -85,11 +85,5 @@ public abstract class DataValidator<D extends BaseData<?>> {
85 if (!expectedFields.containsAll(actualFields) || !actualFields.containsAll(expectedFields)) { 85 if (!expectedFields.containsAll(actualFields) || !actualFields.containsAll(expectedFields)) {
86 throw new DataValidationException("Provided json structure is different from stored one '" + actualNode + "'!"); 86 throw new DataValidationException("Provided json structure is different from stored one '" + actualNode + "'!");
87 } 87 }
88 -  
89 - for (String field : actualFields) {  
90 - if (!actualNode.get(field).isTextual()) {  
91 - throw new DataValidationException("Provided json structure can't contain non-text values '" + actualNode + "'!");  
92 - }  
93 - }  
94 } 88 }
95 } 89 }
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
19 import org.thingsboard.server.common.data.User; 19 import org.thingsboard.server.common.data.User;
20 import org.thingsboard.server.common.data.id.CustomerId; 20 import org.thingsboard.server.common.data.id.CustomerId;
21 import org.thingsboard.server.common.data.id.TenantId; 21 import org.thingsboard.server.common.data.id.TenantId;
  22 +import org.thingsboard.server.common.data.id.UserCredentialsId;
22 import org.thingsboard.server.common.data.id.UserId; 23 import org.thingsboard.server.common.data.id.UserId;
23 import org.thingsboard.server.common.data.page.TextPageData; 24 import org.thingsboard.server.common.data.page.TextPageData;
24 import org.thingsboard.server.common.data.page.TextPageLink; 25 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -46,6 +47,10 @@ public interface UserService { @@ -46,6 +47,10 @@ public interface UserService {
46 47
47 UserCredentials requestPasswordReset(TenantId tenantId, String email); 48 UserCredentials requestPasswordReset(TenantId tenantId, String email);
48 49
  50 + UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId);
  51 +
  52 + UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
  53 +
49 void deleteUser(TenantId tenantId, UserId userId); 54 void deleteUser(TenantId tenantId, UserId userId);
50 55
51 TextPageData<User> findTenantAdmins(TenantId tenantId, TextPageLink pageLink); 56 TextPageData<User> findTenantAdmins(TenantId tenantId, TextPageLink pageLink);
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.Tenant; @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.Tenant;
27 import org.thingsboard.server.common.data.User; 27 import org.thingsboard.server.common.data.User;
28 import org.thingsboard.server.common.data.id.CustomerId; 28 import org.thingsboard.server.common.data.id.CustomerId;
29 import org.thingsboard.server.common.data.id.TenantId; 29 import org.thingsboard.server.common.data.id.TenantId;
  30 +import org.thingsboard.server.common.data.id.UserCredentialsId;
30 import org.thingsboard.server.common.data.id.UserId; 31 import org.thingsboard.server.common.data.id.UserId;
31 import org.thingsboard.server.common.data.page.TextPageData; 32 import org.thingsboard.server.common.data.page.TextPageData;
32 import org.thingsboard.server.common.data.page.TextPageLink; 33 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -176,6 +177,24 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @@ -176,6 +177,24 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
176 return saveUserCredentials(tenantId, userCredentials); 177 return saveUserCredentials(tenantId, userCredentials);
177 } 178 }
178 179
  180 + @Override
  181 + public UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId) {
  182 + UserCredentials userCredentials = userCredentialsDao.findById(tenantId, userCredentialsId.getId());
  183 + if (!userCredentials.isEnabled()) {
  184 + throw new IncorrectParameterException("Unable to reset password for inactive user");
  185 + }
  186 + userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
  187 + return saveUserCredentials(tenantId, userCredentials);
  188 + }
  189 +
  190 + @Override
  191 + public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
  192 + log.trace("Executing replaceUserCredentials [{}]", userCredentials);
  193 + userCredentialsValidator.validate(userCredentials, data -> tenantId);
  194 + userCredentialsDao.removeById(tenantId, userCredentials.getUuidId());
  195 + userCredentials.setId(null);
  196 + return userCredentialsDao.save(tenantId, userCredentials);
  197 + }
179 198
180 @Override 199 @Override
181 public void deleteUser(TenantId tenantId, UserId userId) { 200 public void deleteUser(TenantId tenantId, UserId userId) {
@@ -76,13 +76,4 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest { @@ -76,13 +76,4 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest {
76 adminSettings.setJsonValue(json); 76 adminSettings.setJsonValue(json);
77 adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); 77 adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings);
78 } 78 }
79 -  
80 - @Test(expected = DataValidationException.class)  
81 - public void testSaveAdminSettingsWithNonTextValue() {  
82 - AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(SYSTEM_TENANT_ID, "mail");  
83 - JsonNode json = adminSettings.getJsonValue();  
84 - ((ObjectNode) json).put("timeout", 10000L);  
85 - adminSettings.setJsonValue(json);  
86 - adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings);  
87 - }  
88 } 79 }
1 { 1 {
2 "name": "thingsboard-js-executor", 2 "name": "thingsboard-js-executor",
3 - "version": "2.4.0", 3 + "version": "2.4.1",
4 "lockfileVersion": 1, 4 "lockfileVersion": 1,
5 "requires": true, 5 "requires": true,
6 "dependencies": { 6 "dependencies": {
1 { 1 {
2 "name": "thingsboard-web-ui", 2 "name": "thingsboard-web-ui",
3 - "version": "2.4.0", 3 + "version": "2.4.1",
4 "lockfileVersion": 1, 4 "lockfileVersion": 1,
5 "requires": true, 5 "requires": true,
6 "dependencies": { 6 "dependencies": {
@@ -89,6 +89,8 @@ @@ -89,6 +89,8 @@
89 <fst.version>2.57</fst.version> 89 <fst.version>2.57</fst.version>
90 <antlr.version>2.7.7</antlr.version> 90 <antlr.version>2.7.7</antlr.version>
91 <snakeyaml.version>1.23</snakeyaml.version> 91 <snakeyaml.version>1.23</snakeyaml.version>
  92 + <passay.version>1.5.0</passay.version>
  93 + <ua-parser.version>1.4.3</ua-parser.version>
92 </properties> 94 </properties>
93 95
94 <modules> 96 <modules>
@@ -840,6 +842,16 @@ @@ -840,6 +842,16 @@
840 <artifactId>jts-core</artifactId> 842 <artifactId>jts-core</artifactId>
841 <version>${jts.version}</version> 843 <version>${jts.version}</version>
842 </dependency> 844 </dependency>
  845 + <dependency>
  846 + <groupId>org.passay</groupId>
  847 + <artifactId>passay</artifactId>
  848 + <version>${passay.version}</version>
  849 + </dependency>
  850 + <dependency>
  851 + <groupId>com.github.ua-parser</groupId>
  852 + <artifactId>uap-java</artifactId>
  853 + <version>${ua-parser.version}</version>
  854 + </dependency>
843 </dependencies> 855 </dependencies>
844 </dependencyManagement> 856 </dependencyManagement>
845 857
1 { 1 {
2 "name": "thingsboard", 2 "name": "thingsboard",
3 - "version": "2.4.0", 3 + "version": "2.4.1",
4 "lockfileVersion": 1, 4 "lockfileVersion": 1,
5 "requires": true, 5 "requires": true,
6 "dependencies": { 6 "dependencies": {
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 17
18 import generalSettingsTemplate from '../admin/general-settings.tpl.html'; 18 import generalSettingsTemplate from '../admin/general-settings.tpl.html';
19 import outgoingMailSettingsTemplate from '../admin/outgoing-mail-settings.tpl.html'; 19 import outgoingMailSettingsTemplate from '../admin/outgoing-mail-settings.tpl.html';
  20 +import securitySettingsTemplate from '../admin/security-settings.tpl.html';
20 21
21 /* eslint-enable import/no-unresolved, import/default */ 22 /* eslint-enable import/no-unresolved, import/default */
22 23
@@ -69,5 +70,23 @@ export default function AdminRoutes($stateProvider) { @@ -69,5 +70,23 @@ export default function AdminRoutes($stateProvider) {
69 ncyBreadcrumb: { 70 ncyBreadcrumb: {
70 label: '{"icon": "mail", "label": "admin.outgoing-mail"}' 71 label: '{"icon": "mail", "label": "admin.outgoing-mail"}'
71 } 72 }
  73 + })
  74 + .state('home.settings.security-settings', {
  75 + url: '/security-settings',
  76 + module: 'private',
  77 + auth: ['SYS_ADMIN'],
  78 + views: {
  79 + "content@home": {
  80 + templateUrl: securitySettingsTemplate,
  81 + controllerAs: 'vm',
  82 + controller: 'SecuritySettingsController'
  83 + }
  84 + },
  85 + data: {
  86 + pageTitle: 'admin.security-settings'
  87 + },
  88 + ncyBreadcrumb: {
  89 + label: '{"icon": "security", "label": "admin.security-settings"}'
  90 + }
72 }); 91 });
73 } 92 }
@@ -22,6 +22,7 @@ import thingsboardToast from '../services/toast'; @@ -22,6 +22,7 @@ import thingsboardToast from '../services/toast';
22 22
23 import AdminRoutes from './admin.routes'; 23 import AdminRoutes from './admin.routes';
24 import AdminController from './admin.controller'; 24 import AdminController from './admin.controller';
  25 +import SecuritySettingsController from './security-settings.controller';
25 26
26 export default angular.module('thingsboard.admin', [ 27 export default angular.module('thingsboard.admin', [
27 uiRouter, 28 uiRouter,
@@ -33,4 +34,5 @@ export default angular.module('thingsboard.admin', [ @@ -33,4 +34,5 @@ export default angular.module('thingsboard.admin', [
33 ]) 34 ])
34 .config(AdminRoutes) 35 .config(AdminRoutes)
35 .controller('AdminController', AdminController) 36 .controller('AdminController', AdminController)
  37 + .controller('SecuritySettingsController', SecuritySettingsController)
36 .name; 38 .name;
  1 +/*
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './settings-card.scss';
  18 +
  19 +/*@ngInject*/
  20 +export default function SecuritySettingsController(adminService, $mdExpansionPanel) {
  21 +
  22 + var vm = this;
  23 + vm.$mdExpansionPanel = $mdExpansionPanel;
  24 +
  25 + vm.save = save;
  26 +
  27 + loadSettings();
  28 +
  29 + function loadSettings() {
  30 + adminService.getSecuritySettings().then(function success(securitySettings) {
  31 + vm.securitySettings = securitySettings;
  32 + });
  33 + }
  34 +
  35 + function save() {
  36 + adminService.saveSecuritySettings(vm.securitySettings).then(function success(securitySettings) {
  37 + vm.securitySettings = securitySettings;
  38 + vm.settingsForm.$setPristine();
  39 + });
  40 + }
  41 +
  42 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div tb-help="'securitySettings'" help-container-id="help-container">
  19 + <md-card class="settings-card">
  20 + <md-card-title>
  21 + <md-card-title-text layout="row">
  22 + <span translate class="md-headline">admin.security-settings</span>
  23 + <span flex></span>
  24 + <div id="help-container"></div>
  25 + </md-card-title-text>
  26 + </md-card-title>
  27 + <md-progress-linear md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
  28 + <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
  29 + <md-card-content>
  30 + <form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm">
  31 + <fieldset ng-disabled="$root.loading">
  32 + <md-expansion-panel-group md-component-id="securitySettingsPanelGroup" auto-expand="true" multiple>
  33 + <md-expansion-panel md-component-id="passwordPolicyPanel" id="passwordPolicyPanel">
  34 + <md-expansion-panel-collapsed>
  35 + <div class="tb-panel-title" translate>admin.password-policy</div>
  36 + <md-expansion-panel-icon></md-expansion-panel-icon>
  37 + </md-expansion-panel-collapsed>
  38 + <md-expansion-panel-expanded>
  39 + <md-expansion-panel-header ng-click="vm.$mdExpansionPanel('passwordPolicyPanel').collapse()">
  40 + <div class="tb-panel-title" translate>admin.password-policy</div>
  41 + <md-expansion-panel-icon></md-expansion-panel-icon>
  42 + </md-expansion-panel-header>
  43 + <md-expansion-panel-content>
  44 + <md-input-container class="md-block">
  45 + <label translate>admin.minimum-password-length</label>
  46 + <input type="number"
  47 + step="1"
  48 + min="5"
  49 + max="50"
  50 + required
  51 + name="minimumPasswordLength"
  52 + ng-model="vm.securitySettings.passwordPolicy.minimumLength">
  53 + <div ng-messages="vm.settingsForm.minimumPasswordLength.$error">
  54 + <div translate ng-message="required">admin.minimum-password-length-required</div>
  55 + <div translate ng-message="min">admin.minimum-password-length-range</div>
  56 + <div translate ng-message="max">admin.minimum-password-length-range</div>
  57 + </div>
  58 + </md-input-container>
  59 + <md-input-container class="md-block">
  60 + <label translate>admin.minimum-uppercase-letters</label>
  61 + <input type="number"
  62 + step="1"
  63 + min="0"
  64 + name="minimumUppercaseLetters"
  65 + ng-model="vm.securitySettings.passwordPolicy.minimumUppercaseLetters">
  66 + <div ng-messages="vm.settingsForm.minimumUppercaseLetters.$error">
  67 + <div translate ng-message="min">admin.minimum-uppercase-letters-range</div>
  68 + </div>
  69 + </md-input-container>
  70 + <md-input-container class="md-block">
  71 + <label translate>admin.minimum-lowercase-letters</label>
  72 + <input type="number"
  73 + step="1"
  74 + min="0"
  75 + name="minimumLowercaseLetters"
  76 + ng-model="vm.securitySettings.passwordPolicy.minimumLowercaseLetters">
  77 + <div ng-messages="vm.settingsForm.minimumLowercaseLetters.$error">
  78 + <div translate ng-message="min">admin.minimum-lowercase-letters-range</div>
  79 + </div>
  80 + </md-input-container>
  81 + <md-input-container class="md-block">
  82 + <label translate>admin.minimum-digits</label>
  83 + <input type="number"
  84 + step="1"
  85 + min="0"
  86 + name="minimumDigits"
  87 + ng-model="vm.securitySettings.passwordPolicy.minimumDigits">
  88 + <div ng-messages="vm.settingsForm.minimumDigits.$error">
  89 + <div translate ng-message="min">admin.minimum-digits-range</div>
  90 + </div>
  91 + </md-input-container>
  92 + <md-input-container class="md-block">
  93 + <label translate>admin.minimum-special-characters</label>
  94 + <input type="number"
  95 + step="1"
  96 + min="0"
  97 + name="minimumSpecialCharacters"
  98 + ng-model="vm.securitySettings.passwordPolicy.minimumSpecialCharacters">
  99 + <div ng-messages="vm.settingsForm.minimumSpecialCharacters.$error">
  100 + <div translate ng-message="min">admin.minimum-special-characters-range</div>
  101 + </div>
  102 + </md-input-container>
  103 + <md-input-container class="md-block">
  104 + <label translate>admin.password-expiration-period-days</label>
  105 + <input type="number"
  106 + step="1"
  107 + min="0"
  108 + name="passwordExpirationPeriodDays"
  109 + ng-model="vm.securitySettings.passwordPolicy.passwordExpirationPeriodDays">
  110 + <div ng-messages="vm.settingsForm.passwordExpirationPeriodDays.$error">
  111 + <div translate ng-message="min">admin.password-expiration-period-days-range</div>
  112 + </div>
  113 + </md-input-container>
  114 + </md-expansion-panel-content>
  115 + </md-expansion-panel-expanded>
  116 + </md-expansion-panel>
  117 + </md-expansion-panel-group>
  118 + <div layout="row" layout-align="end center" width="100%" layout-wrap>
  119 + <md-button ng-disabled="$root.loading || vm.settingsForm.$invalid || !vm.settingsForm.$dirty" type="submit" class="md-raised md-primary">{{'action.save' | translate}}</md-button>
  120 + </div>
  121 + </fieldset>
  122 + </form>
  123 + </md-card-content>
  124 + </md-card>
  125 +</div>
@@ -20,4 +20,8 @@ md-card.settings-card { @@ -20,4 +20,8 @@ md-card.settings-card {
20 @media (min-width: $layout-breakpoint-sm) { 20 @media (min-width: $layout-breakpoint-sm) {
21 width: 60%; 21 width: 60%;
22 } 22 }
  23 +
  24 + md-icon.md-expansion-panel-icon {
  25 + margin-right: 0;
  26 + }
23 } 27 }
@@ -23,6 +23,8 @@ function AdminService($http, $q) { @@ -23,6 +23,8 @@ function AdminService($http, $q) {
23 var service = { 23 var service = {
24 getAdminSettings: getAdminSettings, 24 getAdminSettings: getAdminSettings,
25 saveAdminSettings: saveAdminSettings, 25 saveAdminSettings: saveAdminSettings,
  26 + getSecuritySettings: getSecuritySettings,
  27 + saveSecuritySettings: saveSecuritySettings,
26 sendTestMail: sendTestMail, 28 sendTestMail: sendTestMail,
27 checkUpdates: checkUpdates 29 checkUpdates: checkUpdates
28 } 30 }
@@ -51,6 +53,28 @@ function AdminService($http, $q) { @@ -51,6 +53,28 @@ function AdminService($http, $q) {
51 return deferred.promise; 53 return deferred.promise;
52 } 54 }
53 55
  56 + function getSecuritySettings() {
  57 + var deferred = $q.defer();
  58 + var url = '/api/admin/securitySettings';
  59 + $http.get(url, null).then(function success(response) {
  60 + deferred.resolve(response.data);
  61 + }, function fail() {
  62 + deferred.reject();
  63 + });
  64 + return deferred.promise;
  65 + }
  66 +
  67 + function saveSecuritySettings(securitySettings) {
  68 + var deferred = $q.defer();
  69 + var url = '/api/admin/securitySettings';
  70 + $http.post(url, securitySettings).then(function success(response) {
  71 + deferred.resolve(response.data);
  72 + }, function fail(response) {
  73 + deferred.reject(response.data);
  74 + });
  75 + return deferred.promise;
  76 + }
  77 +
54 function sendTestMail(settings) { 78 function sendTestMail(settings) {
55 var deferred = $q.defer(); 79 var deferred = $q.defer();
56 var url = '/api/admin/settings/testMail'; 80 var url = '/api/admin/settings/testMail';
@@ -141,7 +141,11 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time @@ -141,7 +141,11 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
141 } 141 }
142 142
143 function logout() { 143 function logout() {
144 - clearJwtToken(true); 144 + $http.post('/api/auth/logout', null, {ignoreErrors: true}).then(function success() {
  145 + clearJwtToken(true);
  146 + }, function fail() {
  147 + clearJwtToken(true);
  148 + });
145 } 149 }
146 150
147 function clearJwtToken(doLogout) { 151 function clearJwtToken(doLogout) {
@@ -20,6 +20,7 @@ export default angular.module('thingsboard.types', []) @@ -20,6 +20,7 @@ export default angular.module('thingsboard.types', [])
20 general: 2, 20 general: 2,
21 authentication: 10, 21 authentication: 10,
22 jwtTokenExpired: 11, 22 jwtTokenExpired: 11,
  23 + credentialsExpired: 15,
23 permissionDenied: 20, 24 permissionDenied: 20,
24 invalidArguments: 30, 25 invalidArguments: 30,
25 badRequestParams: 31, 26 badRequestParams: 31,
@@ -212,6 +213,12 @@ export default angular.module('thingsboard.types', []) @@ -212,6 +213,12 @@ export default angular.module('thingsboard.types', [])
212 }, 213 },
213 "ALARM_CLEAR": { 214 "ALARM_CLEAR": {
214 name: "audit-log.type-alarm-clear" 215 name: "audit-log.type-alarm-clear"
  216 + },
  217 + "LOGIN": {
  218 + name: "audit-log.type-login"
  219 + },
  220 + "LOGOUT": {
  221 + name: "audit-log.type-logout"
215 } 222 }
216 }, 223 },
217 auditLogActionStatus: { 224 auditLogActionStatus: {
@@ -170,7 +170,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { @@ -170,7 +170,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
170 var errorCode = rejectionErrorCode(rejection); 170 var errorCode = rejectionErrorCode(rejection);
171 if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) { 171 if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
172 return refreshTokenAndRetry(rejection); 172 return refreshTokenAndRetry(rejection);
173 - } else { 173 + } else if (errorCode !== getTypes().serverErrorCode.credentialsExpired) {
174 unhandled = true; 174 unhandled = true;
175 } 175 }
176 } else if (rejection.status === 403) { 176 } else if (rejection.status === 403) {
@@ -56,6 +56,7 @@ export default angular.module('thingsboard.help', []) @@ -56,6 +56,7 @@ export default angular.module('thingsboard.help', [])
56 { 56 {
57 linksMap: { 57 linksMap: {
58 outgoingMailSettings: helpBaseUrl + "/docs/user-guide/ui/mail-settings", 58 outgoingMailSettings: helpBaseUrl + "/docs/user-guide/ui/mail-settings",
  59 + securitySettings: helpBaseUrl + "/docs/user-guide/ui/security-settings",
59 ruleEngine: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/overview/", 60 ruleEngine: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/overview/",
60 ruleNodeCheckRelation: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node", 61 ruleNodeCheckRelation: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node",
61 ruleNodeJsFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node", 62 ruleNodeJsFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node",
@@ -84,7 +84,22 @@ @@ -84,7 +84,22 @@
84 "timeout-required": "Timeout is required.", 84 "timeout-required": "Timeout is required.",
85 "timeout-invalid": "That doesn't look like a valid timeout.", 85 "timeout-invalid": "That doesn't look like a valid timeout.",
86 "enable-tls": "Enable TLS", 86 "enable-tls": "Enable TLS",
87 - "send-test-mail": "Send test mail" 87 + "send-test-mail": "Send test mail",
  88 + "security-settings": "Security settings",
  89 + "password-policy": "Password policy",
  90 + "minimum-password-length": "Minimum password length",
  91 + "minimum-password-length-required": "Minimum password length is required",
  92 + "minimum-password-length-range": "Minimum password length should be in a range from 5 to 50",
  93 + "minimum-uppercase-letters": "Minimum number of uppercase letters",
  94 + "minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative",
  95 + "minimum-lowercase-letters": "Minimum number of lowercase letters",
  96 + "minimum-lowercase-letters-range": "Minimum number of lowercase letters can't be negative",
  97 + "minimum-digits": "Minimum number of digits",
  98 + "minimum-digits-range": "Minimum number of digits can't be negative",
  99 + "minimum-special-characters": "Minimum number of special characters",
  100 + "minimum-special-characters-range": "Minimum number of special characters can't be negative",
  101 + "password-expiration-period-days": "Password expiration period in days",
  102 + "password-expiration-period-days-range": "Password expiration period in days can't be negative"
88 }, 103 },
89 "alarm": { 104 "alarm": {
90 "alarm": "Alarm", 105 "alarm": "Alarm",
@@ -306,6 +321,8 @@ @@ -306,6 +321,8 @@
306 "type-relations-delete": "All relation deleted", 321 "type-relations-delete": "All relation deleted",
307 "type-alarm-ack": "Acknowledged", 322 "type-alarm-ack": "Acknowledged",
308 "type-alarm-clear": "Cleared", 323 "type-alarm-clear": "Cleared",
  324 + "type-login": "Login",
  325 + "type-logout": "Logout",
309 "status-success": "Success", 326 "status-success": "Success",
310 "status-failure": "Failure", 327 "status-failure": "Failure",
311 "audit-log-details": "Audit log details", 328 "audit-log-details": "Audit log details",
@@ -1183,6 +1200,7 @@ @@ -1183,6 +1200,7 @@
1183 "remember-me": "Remember me", 1200 "remember-me": "Remember me",
1184 "forgot-password": "Forgot Password?", 1201 "forgot-password": "Forgot Password?",
1185 "password-reset": "Password reset", 1202 "password-reset": "Password reset",
  1203 + "expired-password-reset-message": "Your credentials has been expired! Please create new password.",
1186 "new-password": "New password", 1204 "new-password": "New password",
1187 "new-password-again": "New password again", 1205 "new-password-again": "New password again",
1188 "password-link-sent-message": "Password reset link was successfully sent!", 1206 "password-link-sent-message": "Password reset link was successfully sent!",
@@ -20,7 +20,7 @@ import logoSvg from '../../svg/logo_title_white.svg'; @@ -20,7 +20,7 @@ import logoSvg from '../../svg/logo_title_white.svg';
20 /* eslint-enable import/no-unresolved, import/default */ 20 /* eslint-enable import/no-unresolved, import/default */
21 21
22 /*@ngInject*/ 22 /*@ngInject*/
23 -export default function LoginController(toast, loginService, userService/*, $rootScope, $log, $translate*/) { 23 +export default function LoginController(toast, loginService, userService, types, $state/*, $rootScope, $log, $translate*/) {
24 var vm = this; 24 var vm = this;
25 25
26 vm.logoSvg = logoSvg; 26 vm.logoSvg = logoSvg;
@@ -37,7 +37,7 @@ export default function LoginController(toast, loginService, userService/*, $roo @@ -37,7 +37,7 @@ export default function LoginController(toast, loginService, userService/*, $roo
37 var token = response.data.token; 37 var token = response.data.token;
38 var refreshToken = response.data.refreshToken; 38 var refreshToken = response.data.refreshToken;
39 userService.setUserFromJwtToken(token, refreshToken, true); 39 userService.setUserFromJwtToken(token, refreshToken, true);
40 - }, function fail(/*response*/) { 40 + }, function fail(response) {
41 /*if (response && response.data && response.data.message) { 41 /*if (response && response.data && response.data.message) {
42 toast.showError(response.data.message); 42 toast.showError(response.data.message);
43 } else if (response && response.statusText) { 43 } else if (response && response.statusText) {
@@ -45,6 +45,11 @@ export default function LoginController(toast, loginService, userService/*, $roo @@ -45,6 +45,11 @@ export default function LoginController(toast, loginService, userService/*, $roo
45 } else { 45 } else {
46 toast.showError($translate.instant('error.unknown-error')); 46 toast.showError($translate.instant('error.unknown-error'));
47 }*/ 47 }*/
  48 + if (response && response.data && response.data.errorCode) {
  49 + if (response.data.errorCode === types.serverErrorCode.credentialsExpired) {
  50 + $state.go('login.resetExpiredPassword', {resetToken: response.data.resetToken});
  51 + }
  52 + }
48 }); 53 });
49 } 54 }
50 55
@@ -63,6 +63,20 @@ export default function LoginRoutes($stateProvider) { @@ -63,6 +63,20 @@ export default function LoginRoutes($stateProvider) {
63 data: { 63 data: {
64 pageTitle: 'login.reset-password' 64 pageTitle: 'login.reset-password'
65 } 65 }
  66 + }).state('login.resetExpiredPassword', {
  67 + url: '/resetExpiredPassword?resetToken',
  68 + module: 'public',
  69 + views: {
  70 + "@": {
  71 + controller: 'ResetPasswordController',
  72 + controllerAs: 'vm',
  73 + templateUrl: resetPasswordTemplate
  74 + }
  75 + },
  76 + data: {
  77 + expiredPassword: true,
  78 + pageTitle: 'login.reset-password'
  79 + }
66 }).state('login.createPassword', { 80 }).state('login.createPassword', {
67 url: '/createPassword?activateToken', 81 url: '/createPassword?activateToken',
68 module: 'public', 82 module: 'public',
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 /*@ngInject*/ 16 /*@ngInject*/
17 -export default function ResetPasswordController($stateParams, $translate, toast, loginService, userService) { 17 +export default function ResetPasswordController($stateParams, $state, $translate, toast, loginService, userService) {
18 var vm = this; 18 var vm = this;
19 19
20 vm.newPassword = ''; 20 vm.newPassword = '';
@@ -22,6 +22,8 @@ export default function ResetPasswordController($stateParams, $translate, toast, @@ -22,6 +22,8 @@ export default function ResetPasswordController($stateParams, $translate, toast,
22 22
23 vm.resetPassword = resetPassword; 23 vm.resetPassword = resetPassword;
24 24
  25 + vm.isExpiredPassword = $state.$current.data.expiredPassword === true;
  26 +
25 function resetPassword() { 27 function resetPassword() {
26 if (vm.newPassword !== vm.newPassword2) { 28 if (vm.newPassword !== vm.newPassword2) {
27 toast.showError($translate.instant('login.passwords-mismatch-error')); 29 toast.showError($translate.instant('login.passwords-mismatch-error'));
@@ -20,6 +20,7 @@ @@ -20,6 +20,7 @@
20 <md-card-title> 20 <md-card-title>
21 <md-card-title-text> 21 <md-card-title-text>
22 <span translate class="md-headline">login.password-reset</span> 22 <span translate class="md-headline">login.password-reset</span>
  23 + <span ng-if="vm.isExpiredPassword" translate class="md-subhead">login.expired-password-reset-message</span>
23 </md-card-title-text> 24 </md-card-title-text>
24 </md-card-title> 25 </md-card-title>
25 <md-progress-linear class="md-warn" style="z-index: 1; max-height: 5px; width: inherit; position: absolute" 26 <md-progress-linear class="md-warn" style="z-index: 1; max-height: 5px; width: inherit; position: absolute"
@@ -82,7 +82,7 @@ function Menu(userService, $state, $rootScope) { @@ -82,7 +82,7 @@ function Menu(userService, $state, $rootScope) {
82 name: 'admin.system-settings', 82 name: 'admin.system-settings',
83 type: 'toggle', 83 type: 'toggle',
84 state: 'home.settings', 84 state: 'home.settings',
85 - height: '80px', 85 + height: '120px',
86 icon: 'settings', 86 icon: 'settings',
87 pages: [ 87 pages: [
88 { 88 {
@@ -96,6 +96,12 @@ function Menu(userService, $state, $rootScope) { @@ -96,6 +96,12 @@ function Menu(userService, $state, $rootScope) {
96 type: 'link', 96 type: 'link',
97 state: 'home.settings.outgoing-mail', 97 state: 'home.settings.outgoing-mail',
98 icon: 'mail' 98 icon: 'mail'
  99 + },
  100 + {
  101 + name: 'admin.security-settings',
  102 + type: 'link',
  103 + state: 'home.settings.security-settings',
  104 + icon: 'security'
99 } 105 }
100 ] 106 ]
101 }]; 107 }];
@@ -132,6 +138,11 @@ function Menu(userService, $state, $rootScope) { @@ -132,6 +138,11 @@ function Menu(userService, $state, $rootScope) {
132 name: 'admin.outgoing-mail', 138 name: 'admin.outgoing-mail',
133 icon: 'mail', 139 icon: 'mail',
134 state: 'home.settings.outgoing-mail' 140 state: 'home.settings.outgoing-mail'
  141 + },
  142 + {
  143 + name: 'admin.security-settings',
  144 + icon: 'security',
  145 + state: 'home.settings.security-settings'
135 } 146 }
136 ] 147 ]
137 }]; 148 }];