Commit 3935f616c7a30846211ec128879cca6653bafce7

Authored by VoBa
Committed by Igor Kulikov
1 parent 9538f65d

Feature/security updates (#1993)

* Added last login to user details

* Added last login to profile page

* Fixed label style

* Added actionType filter to Audit Logs controller

* Lockout feature; Password Reuse frequency

* Send email notification in case user lockout; Refactoring

* Fixed set of null password history; Added toast on user account locked/unlocked

* Typo fixed

* Ignored json validator for securitySettings

* Added Lockout action type

* Added permission check for user disable/enable

* Fixed email title
Showing 32 changed files with 551 additions and 88 deletions
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
  18 +import org.apache.commons.lang3.StringUtils;
18 import org.springframework.security.access.prepost.PreAuthorize; 19 import org.springframework.security.access.prepost.PreAuthorize;
19 import org.springframework.web.bind.annotation.PathVariable; 20 import org.springframework.web.bind.annotation.PathVariable;
20 import org.springframework.web.bind.annotation.RequestMapping; 21 import org.springframework.web.bind.annotation.RequestMapping;
@@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMethod; @@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
22 import org.springframework.web.bind.annotation.RequestParam; 23 import org.springframework.web.bind.annotation.RequestParam;
23 import org.springframework.web.bind.annotation.ResponseBody; 24 import org.springframework.web.bind.annotation.ResponseBody;
24 import org.springframework.web.bind.annotation.RestController; 25 import org.springframework.web.bind.annotation.RestController;
  26 +import org.thingsboard.server.common.data.audit.ActionType;
25 import org.thingsboard.server.common.data.audit.AuditLog; 27 import org.thingsboard.server.common.data.audit.AuditLog;
26 import org.thingsboard.server.common.data.exception.ThingsboardException; 28 import org.thingsboard.server.common.data.exception.ThingsboardException;
27 import org.thingsboard.server.common.data.id.CustomerId; 29 import org.thingsboard.server.common.data.id.CustomerId;
@@ -31,7 +33,10 @@ import org.thingsboard.server.common.data.id.UserId; @@ -31,7 +33,10 @@ import org.thingsboard.server.common.data.id.UserId;
31 import org.thingsboard.server.common.data.page.TimePageData; 33 import org.thingsboard.server.common.data.page.TimePageData;
32 import org.thingsboard.server.common.data.page.TimePageLink; 34 import org.thingsboard.server.common.data.page.TimePageLink;
33 35
  36 +import java.util.Arrays;
  37 +import java.util.List;
34 import java.util.UUID; 38 import java.util.UUID;
  39 +import java.util.stream.Collectors;
35 40
36 @RestController 41 @RestController
37 @RequestMapping("/api") 42 @RequestMapping("/api")
@@ -46,12 +51,14 @@ public class AuditLogController extends BaseController { @@ -46,12 +51,14 @@ public class AuditLogController extends BaseController {
46 @RequestParam(required = false) Long startTime, 51 @RequestParam(required = false) Long startTime,
47 @RequestParam(required = false) Long endTime, 52 @RequestParam(required = false) Long endTime,
48 @RequestParam(required = false, defaultValue = "false") boolean ascOrder, 53 @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
49 - @RequestParam(required = false) String offset) throws ThingsboardException { 54 + @RequestParam(required = false) String offset,
  55 + @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
50 try { 56 try {
51 checkParameter("CustomerId", strCustomerId); 57 checkParameter("CustomerId", strCustomerId);
52 TenantId tenantId = getCurrentUser().getTenantId(); 58 TenantId tenantId = getCurrentUser().getTenantId();
53 TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); 59 TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
54 - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), pageLink)); 60 + List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
  61 + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink));
55 } catch (Exception e) { 62 } catch (Exception e) {
56 throw handleException(e); 63 throw handleException(e);
57 } 64 }
@@ -66,12 +73,14 @@ public class AuditLogController extends BaseController { @@ -66,12 +73,14 @@ public class AuditLogController extends BaseController {
66 @RequestParam(required = false) Long startTime, 73 @RequestParam(required = false) Long startTime,
67 @RequestParam(required = false) Long endTime, 74 @RequestParam(required = false) Long endTime,
68 @RequestParam(required = false, defaultValue = "false") boolean ascOrder, 75 @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
69 - @RequestParam(required = false) String offset) throws ThingsboardException { 76 + @RequestParam(required = false) String offset,
  77 + @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
70 try { 78 try {
71 checkParameter("UserId", strUserId); 79 checkParameter("UserId", strUserId);
72 TenantId tenantId = getCurrentUser().getTenantId(); 80 TenantId tenantId = getCurrentUser().getTenantId();
73 TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); 81 TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
74 - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), pageLink)); 82 + List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
  83 + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink));
75 } catch (Exception e) { 84 } catch (Exception e) {
76 throw handleException(e); 85 throw handleException(e);
77 } 86 }
@@ -87,13 +96,15 @@ public class AuditLogController extends BaseController { @@ -87,13 +96,15 @@ public class AuditLogController extends BaseController {
87 @RequestParam(required = false) Long startTime, 96 @RequestParam(required = false) Long startTime,
88 @RequestParam(required = false) Long endTime, 97 @RequestParam(required = false) Long endTime,
89 @RequestParam(required = false, defaultValue = "false") boolean ascOrder, 98 @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
90 - @RequestParam(required = false) String offset) throws ThingsboardException { 99 + @RequestParam(required = false) String offset,
  100 + @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
91 try { 101 try {
92 checkParameter("EntityId", strEntityId); 102 checkParameter("EntityId", strEntityId);
93 checkParameter("EntityType", strEntityType); 103 checkParameter("EntityType", strEntityType);
94 TenantId tenantId = getCurrentUser().getTenantId(); 104 TenantId tenantId = getCurrentUser().getTenantId();
95 TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); 105 TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
96 - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), pageLink)); 106 + List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
  107 + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink));
97 } catch (Exception e) { 108 } catch (Exception e) {
98 throw handleException(e); 109 throw handleException(e);
99 } 110 }
@@ -107,13 +118,24 @@ public class AuditLogController extends BaseController { @@ -107,13 +118,24 @@ public class AuditLogController extends BaseController {
107 @RequestParam(required = false) Long startTime, 118 @RequestParam(required = false) Long startTime,
108 @RequestParam(required = false) Long endTime, 119 @RequestParam(required = false) Long endTime,
109 @RequestParam(required = false, defaultValue = "false") boolean ascOrder, 120 @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
110 - @RequestParam(required = false) String offset) throws ThingsboardException { 121 + @RequestParam(required = false) String offset,
  122 + @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
111 try { 123 try {
112 TenantId tenantId = getCurrentUser().getTenantId(); 124 TenantId tenantId = getCurrentUser().getTenantId();
113 TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); 125 TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
114 - return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, pageLink)); 126 + List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
  127 + return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink));
115 } catch (Exception e) { 128 } catch (Exception e) {
116 throw handleException(e); 129 throw handleException(e);
117 } 130 }
118 } 131 }
  132 +
  133 + private List<ActionType> parseActionTypesStr(String actionTypesStr) {
  134 + List<ActionType> result = null;
  135 + if (StringUtils.isNoneBlank(actionTypesStr)) {
  136 + String[] tmp = actionTypesStr.split(",");
  137 + result = Arrays.stream(tmp).map(at -> ActionType.valueOf(at.toUpperCase())).collect(Collectors.toList());
  138 + }
  139 + return result;
  140 + }
119 } 141 }
@@ -112,7 +112,7 @@ public class AuthController extends BaseController { @@ -112,7 +112,7 @@ public class AuthController extends BaseController {
112 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { 112 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
113 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);
114 } 114 }
115 - systemSecurityService.validatePassword(securityUser.getTenantId(), newPassword); 115 + systemSecurityService.validatePassword(securityUser.getTenantId(), newPassword, userCredentials);
116 if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) { 116 if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) {
117 throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); 117 throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
118 } 118 }
@@ -206,7 +206,7 @@ public class AuthController extends BaseController { @@ -206,7 +206,7 @@ public class AuthController extends BaseController {
206 try { 206 try {
207 String activateToken = activateRequest.get("activateToken").asText(); 207 String activateToken = activateRequest.get("activateToken").asText();
208 String password = activateRequest.get("password").asText(); 208 String password = activateRequest.get("password").asText();
209 - systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password); 209 + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, null);
210 String encodedPassword = passwordEncoder.encode(password); 210 String encodedPassword = passwordEncoder.encode(password);
211 UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword); 211 UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword);
212 User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId()); 212 User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
@@ -246,7 +246,7 @@ public class AuthController extends BaseController { @@ -246,7 +246,7 @@ public class AuthController extends BaseController {
246 String password = resetPasswordRequest.get("password").asText(); 246 String password = resetPasswordRequest.get("password").asText();
247 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); 247 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
248 if (userCredentials != null) { 248 if (userCredentials != null) {
249 - systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password); 249 + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, userCredentials);
250 if (passwordEncoder.matches(password, userCredentials.getPassword())) { 250 if (passwordEncoder.matches(password, userCredentials.getPassword())) {
251 throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); 251 throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
252 } 252 }
@@ -126,7 +126,7 @@ public class UserController extends BaseController { @@ -126,7 +126,7 @@ public class UserController extends BaseController {
126 126
127 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") 127 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
128 @RequestMapping(value = "/user", method = RequestMethod.POST) 128 @RequestMapping(value = "/user", method = RequestMethod.POST)
129 - @ResponseBody 129 + @ResponseBody
130 public User saveUser(@RequestBody User user, 130 public User saveUser(@RequestBody User user,
131 @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, 131 @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
132 HttpServletRequest request) throws ThingsboardException { 132 HttpServletRequest request) throws ThingsboardException {
@@ -285,5 +285,23 @@ public class UserController extends BaseController { @@ -285,5 +285,23 @@ public class UserController extends BaseController {
285 throw handleException(e); 285 throw handleException(e);
286 } 286 }
287 } 287 }
288 - 288 +
  289 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
  290 + @RequestMapping(value = "/user/{userId}/userCredentialsEnabled", method = RequestMethod.POST)
  291 + @ResponseBody
  292 + public void setUserCredentialsEnabled(@PathVariable(USER_ID) String strUserId,
  293 + @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException {
  294 + checkParameter(USER_ID, strUserId);
  295 + try {
  296 + UserId userId = new UserId(toUUID(strUserId));
  297 + User user = checkUserId(userId, Operation.WRITE);
  298 +
  299 + accessControlService.checkPermission(getCurrentUser(), Resource.USER, Operation.WRITE, user.getId(), user);
  300 +
  301 + TenantId tenantId = getCurrentUser().getTenantId();
  302 + userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled);
  303 + } catch (Exception e) {
  304 + throw handleException(e);
  305 + }
  306 + }
289 } 307 }
@@ -18,16 +18,15 @@ package org.thingsboard.server.exception; @@ -18,16 +18,15 @@ 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;  
22 import org.springframework.http.HttpStatus; 21 import org.springframework.http.HttpStatus;
23 import org.springframework.http.MediaType; 22 import org.springframework.http.MediaType;
24 import org.springframework.security.access.AccessDeniedException; 23 import org.springframework.security.access.AccessDeniedException;
25 import org.springframework.security.authentication.BadCredentialsException; 24 import org.springframework.security.authentication.BadCredentialsException;
26 -import org.springframework.security.authentication.CredentialsExpiredException; 25 +import org.springframework.security.authentication.DisabledException;
  26 +import org.springframework.security.authentication.LockedException;
27 import org.springframework.security.core.AuthenticationException; 27 import org.springframework.security.core.AuthenticationException;
28 import org.springframework.security.web.access.AccessDeniedHandler; 28 import org.springframework.security.web.access.AccessDeniedHandler;
29 import org.springframework.stereotype.Component; 29 import org.springframework.stereotype.Component;
30 -import org.thingsboard.server.common.data.EntityType;  
31 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 30 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
32 import org.thingsboard.server.common.data.exception.ThingsboardException; 31 import org.thingsboard.server.common.data.exception.ThingsboardException;
33 import org.thingsboard.server.common.msg.tools.TbRateLimitsException; 32 import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
@@ -39,8 +38,6 @@ import javax.servlet.ServletException; @@ -39,8 +38,6 @@ import javax.servlet.ServletException;
39 import javax.servlet.http.HttpServletRequest; 38 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletResponse; 39 import javax.servlet.http.HttpServletResponse;
41 import java.io.IOException; 40 import java.io.IOException;
42 -import java.net.URI;  
43 -import java.net.URISyntaxException;  
44 41
45 @Component 42 @Component
46 @Slf4j 43 @Slf4j
@@ -142,6 +139,10 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { @@ -142,6 +139,10 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
142 response.setStatus(HttpStatus.UNAUTHORIZED.value()); 139 response.setStatus(HttpStatus.UNAUTHORIZED.value());
143 if (authenticationException instanceof BadCredentialsException) { 140 if (authenticationException instanceof BadCredentialsException) {
144 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 141 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
  142 + } else if (authenticationException instanceof DisabledException) {
  143 + mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
  144 + } else if (authenticationException instanceof LockedException) {
  145 + mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
145 } else if (authenticationException instanceof JwtExpiredTokenException) { 146 } else if (authenticationException instanceof JwtExpiredTokenException) {
146 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); 147 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
147 } else if (authenticationException instanceof AuthMethodNotSupportedException) { 148 } else if (authenticationException instanceof AuthMethodNotSupportedException) {
@@ -212,6 +212,21 @@ public class DefaultMailService implements MailService { @@ -212,6 +212,21 @@ public class DefaultMailService implements MailService {
212 mailSender.send(helper.getMimeMessage()); 212 mailSender.send(helper.getMimeMessage());
213 } 213 }
214 214
  215 + @Override
  216 + public void sendAccountLockoutEmail( String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException {
  217 + String subject = messages.getMessage("account.lockout.subject", null, Locale.US);
  218 +
  219 + Map<String, Object> model = new HashMap<String, Object>();
  220 + model.put("lockoutAccount", lockoutEmail);
  221 + model.put("maxFailedLoginAttempts", maxFailedLoginAttempts);
  222 + model.put(TARGET_EMAIL, email);
  223 +
  224 + String message = mergeTemplateIntoString(this.engine,
  225 + "account.lockout.vm", UTF_8, model);
  226 +
  227 + sendMail(mailSender, mailFrom, email, subject, message);
  228 + }
  229 +
215 private void sendMail(JavaMailSenderImpl mailSender, 230 private void sendMail(JavaMailSenderImpl mailSender,
216 String mailFrom, String email, 231 String mailFrom, String email,
217 String subject, String message) throws ThingsboardException { 232 String subject, String message) throws ThingsboardException {
@@ -19,13 +19,12 @@ import lombok.extern.slf4j.Slf4j; @@ -19,13 +19,12 @@ import lombok.extern.slf4j.Slf4j;
19 import org.springframework.beans.factory.annotation.Autowired; 19 import org.springframework.beans.factory.annotation.Autowired;
20 import org.springframework.security.authentication.AuthenticationProvider; 20 import org.springframework.security.authentication.AuthenticationProvider;
21 import org.springframework.security.authentication.BadCredentialsException; 21 import org.springframework.security.authentication.BadCredentialsException;
22 -import org.springframework.security.authentication.DisabledException;  
23 import org.springframework.security.authentication.InsufficientAuthenticationException; 22 import org.springframework.security.authentication.InsufficientAuthenticationException;
  23 +import org.springframework.security.authentication.LockedException;
24 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 24 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
25 import org.springframework.security.core.Authentication; 25 import org.springframework.security.core.Authentication;
26 import org.springframework.security.core.AuthenticationException; 26 import org.springframework.security.core.AuthenticationException;
27 import org.springframework.security.core.userdetails.UsernameNotFoundException; 27 import org.springframework.security.core.userdetails.UsernameNotFoundException;
28 -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;  
29 import org.springframework.stereotype.Component; 28 import org.springframework.stereotype.Component;
30 import org.springframework.util.Assert; 29 import org.springframework.util.Assert;
31 import org.thingsboard.server.common.data.Customer; 30 import org.thingsboard.server.common.data.Customer;
@@ -100,18 +99,21 @@ public class RestAuthenticationProvider implements AuthenticationProvider { @@ -100,18 +99,21 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
100 throw new UsernameNotFoundException("User credentials not found"); 99 throw new UsernameNotFoundException("User credentials not found");
101 } 100 }
102 101
103 - systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, password); 102 + try {
  103 + systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password);
  104 + } catch (LockedException e) {
  105 + logLoginAction(user, authentication, ActionType.LOCKOUT, null);
  106 + throw e;
  107 + }
104 108
105 if (user.getAuthority() == null) 109 if (user.getAuthority() == null)
106 throw new InsufficientAuthenticationException("User has no authority assigned"); 110 throw new InsufficientAuthenticationException("User has no authority assigned");
107 111
108 SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); 112 SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
109 -  
110 - logLoginAction(user, authentication, null);  
111 - 113 + logLoginAction(user, authentication, ActionType.LOGIN, null);
112 return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); 114 return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
113 } catch (Exception e) { 115 } catch (Exception e) {
114 - logLoginAction(user, authentication, e); 116 + logLoginAction(user, authentication, ActionType.LOGIN, e);
115 throw e; 117 throw e;
116 } 118 }
117 } 119 }
@@ -148,7 +150,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { @@ -148,7 +150,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
148 return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); 150 return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
149 } 151 }
150 152
151 - private void logLoginAction(User user, Authentication authentication, Exception e) { 153 + private void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e) {
152 String clientAddress = "Unknown"; 154 String clientAddress = "Unknown";
153 String browser = "Unknown"; 155 String browser = "Unknown";
154 String os = "Unknown"; 156 String os = "Unknown";
@@ -194,6 +196,6 @@ public class RestAuthenticationProvider implements AuthenticationProvider { @@ -194,6 +196,6 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
194 } 196 }
195 auditLogService.logEntityAction( 197 auditLogService.logEntityAction(
196 user.getTenantId(), user.getCustomerId(), user.getId(), 198 user.getTenantId(), user.getCustomerId(), user.getId(),
197 - user.getName(), user.getId(), null, ActionType.LOGIN, e, clientAddress, browser, os, device); 199 + user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device);
198 } 200 }
199 } 201 }
@@ -24,4 +24,6 @@ public class SecuritySettings implements Serializable { @@ -24,4 +24,6 @@ public class SecuritySettings implements Serializable {
24 24
25 private UserPasswordPolicy passwordPolicy; 25 private UserPasswordPolicy passwordPolicy;
26 26
  27 + private Integer maxFailedLoginAttempts;
  28 + private String userLockoutNotificationEmail;
27 } 29 }
@@ -29,5 +29,6 @@ public class UserPasswordPolicy implements Serializable { @@ -29,5 +29,6 @@ public class UserPasswordPolicy implements Serializable {
29 private Integer minimumSpecialCharacters; 29 private Integer minimumSpecialCharacters;
30 30
31 private Integer passwordExpirationPeriodDays; 31 private Integer passwordExpirationPeriodDays;
  32 + private Integer passwordReuseFrequencyDays;
32 33
33 } 34 }
@@ -15,24 +15,38 @@ @@ -15,24 +15,38 @@
15 */ 15 */
16 package org.thingsboard.server.service.security.system; 16 package org.thingsboard.server.service.security.system;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
18 import com.fasterxml.jackson.databind.ObjectMapper; 19 import com.fasterxml.jackson.databind.ObjectMapper;
  20 +import com.fasterxml.jackson.databind.node.ObjectNode;
19 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
20 -import org.passay.*; 22 +import org.apache.commons.lang3.StringUtils;
  23 +import org.passay.CharacterRule;
  24 +import org.passay.EnglishCharacterData;
  25 +import org.passay.LengthRule;
  26 +import org.passay.PasswordData;
  27 +import org.passay.PasswordValidator;
  28 +import org.passay.Rule;
  29 +import org.passay.RuleResult;
21 import org.springframework.beans.factory.annotation.Autowired; 30 import org.springframework.beans.factory.annotation.Autowired;
22 import org.springframework.cache.annotation.CacheEvict; 31 import org.springframework.cache.annotation.CacheEvict;
23 import org.springframework.cache.annotation.Cacheable; 32 import org.springframework.cache.annotation.Cacheable;
24 import org.springframework.security.authentication.BadCredentialsException; 33 import org.springframework.security.authentication.BadCredentialsException;
25 -import org.springframework.security.authentication.CredentialsExpiredException;  
26 import org.springframework.security.authentication.DisabledException; 34 import org.springframework.security.authentication.DisabledException;
  35 +import org.springframework.security.authentication.LockedException;
27 import org.springframework.security.core.AuthenticationException; 36 import org.springframework.security.core.AuthenticationException;
28 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 37 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
29 import org.springframework.stereotype.Service; 38 import org.springframework.stereotype.Service;
  39 +import org.thingsboard.rule.engine.api.MailService;
30 import org.thingsboard.server.common.data.AdminSettings; 40 import org.thingsboard.server.common.data.AdminSettings;
  41 +import org.thingsboard.server.common.data.User;
  42 +import org.thingsboard.server.common.data.exception.ThingsboardException;
31 import org.thingsboard.server.common.data.id.TenantId; 43 import org.thingsboard.server.common.data.id.TenantId;
32 import org.thingsboard.server.common.data.security.UserCredentials; 44 import org.thingsboard.server.common.data.security.UserCredentials;
  45 +import org.thingsboard.server.dao.audit.AuditLogService;
33 import org.thingsboard.server.dao.exception.DataValidationException; 46 import org.thingsboard.server.dao.exception.DataValidationException;
34 import org.thingsboard.server.dao.settings.AdminSettingsService; 47 import org.thingsboard.server.dao.settings.AdminSettingsService;
35 import org.thingsboard.server.dao.user.UserService; 48 import org.thingsboard.server.dao.user.UserService;
  49 +import org.thingsboard.server.dao.user.UserServiceImpl;
36 import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; 50 import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
37 import org.thingsboard.server.service.security.model.SecuritySettings; 51 import org.thingsboard.server.service.security.model.SecuritySettings;
38 import org.thingsboard.server.service.security.model.UserPasswordPolicy; 52 import org.thingsboard.server.service.security.model.UserPasswordPolicy;
@@ -40,9 +54,9 @@ import org.thingsboard.server.service.security.model.UserPasswordPolicy; @@ -40,9 +54,9 @@ import org.thingsboard.server.service.security.model.UserPasswordPolicy;
40 import javax.annotation.Resource; 54 import javax.annotation.Resource;
41 import java.util.ArrayList; 55 import java.util.ArrayList;
42 import java.util.List; 56 import java.util.List;
  57 +import java.util.Map;
43 import java.util.concurrent.TimeUnit; 58 import java.util.concurrent.TimeUnit;
44 59
45 -import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;  
46 import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE; 60 import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
47 61
48 @Service 62 @Service
@@ -60,6 +74,9 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @@ -60,6 +74,9 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
60 @Autowired 74 @Autowired
61 private UserService userService; 75 private UserService userService;
62 76
  77 + @Autowired
  78 + private MailService mailService;
  79 +
63 @Resource 80 @Resource
64 private SystemSecurityService self; 81 private SystemSecurityService self;
65 82
@@ -100,9 +117,23 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @@ -100,9 +117,23 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
100 } 117 }
101 118
102 @Override 119 @Override
103 - public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String password) throws AuthenticationException {  
104 - 120 + public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException {
105 if (!encoder.matches(password, userCredentials.getPassword())) { 121 if (!encoder.matches(password, userCredentials.getPassword())) {
  122 + int failedLoginAttempts = userService.onUserLoginIncorrectCredentials(tenantId, userCredentials.getUserId());
  123 + SecuritySettings securitySettings = getSecuritySettings(tenantId);
  124 + if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
  125 + if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
  126 + userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userCredentials.getUserId(), false);
  127 + if (StringUtils.isNoneBlank(securitySettings.getUserLockoutNotificationEmail())) {
  128 + try {
  129 + mailService.sendAccountLockoutEmail(username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
  130 + } catch (ThingsboardException e) {
  131 + log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, securitySettings.getUserLockoutNotificationEmail(), e);
  132 + }
  133 + }
  134 + throw new LockedException("Authentication Failed. Username was locked due to security policy.");
  135 + }
  136 + }
106 throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); 137 throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
107 } 138 }
108 139
@@ -110,6 +141,8 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @@ -110,6 +141,8 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
110 throw new DisabledException("User is not active"); 141 throw new DisabledException("User is not active");
111 } 142 }
112 143
  144 + userService.onUserLoginSuccessful(tenantId, userCredentials.getUserId());
  145 +
113 SecuritySettings securitySettings = self.getSecuritySettings(tenantId); 146 SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
114 if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) { 147 if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
115 if ((userCredentials.getCreatedTime() 148 if ((userCredentials.getCreatedTime()
@@ -122,7 +155,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @@ -122,7 +155,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
122 } 155 }
123 156
124 @Override 157 @Override
125 - public void validatePassword(TenantId tenantId, String password) throws DataValidationException { 158 + public void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException {
126 SecuritySettings securitySettings = self.getSecuritySettings(tenantId); 159 SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
127 UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); 160 UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
128 161
@@ -147,6 +180,22 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @@ -147,6 +180,22 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
147 String message = String.join("\n", validator.getMessages(result)); 180 String message = String.join("\n", validator.getMessages(result));
148 throw new DataValidationException(message); 181 throw new DataValidationException(message);
149 } 182 }
  183 +
  184 + if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) {
  185 + long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays());
  186 + User user = userService.findUserById(tenantId, userCredentials.getUserId());
  187 + JsonNode additionalInfo = user.getAdditionalInfo();
  188 + if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) {
  189 + JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY);
  190 + Map<String, String> userPasswordHistoryMap = objectMapper.convertValue(userPasswordHistoryJson, Map.class);
  191 + for (Map.Entry<String, String> entry : userPasswordHistoryMap.entrySet()) {
  192 + if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) {
  193 + throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days");
  194 + }
  195 + }
  196 +
  197 + }
  198 + }
150 } 199 }
151 200
152 private static boolean isPositiveInteger(Integer val) { 201 private static boolean isPositiveInteger(Integer val) {
@@ -27,8 +27,8 @@ public interface SystemSecurityService { @@ -27,8 +27,8 @@ public interface SystemSecurityService {
27 27
28 SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings); 28 SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings);
29 29
30 - void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String password) throws AuthenticationException; 30 + void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException;
31 31
32 - void validatePassword(TenantId tenantId, String password) throws DataValidationException; 32 + void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException;
33 33
34 } 34 }
@@ -3,3 +3,4 @@ activation.subject=Your account activation on Thingsboard @@ -3,3 +3,4 @@ activation.subject=Your account activation on Thingsboard
3 account.activated.subject=Thingsboard - your account has been activated 3 account.activated.subject=Thingsboard - your account has been activated
4 reset.password.subject=Thingsboard - Password reset has been requested 4 reset.password.subject=Thingsboard - Password reset has been requested
5 password.was.reset.subject=Thingsboard - your account password has been reset 5 password.was.reset.subject=Thingsboard - your account password has been reset
  6 +account.lockout.subject=Thingsboard - User account has been lockout
  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 +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  17 +<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  18 +<head>
  19 +<meta name="viewport" content="width=device-width" />
  20 +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  21 +<title>Thingsboard - Account Lockout</title>
  22 +
  23 +
  24 +<style type="text/css">
  25 +img {
  26 +max-width: 100%;
  27 +}
  28 +body {
  29 +-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
  30 +}
  31 +body {
  32 +background-color: #f6f6f6;
  33 +}
  34 +@media only screen and (max-width: 640px) {
  35 + body {
  36 + padding: 0 !important;
  37 + }
  38 + h1 {
  39 + font-weight: 800 !important; margin: 20px 0 5px !important;
  40 + }
  41 + h2 {
  42 + font-weight: 800 !important; margin: 20px 0 5px !important;
  43 + }
  44 + h3 {
  45 + font-weight: 800 !important; margin: 20px 0 5px !important;
  46 + }
  47 + h4 {
  48 + font-weight: 800 !important; margin: 20px 0 5px !important;
  49 + }
  50 + h1 {
  51 + font-size: 22px !important;
  52 + }
  53 + h2 {
  54 + font-size: 18px !important;
  55 + }
  56 + h3 {
  57 + font-size: 16px !important;
  58 + }
  59 + .container {
  60 + padding: 0 !important; width: 100% !important;
  61 + }
  62 + .content {
  63 + padding: 0 !important;
  64 + }
  65 + .content-wrap {
  66 + padding: 10px !important;
  67 + }
  68 + .invoice {
  69 + width: 100% !important;
  70 + }
  71 +}
  72 +</style>
  73 +</head>
  74 +
  75 +<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
  76 +
  77 +<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
  78 + <td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
  79 + <div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
  80 + <table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
  81 + <meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  82 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  83 + <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
  84 + <h2>Thingsboard user account has been locked out</h2>
  85 + </td>
  86 + </tr>
  87 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  88 + <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
  89 + Thingsboard user account $lockoutAccount has been lockout due to failed credentials were provided more than $maxFailedLoginAttempts times.
  90 + </td>
  91 + </tr>
  92 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  93 + <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
  94 + &mdash; The Thingsboard
  95 + </td>
  96 + </tr></table></td>
  97 + </tr>
  98 + </table>
  99 + <div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
  100 + <table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  101 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  102 + <td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
  103 + </tr>
  104 + </table>
  105 + </div>
  106 + </div>
  107 + </td>
  108 + <td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
  109 + </tr>
  110 +</table>
  111 +</body>
  112 +</html>
@@ -32,13 +32,13 @@ import java.util.List; @@ -32,13 +32,13 @@ import java.util.List;
32 32
33 public interface AuditLogService { 33 public interface AuditLogService {
34 34
35 - TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink); 35 + TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List<ActionType> actionTypes, TimePageLink pageLink);
36 36
37 - TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink); 37 + TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List<ActionType> actionTypes, TimePageLink pageLink);
38 38
39 - TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink); 39 + TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink);
40 40
41 - TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink); 41 + TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, List<ActionType> actionTypes, TimePageLink pageLink);
42 42
43 <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction( 43 <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction(
44 TenantId tenantId, 44 TenantId tenantId,
@@ -60,5 +60,10 @@ public interface UserService { @@ -60,5 +60,10 @@ public interface UserService {
60 TextPageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); 60 TextPageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
61 61
62 void deleteCustomerUsers(TenantId tenantId, CustomerId customerId); 62 void deleteCustomerUsers(TenantId tenantId, CustomerId customerId);
63 - 63 +
  64 + void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled);
  65 +
  66 + void onUserLoginSuccessful(TenantId tenantId, UserId userId);
  67 +
  68 + int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId);
64 } 69 }
@@ -39,7 +39,8 @@ public enum ActionType { @@ -39,7 +39,8 @@ public enum ActionType {
39 ALARM_ACK(false), 39 ALARM_ACK(false),
40 ALARM_CLEAR(false), 40 ALARM_CLEAR(false),
41 LOGIN(false), 41 LOGIN(false),
42 - LOGOUT(false); 42 + LOGOUT(false),
  43 + LOCKOUT(false);
43 44
44 private final boolean isRead; 45 private final boolean isRead;
45 46
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 package org.thingsboard.server.dao.audit; 16 package org.thingsboard.server.dao.audit;
17 17
18 import com.google.common.util.concurrent.ListenableFuture; 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import org.thingsboard.server.common.data.audit.ActionType;
19 import org.thingsboard.server.common.data.audit.AuditLog; 20 import org.thingsboard.server.common.data.audit.AuditLog;
20 import org.thingsboard.server.common.data.id.CustomerId; 21 import org.thingsboard.server.common.data.id.CustomerId;
21 import org.thingsboard.server.common.data.id.EntityId; 22 import org.thingsboard.server.common.data.id.EntityId;
@@ -37,11 +38,11 @@ public interface AuditLogDao { @@ -37,11 +38,11 @@ public interface AuditLogDao {
37 38
38 ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog); 39 ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog);
39 40
40 - List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink); 41 + List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink);
41 42
42 - List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink); 43 + List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List<ActionType> actionTypes, TimePageLink pageLink);
43 44
44 - List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink); 45 + List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List<ActionType> actionTypes, TimePageLink pageLink);
45 46
46 - List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink); 47 + List<AuditLog> findAuditLogsByTenantId(UUID tenantId, List<ActionType> actionTypes, TimePageLink pageLink);
47 } 48 }
@@ -81,37 +81,37 @@ public class AuditLogServiceImpl implements AuditLogService { @@ -81,37 +81,37 @@ public class AuditLogServiceImpl implements AuditLogService {
81 private AuditLogSink auditLogSink; 81 private AuditLogSink auditLogSink;
82 82
83 @Override 83 @Override
84 - public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { 84 + public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List<ActionType> actionTypes, TimePageLink pageLink) {
85 log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink); 85 log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink);
86 validateId(tenantId, INCORRECT_TENANT_ID + tenantId); 86 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
87 validateId(customerId, "Incorrect customerId " + customerId); 87 validateId(customerId, "Incorrect customerId " + customerId);
88 - List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, pageLink); 88 + List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, actionTypes, pageLink);
89 return new TimePageData<>(auditLogs, pageLink); 89 return new TimePageData<>(auditLogs, pageLink);
90 } 90 }
91 91
92 @Override 92 @Override
93 - public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { 93 + public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List<ActionType> actionTypes, TimePageLink pageLink) {
94 log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink); 94 log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink);
95 validateId(tenantId, INCORRECT_TENANT_ID + tenantId); 95 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
96 validateId(userId, "Incorrect userId" + userId); 96 validateId(userId, "Incorrect userId" + userId);
97 - List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, pageLink); 97 + List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, actionTypes, pageLink);
98 return new TimePageData<>(auditLogs, pageLink); 98 return new TimePageData<>(auditLogs, pageLink);
99 } 99 }
100 100
101 @Override 101 @Override
102 - public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { 102 + public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink) {
103 log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink); 103 log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink);
104 validateId(tenantId, INCORRECT_TENANT_ID + tenantId); 104 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
105 validateEntityId(entityId, INCORRECT_TENANT_ID + entityId); 105 validateEntityId(entityId, INCORRECT_TENANT_ID + entityId);
106 - List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, pageLink); 106 + List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, actionTypes, pageLink);
107 return new TimePageData<>(auditLogs, pageLink); 107 return new TimePageData<>(auditLogs, pageLink);
108 } 108 }
109 109
110 @Override 110 @Override
111 - public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { 111 + public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, List<ActionType> actionTypes, TimePageLink pageLink) {
112 log.trace("Executing findAuditLogs [{}]", pageLink); 112 log.trace("Executing findAuditLogs [{}]", pageLink);
113 validateId(tenantId, INCORRECT_TENANT_ID + tenantId); 113 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
114 - List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), pageLink); 114 + List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), actionTypes, pageLink);
115 return new TimePageData<>(auditLogs, pageLink); 115 return new TimePageData<>(auditLogs, pageLink);
116 } 116 }
117 117
@@ -250,6 +250,7 @@ public class AuditLogServiceImpl implements AuditLogService { @@ -250,6 +250,7 @@ public class AuditLogServiceImpl implements AuditLogService {
250 break; 250 break;
251 case LOGIN: 251 case LOGIN:
252 case LOGOUT: 252 case LOGOUT:
  253 + case LOCKOUT:
253 String clientAddress = extractParameter(String.class, 0, additionalInfo); 254 String clientAddress = extractParameter(String.class, 0, additionalInfo);
254 String browser = extractParameter(String.class, 1, additionalInfo); 255 String browser = extractParameter(String.class, 1, additionalInfo);
255 String os = extractParameter(String.class, 2, additionalInfo); 256 String os = extractParameter(String.class, 2, additionalInfo);
@@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired;
29 import org.springframework.beans.factory.annotation.Value; 29 import org.springframework.beans.factory.annotation.Value;
30 import org.springframework.core.env.Environment; 30 import org.springframework.core.env.Environment;
31 import org.springframework.stereotype.Component; 31 import org.springframework.stereotype.Component;
  32 +import org.thingsboard.server.common.data.audit.ActionType;
32 import org.thingsboard.server.common.data.audit.AuditLog; 33 import org.thingsboard.server.common.data.audit.AuditLog;
33 import org.thingsboard.server.common.data.id.CustomerId; 34 import org.thingsboard.server.common.data.id.CustomerId;
34 import org.thingsboard.server.common.data.id.EntityId; 35 import org.thingsboard.server.common.data.id.EntityId;
@@ -273,7 +274,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo @@ -273,7 +274,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
273 } 274 }
274 275
275 @Override 276 @Override
276 - public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) { 277 + public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink) {
277 log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); 278 log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink);
278 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_ENTITY_ID_CF, 279 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_ENTITY_ID_CF,
279 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), 280 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
@@ -285,7 +286,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo @@ -285,7 +286,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
285 } 286 }
286 287
287 @Override 288 @Override
288 - public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) { 289 + public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List<ActionType> actionTypes, TimePageLink pageLink) {
289 log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink); 290 log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink);
290 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_CUSTOMER_ID_CF, 291 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_CUSTOMER_ID_CF,
291 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), 292 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
@@ -296,7 +297,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo @@ -296,7 +297,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
296 } 297 }
297 298
298 @Override 299 @Override
299 - public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) { 300 + public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List<ActionType> actionTypes, TimePageLink pageLink) {
300 log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink); 301 log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink);
301 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_USER_ID_CF, 302 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_USER_ID_CF,
302 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), 303 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
@@ -307,7 +308,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo @@ -307,7 +308,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
307 } 308 }
308 309
309 @Override 310 @Override
310 - public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { 311 + public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, List<ActionType> actionTypes, TimePageLink pageLink) {
311 log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); 312 log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink);
312 313
313 long minPartition; 314 long minPartition;
@@ -37,22 +37,22 @@ import java.util.List; @@ -37,22 +37,22 @@ import java.util.List;
37 public class DummyAuditLogServiceImpl implements AuditLogService { 37 public class DummyAuditLogServiceImpl implements AuditLogService {
38 38
39 @Override 39 @Override
40 - public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { 40 + public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List<ActionType> actionTypes, TimePageLink pageLink) {
41 return new TimePageData<>(null, pageLink); 41 return new TimePageData<>(null, pageLink);
42 } 42 }
43 43
44 @Override 44 @Override
45 - public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { 45 + public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List<ActionType> actionTypes, TimePageLink pageLink) {
46 return new TimePageData<>(null, pageLink); 46 return new TimePageData<>(null, pageLink);
47 } 47 }
48 48
49 @Override 49 @Override
50 - public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { 50 + public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink) {
51 return new TimePageData<>(null, pageLink); 51 return new TimePageData<>(null, pageLink);
52 } 52 }
53 53
54 @Override 54 @Override
55 - public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { 55 + public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, List<ActionType> actionTypes, TimePageLink pageLink) {
56 return new TimePageData<>(null, pageLink); 56 return new TimePageData<>(null, pageLink);
57 } 57 }
58 58
@@ -73,7 +73,9 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -73,7 +73,9 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
73 if (!existentAdminSettings.getKey().equals(adminSettings.getKey())) { 73 if (!existentAdminSettings.getKey().equals(adminSettings.getKey())) {
74 throw new DataValidationException("Changing key of admin settings entry is prohibited!"); 74 throw new DataValidationException("Changing key of admin settings entry is prohibited!");
75 } 75 }
76 - validateJsonStructure(existentAdminSettings.getJsonValue(), adminSettings.getJsonValue()); 76 + if (adminSettings.getKey().equals("mail")) {
  77 + validateJsonStructure(existentAdminSettings.getJsonValue(), adminSettings.getJsonValue());
  78 + }
77 } 79 }
78 } 80 }
79 81
@@ -26,6 +26,7 @@ import org.springframework.data.jpa.domain.Specification; @@ -26,6 +26,7 @@ import org.springframework.data.jpa.domain.Specification;
26 import org.springframework.data.repository.CrudRepository; 26 import org.springframework.data.repository.CrudRepository;
27 import org.springframework.stereotype.Component; 27 import org.springframework.stereotype.Component;
28 import org.thingsboard.server.common.data.UUIDConverter; 28 import org.thingsboard.server.common.data.UUIDConverter;
  29 +import org.thingsboard.server.common.data.audit.ActionType;
29 import org.thingsboard.server.common.data.audit.AuditLog; 30 import org.thingsboard.server.common.data.audit.AuditLog;
30 import org.thingsboard.server.common.data.id.CustomerId; 31 import org.thingsboard.server.common.data.id.CustomerId;
31 import org.thingsboard.server.common.data.id.EntityId; 32 import org.thingsboard.server.common.data.id.EntityId;
@@ -101,34 +102,34 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp @@ -101,34 +102,34 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
101 } 102 }
102 103
103 @Override 104 @Override
104 - public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) {  
105 - return findAuditLogs(tenantId, entityId, null, null, pageLink); 105 + public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink) {
  106 + return findAuditLogs(tenantId, entityId, null, null, actionTypes, pageLink);
106 } 107 }
107 108
108 @Override 109 @Override
109 - public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) {  
110 - return findAuditLogs(tenantId, null, customerId, null, pageLink); 110 + public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List<ActionType> actionTypes, TimePageLink pageLink) {
  111 + return findAuditLogs(tenantId, null, customerId, null, actionTypes, pageLink);
111 } 112 }
112 113
113 @Override 114 @Override
114 - public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) {  
115 - return findAuditLogs(tenantId, null, null, userId, pageLink); 115 + public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List<ActionType> actionTypes, TimePageLink pageLink) {
  116 + return findAuditLogs(tenantId, null, null, userId, actionTypes, pageLink);
116 } 117 }
117 118
118 @Override 119 @Override
119 - public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {  
120 - return findAuditLogs(tenantId, null, null, null, pageLink); 120 + public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, List<ActionType> actionTypes, TimePageLink pageLink) {
  121 + return findAuditLogs(tenantId, null, null, null, actionTypes, pageLink);
121 } 122 }
122 123
123 - private List<AuditLog> findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, TimePageLink pageLink) { 124 + private List<AuditLog> findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, List<ActionType> actionTypes, TimePageLink pageLink) {
124 Specification<AuditLogEntity> timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id"); 125 Specification<AuditLogEntity> timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id");
125 - Specification<AuditLogEntity> fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId); 126 + Specification<AuditLogEntity> fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId, actionTypes);
126 Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC; 127 Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC;
127 Pageable pageable = new PageRequest(0, pageLink.getLimit(), sortDirection, ID_PROPERTY); 128 Pageable pageable = new PageRequest(0, pageLink.getLimit(), sortDirection, ID_PROPERTY);
128 return DaoUtil.convertDataList(auditLogRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent()); 129 return DaoUtil.convertDataList(auditLogRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent());
129 } 130 }
130 131
131 - private Specification<AuditLogEntity> getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId) { 132 + private Specification<AuditLogEntity> getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, List<ActionType> actionTypes) {
132 return (root, criteriaQuery, criteriaBuilder) -> { 133 return (root, criteriaQuery, criteriaBuilder) -> {
133 List<Predicate> predicates = new ArrayList<>(); 134 List<Predicate> predicates = new ArrayList<>();
134 if (tenantId != null) { 135 if (tenantId != null) {
@@ -142,12 +143,15 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp @@ -142,12 +143,15 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
142 predicates.add(entityIdPredicate); 143 predicates.add(entityIdPredicate);
143 } 144 }
144 if (customerId != null) { 145 if (customerId != null) {
145 - Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId()));  
146 - predicates.add(tenantIdPredicate); 146 + Predicate customerIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId()));
  147 + predicates.add(customerIdPredicate);
147 } 148 }
148 if (userId != null) { 149 if (userId != null) {
149 - Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId()));  
150 - predicates.add(tenantIdPredicate); 150 + Predicate userIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId()));
  151 + predicates.add(userIdPredicate);
  152 + }
  153 + if (actionTypes != null && !actionTypes.isEmpty()) {
  154 + predicates.add(root.get("actionType").in(actionTypes));
151 } 155 }
152 return criteriaBuilder.and(predicates.toArray(new Predicate[]{})); 156 return criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
153 }; 157 };
@@ -15,6 +15,9 @@ @@ -15,6 +15,9 @@
15 */ 15 */
16 package org.thingsboard.server.dao.user; 16 package org.thingsboard.server.dao.user;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import com.fasterxml.jackson.databind.ObjectMapper;
  20 +import com.fasterxml.jackson.databind.node.ObjectNode;
18 import com.google.common.util.concurrent.ListenableFuture; 21 import com.google.common.util.concurrent.ListenableFuture;
19 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
20 import org.apache.commons.lang3.RandomStringUtils; 23 import org.apache.commons.lang3.RandomStringUtils;
@@ -42,7 +45,9 @@ import org.thingsboard.server.dao.service.DataValidator; @@ -42,7 +45,9 @@ import org.thingsboard.server.dao.service.DataValidator;
42 import org.thingsboard.server.dao.service.PaginatedRemover; 45 import org.thingsboard.server.dao.service.PaginatedRemover;
43 import org.thingsboard.server.dao.tenant.TenantDao; 46 import org.thingsboard.server.dao.tenant.TenantDao;
44 47
  48 +import java.util.HashMap;
45 import java.util.List; 49 import java.util.List;
  50 +import java.util.Map;
46 51
47 import static org.thingsboard.server.dao.service.Validator.validateId; 52 import static org.thingsboard.server.dao.service.Validator.validateId;
48 import static org.thingsboard.server.dao.service.Validator.validatePageLink; 53 import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@@ -52,10 +57,19 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @@ -52,10 +57,19 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
52 @Slf4j 57 @Slf4j
53 public class UserServiceImpl extends AbstractEntityService implements UserService { 58 public class UserServiceImpl extends AbstractEntityService implements UserService {
54 59
  60 + public static final String USER_PASSWORD_HISTORY = "userPasswordHistory";
  61 +
  62 + private static final String LAST_LOGIN_TS = "lastLoginTs";
  63 + private static final String FAILED_LOGIN_ATTEMPTS = "failedLoginAttempts";
  64 +
55 private static final int DEFAULT_TOKEN_LENGTH = 30; 65 private static final int DEFAULT_TOKEN_LENGTH = 30;
56 public static final String INCORRECT_USER_ID = "Incorrect userId "; 66 public static final String INCORRECT_USER_ID = "Incorrect userId ";
57 public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; 67 public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
58 68
  69 + private static final String USER_CREDENTIALS_ENABLED = "userCredentialsEnabled";
  70 +
  71 + private static final ObjectMapper objectMapper = new ObjectMapper();
  72 +
59 @Value("${security.user_login_case_sensitive:true}") 73 @Value("${security.user_login_case_sensitive:true}")
60 private boolean userLoginCaseSensitive; 74 private boolean userLoginCaseSensitive;
61 75
@@ -109,7 +123,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @@ -109,7 +123,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
109 userCredentials.setEnabled(false); 123 userCredentials.setEnabled(false);
110 userCredentials.setActivateToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); 124 userCredentials.setActivateToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
111 userCredentials.setUserId(new UserId(savedUser.getUuidId())); 125 userCredentials.setUserId(new UserId(savedUser.getUuidId()));
112 - userCredentialsDao.save(user.getTenantId(), userCredentials); 126 + saveUserCredentialsAndPasswordHistory(user.getTenantId(), userCredentials);
113 } 127 }
114 return savedUser; 128 return savedUser;
115 } 129 }
@@ -139,7 +153,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @@ -139,7 +153,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
139 public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) { 153 public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
140 log.trace("Executing saveUserCredentials [{}]", userCredentials); 154 log.trace("Executing saveUserCredentials [{}]", userCredentials);
141 userCredentialsValidator.validate(userCredentials, data -> tenantId); 155 userCredentialsValidator.validate(userCredentials, data -> tenantId);
142 - return userCredentialsDao.save(tenantId, userCredentials); 156 + return saveUserCredentialsAndPasswordHistory(tenantId, userCredentials);
143 } 157 }
144 158
145 @Override 159 @Override
@@ -193,7 +207,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @@ -193,7 +207,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
193 userCredentialsValidator.validate(userCredentials, data -> tenantId); 207 userCredentialsValidator.validate(userCredentials, data -> tenantId);
194 userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); 208 userCredentialsDao.removeById(tenantId, userCredentials.getUuidId());
195 userCredentials.setId(null); 209 userCredentials.setId(null);
196 - return userCredentialsDao.save(tenantId, userCredentials); 210 + return saveUserCredentialsAndPasswordHistory(tenantId, userCredentials);
197 } 211 }
198 212
199 @Override 213 @Override
@@ -240,6 +254,109 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @@ -240,6 +254,109 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
240 customerUsersRemover.removeEntities(tenantId, customerId); 254 customerUsersRemover.removeEntities(tenantId, customerId);
241 } 255 }
242 256
  257 + @Override
  258 + public void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled) {
  259 + log.trace("Executing setUserCredentialsEnabled [{}], [{}]", userId, enabled);
  260 + validateId(userId, INCORRECT_USER_ID + userId);
  261 + UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId());
  262 + userCredentials.setEnabled(enabled);
  263 + saveUserCredentials(tenantId, userCredentials);
  264 +
  265 + User user = findUserById(tenantId, userId);
  266 + JsonNode additionalInfo = user.getAdditionalInfo();
  267 + if (!(additionalInfo instanceof ObjectNode)) {
  268 + additionalInfo = objectMapper.createObjectNode();
  269 + }
  270 + ((ObjectNode) additionalInfo).put(USER_CREDENTIALS_ENABLED, enabled);
  271 + user.setAdditionalInfo(additionalInfo);
  272 + if (enabled) {
  273 + resetFailedLoginAttempts(user);
  274 + }
  275 + userDao.save(user.getTenantId(), user);
  276 + }
  277 +
  278 +
  279 + @Override
  280 + public void onUserLoginSuccessful(TenantId tenantId, UserId userId) {
  281 + log.trace("Executing onUserLoginSuccessful [{}]", userId);
  282 + User user = findUserById(tenantId, userId);
  283 + setLastLoginTs(user);
  284 + resetFailedLoginAttempts(user);
  285 + saveUser(user);
  286 + }
  287 +
  288 + private void setLastLoginTs(User user) {
  289 + JsonNode additionalInfo = user.getAdditionalInfo();
  290 + if (!(additionalInfo instanceof ObjectNode)) {
  291 + additionalInfo = objectMapper.createObjectNode();
  292 + }
  293 + ((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis());
  294 + user.setAdditionalInfo(additionalInfo);
  295 + }
  296 +
  297 + private void resetFailedLoginAttempts(User user) {
  298 + JsonNode additionalInfo = user.getAdditionalInfo();
  299 + if (!(additionalInfo instanceof ObjectNode)) {
  300 + additionalInfo = objectMapper.createObjectNode();
  301 + }
  302 + ((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, 0);
  303 + user.setAdditionalInfo(additionalInfo);
  304 + }
  305 +
  306 + @Override
  307 + public int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId) {
  308 + log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId);
  309 + User user = findUserById(tenantId, userId);
  310 + int failedLoginAttempts = increaseFailedLoginAttempts(user);
  311 + saveUser(user);
  312 + return failedLoginAttempts;
  313 + }
  314 +
  315 + private int increaseFailedLoginAttempts(User user) {
  316 + JsonNode additionalInfo = user.getAdditionalInfo();
  317 + if (!(additionalInfo instanceof ObjectNode)) {
  318 + additionalInfo = objectMapper.createObjectNode();
  319 + }
  320 + int failedLoginAttempts = 0;
  321 + if (additionalInfo.has(FAILED_LOGIN_ATTEMPTS)) {
  322 + failedLoginAttempts = additionalInfo.get(FAILED_LOGIN_ATTEMPTS).asInt();
  323 + }
  324 + failedLoginAttempts = failedLoginAttempts + 1;
  325 + ((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, failedLoginAttempts);
  326 + user.setAdditionalInfo(additionalInfo);
  327 + return failedLoginAttempts;
  328 + }
  329 +
  330 + private UserCredentials saveUserCredentialsAndPasswordHistory(TenantId tenantId, UserCredentials userCredentials) {
  331 + UserCredentials result = userCredentialsDao.save(tenantId, userCredentials);
  332 + User user = findUserById(tenantId, userCredentials.getUserId());
  333 + if (userCredentials.getPassword() != null) {
  334 + updatePasswordHistory(user, userCredentials);
  335 + }
  336 + return result;
  337 + }
  338 +
  339 + private void updatePasswordHistory(User user, UserCredentials userCredentials) {
  340 + JsonNode additionalInfo = user.getAdditionalInfo();
  341 + if (!(additionalInfo instanceof ObjectNode)) {
  342 + additionalInfo = objectMapper.createObjectNode();
  343 + }
  344 + if (additionalInfo.has(USER_PASSWORD_HISTORY)) {
  345 + JsonNode userPasswordHistoryJson = additionalInfo.get(USER_PASSWORD_HISTORY);
  346 + Map<String, String> userPasswordHistoryMap = objectMapper.convertValue(userPasswordHistoryJson, Map.class);
  347 + userPasswordHistoryMap.put(Long.toString(System.currentTimeMillis()), userCredentials.getPassword());
  348 + userPasswordHistoryJson = objectMapper.valueToTree(userPasswordHistoryMap);
  349 + ((ObjectNode) additionalInfo).replace(USER_PASSWORD_HISTORY, userPasswordHistoryJson);
  350 + } else {
  351 + Map<String, String> userPasswordHistoryMap = new HashMap<>();
  352 + userPasswordHistoryMap.put(Long.toString(System.currentTimeMillis()), userCredentials.getPassword());
  353 + JsonNode userPasswordHistoryJson = objectMapper.valueToTree(userPasswordHistoryMap);
  354 + ((ObjectNode) additionalInfo).set(USER_PASSWORD_HISTORY, userPasswordHistoryJson);
  355 + }
  356 + user.setAdditionalInfo(additionalInfo);
  357 + saveUser(user);
  358 + }
  359 +
243 private DataValidator<User> userValidator = 360 private DataValidator<User> userValidator =
244 new DataValidator<User>() { 361 new DataValidator<User>() {
245 @Override 362 @Override
@@ -15,12 +15,10 @@ @@ -15,12 +15,10 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.api; 16 package org.thingsboard.rule.engine.api;
17 17
18 -import org.thingsboard.server.common.data.exception.ThingsboardException;  
19 -  
20 import com.fasterxml.jackson.databind.JsonNode; 18 import com.fasterxml.jackson.databind.JsonNode;
  19 +import org.thingsboard.server.common.data.exception.ThingsboardException;
21 20
22 import javax.mail.MessagingException; 21 import javax.mail.MessagingException;
23 -import javax.mail.internet.MimeMessage;  
24 22
25 public interface MailService { 23 public interface MailService {
26 24
@@ -39,4 +37,6 @@ public interface MailService { @@ -39,4 +37,6 @@ public interface MailService {
39 void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException; 37 void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException;
40 38
41 void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException; 39 void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException;
  40 +
  41 + void sendAccountLockoutEmail( String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
42 } 42 }
@@ -30,6 +30,37 @@ @@ -30,6 +30,37 @@
30 <form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm"> 30 <form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm">
31 <fieldset ng-disabled="$root.loading"> 31 <fieldset ng-disabled="$root.loading">
32 <md-expansion-panel-group md-component-id="securitySettingsPanelGroup" auto-expand="true" multiple> 32 <md-expansion-panel-group md-component-id="securitySettingsPanelGroup" auto-expand="true" multiple>
  33 + <md-expansion-panel md-component-id="generalPolicyPanel" id="generalPolicyPanel">
  34 + <md-expansion-panel-collapsed>
  35 + <div class="tb-panel-title" translate>admin.general-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('generalPolicyPanel').collapse()">
  40 + <div class="tb-panel-title" translate>admin.general-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.max-failed-login-attempts</label>
  46 + <input type="number"
  47 + step="1"
  48 + min="0"
  49 + name="maxFailedLoginAttempts"
  50 + ng-model="vm.securitySettings.maxFailedLoginAttempts">
  51 + <div ng-messages="vm.settingsForm.maxFailedLoginAttempts.$error">
  52 + <div translate ng-message="min">admin.minimum-max-failed-login-attempts-range</div>
  53 + </div>
  54 + </md-input-container>
  55 + <md-input-container class="md-block">
  56 + <label translate>admin.user-lockout-notification-email</label>
  57 + <input type="email"
  58 + name="userLockoutNotificationEmail"
  59 + ng-model="vm.securitySettings.userLockoutNotificationEmail">
  60 + </md-input-container>
  61 + </md-expansion-panel-content>
  62 + </md-expansion-panel-expanded>
  63 + </md-expansion-panel>
33 <md-expansion-panel md-component-id="passwordPolicyPanel" id="passwordPolicyPanel"> 64 <md-expansion-panel md-component-id="passwordPolicyPanel" id="passwordPolicyPanel">
34 <md-expansion-panel-collapsed> 65 <md-expansion-panel-collapsed>
35 <div class="tb-panel-title" translate>admin.password-policy</div> 66 <div class="tb-panel-title" translate>admin.password-policy</div>
@@ -111,6 +142,17 @@ @@ -111,6 +142,17 @@
111 <div translate ng-message="min">admin.password-expiration-period-days-range</div> 142 <div translate ng-message="min">admin.password-expiration-period-days-range</div>
112 </div> 143 </div>
113 </md-input-container> 144 </md-input-container>
  145 + <md-input-container class="md-block">
  146 + <label translate>admin.password-reuse-frequency-days</label>
  147 + <input type="number"
  148 + step="1"
  149 + min="0"
  150 + name="passwordReuseFrequencyDays"
  151 + ng-model="vm.securitySettings.passwordPolicy.passwordReuseFrequencyDays">
  152 + <div ng-messages="vm.settingsForm.passwordReuseFrequencyDays.$error">
  153 + <div translate ng-message="min">admin.password-reuse-frequency-days-range</div>
  154 + </div>
  155 + </md-input-container>
114 </md-expansion-panel-content> 156 </md-expansion-panel-content>
115 </md-expansion-panel-expanded> 157 </md-expansion-panel-expanded>
116 </md-expansion-panel> 158 </md-expansion-panel>
@@ -64,7 +64,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time @@ -64,7 +64,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
64 logout: logout, 64 logout: logout,
65 reloadUser: reloadUser, 65 reloadUser: reloadUser,
66 isUserTokenAccessEnabled: isUserTokenAccessEnabled, 66 isUserTokenAccessEnabled: isUserTokenAccessEnabled,
67 - loginAsUser: loginAsUser 67 + loginAsUser: loginAsUser,
  68 + setUserCredentialsEnabled: setUserCredentialsEnabled
68 } 69 }
69 70
70 reloadUser(); 71 reloadUser();
@@ -496,6 +497,20 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time @@ -496,6 +497,20 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
496 return deferred.promise; 497 return deferred.promise;
497 } 498 }
498 499
  500 + function setUserCredentialsEnabled(userId, userCredentialsEnabled) {
  501 + var deferred = $q.defer();
  502 + var url = '/api/user/' + userId + '/userCredentialsEnabled';
  503 + if (angular.isDefined(userCredentialsEnabled)) {
  504 + url += '?userCredentialsEnabled=' + userCredentialsEnabled;
  505 + }
  506 + $http.post(url, null).then(function success() {
  507 + deferred.resolve();
  508 + }, function fail() {
  509 + deferred.reject();
  510 + });
  511 + return deferred.promise;
  512 + }
  513 +
499 function getUser(userId, ignoreErrors, config) { 514 function getUser(userId, ignoreErrors, config) {
500 var deferred = $q.defer(); 515 var deferred = $q.defer();
501 var url = '/api/user/' + userId; 516 var url = '/api/user/' + userId;
@@ -219,6 +219,9 @@ export default angular.module('thingsboard.types', []) @@ -219,6 +219,9 @@ export default angular.module('thingsboard.types', [])
219 }, 219 },
220 "LOGOUT": { 220 "LOGOUT": {
221 name: "audit-log.type-logout" 221 name: "audit-log.type-logout"
  222 + },
  223 + "LOCKOUT": {
  224 + name: "audit-log.type-lockout"
222 } 225 }
223 }, 226 },
224 auditLogActionStatus: { 227 auditLogActionStatus: {
@@ -100,7 +100,13 @@ @@ -100,7 +100,13 @@
100 "minimum-special-characters": "Minimum number of special characters", 100 "minimum-special-characters": "Minimum number of special characters",
101 "minimum-special-characters-range": "Minimum number of special characters can't be negative", 101 "minimum-special-characters-range": "Minimum number of special characters can't be negative",
102 "password-expiration-period-days": "Password expiration period in days", 102 "password-expiration-period-days": "Password expiration period in days",
103 - "password-expiration-period-days-range": "Password expiration period in days can't be negative" 103 + "password-expiration-period-days-range": "Password expiration period in days can't be negative",
  104 + "password-reuse-frequency-days": "Password reuse frequency in days",
  105 + "password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative",
  106 + "general-policy": "General policy",
  107 + "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked",
  108 + "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative",
  109 + "user-lockout-notification-email": "In case user account lockout, send notification to email"
104 }, 110 },
105 "alarm": { 111 "alarm": {
106 "alarm": "Alarm", 112 "alarm": "Alarm",
@@ -324,6 +330,7 @@ @@ -324,6 +330,7 @@
324 "type-alarm-clear": "Cleared", 330 "type-alarm-clear": "Cleared",
325 "type-login": "Login", 331 "type-login": "Login",
326 "type-logout": "Logout", 332 "type-logout": "Logout",
  333 + "type-lockout": "Lockout",
327 "status-success": "Success", 334 "status-success": "Success",
328 "status-failure": "Failure", 335 "status-failure": "Failure",
329 "audit-log-details": "Audit log details", 336 "audit-log-details": "Audit log details",
@@ -1216,6 +1223,7 @@ @@ -1216,6 +1223,7 @@
1216 }, 1223 },
1217 "profile": { 1224 "profile": {
1218 "profile": "Profile", 1225 "profile": "Profile",
  1226 + "last-login-time": "Last Login",
1219 "change-password": "Change Password", 1227 "change-password": "Change Password",
1220 "current-password": "Current password" 1228 "current-password": "Current password"
1221 }, 1229 },
@@ -1456,7 +1464,11 @@ @@ -1456,7 +1464,11 @@
1456 "activation-link-copied-message": "User activation link has been copied to clipboard", 1464 "activation-link-copied-message": "User activation link has been copied to clipboard",
1457 "details": "Details", 1465 "details": "Details",
1458 "login-as-tenant-admin": "Login as Tenant Admin", 1466 "login-as-tenant-admin": "Login as Tenant Admin",
1459 - "login-as-customer-user": "Login as Customer User" 1467 + "login-as-customer-user": "Login as Customer User",
  1468 + "disable-account": "Disable User Account",
  1469 + "enable-account": "Enable User Account",
  1470 + "enable-account-message": "User account was successfully enabled!",
  1471 + "disable-account-message": "User account was successfully disabled!"
1460 }, 1472 },
1461 "value": { 1473 "value": {
1462 "type": "Value type", 1474 "type": "Value type",
@@ -22,6 +22,10 @@ @@ -22,6 +22,10 @@
22 <span translate class="md-headline">profile.profile</span> 22 <span translate class="md-headline">profile.profile</span>
23 <span style='opacity: 0.7;'>{{ vm.profileUser.email }}</span> 23 <span style='opacity: 0.7;'>{{ vm.profileUser.email }}</span>
24 </md-card-title-text> 24 </md-card-title-text>
  25 + <md-card-title-text>
  26 + <span translate class="md-subhead">profile.last-login-time</span>
  27 + <span style='opacity: 0.7;'>{{ vm.profileUser.additionalInfo.lastLoginTs | date : 'yyyy-MM-dd HH:mm:ss' }}</span>
  28 + </md-card-title-text>
25 </md-card-title> 29 </md-card-title>
26 <md-progress-linear md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear> 30 <md-progress-linear md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
27 <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span> 31 <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
@@ -15,6 +15,12 @@ @@ -15,6 +15,12 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
  18 +<md-button ng-click="onSetUserCredentialsEnabled({userCredentialsEnabled: false})" ng-show="!isEdit && isUserCredentialsEnabled()" class="md-raised md-primary">{{ 'user.disable-account' |
  19 + translate }}
  20 +</md-button>
  21 +<md-button ng-click="onSetUserCredentialsEnabled({userCredentialsEnabled: true})" ng-show="!isEdit && !isUserCredentialsEnabled()" class="md-raised md-primary">{{ 'user.enable-account' |
  22 + translate }}
  23 +</md-button>
18 <md-button ng-click="onDisplayActivationLink({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 24 <md-button ng-click="onDisplayActivationLink({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{
19 'user.display-activation-link' | translate }} 25 'user.display-activation-link' | translate }}
20 </md-button> 26 </md-button>
@@ -90,6 +90,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d @@ -90,6 +90,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
90 vm.displayActivationLink = displayActivationLink; 90 vm.displayActivationLink = displayActivationLink;
91 vm.resendActivation = resendActivation; 91 vm.resendActivation = resendActivation;
92 vm.loginAsUser = loginAsUser; 92 vm.loginAsUser = loginAsUser;
  93 + vm.setUserCredentialsEnabled = setUserCredentialsEnabled;
93 94
94 initController(); 95 initController();
95 96
@@ -176,6 +177,22 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d @@ -176,6 +177,22 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
176 ); 177 );
177 } 178 }
178 179
  180 + function setUserCredentialsEnabled(user, userCredentialsEnabled) {
  181 + userService.setUserCredentialsEnabled(user.id.id, userCredentialsEnabled).then(
  182 + () => {
  183 + if (!user.additionalInfo) {
  184 + user.additionalInfo = {};
  185 + }
  186 + user.additionalInfo.userCredentialsEnabled = userCredentialsEnabled;
  187 + if (userCredentialsEnabled) {
  188 + toast.showSuccess($translate.instant('user.enable-account-message'));
  189 + } else {
  190 + toast.showSuccess($translate.instant('user.disable-account-message'));
  191 + }
  192 + }
  193 + )
  194 + }
  195 +
179 function openActivationLinkDialog(event, activationLink) { 196 function openActivationLinkDialog(event, activationLink) {
180 $mdDialog.show({ 197 $mdDialog.show({
181 controller: 'ActivationLinkDialogController', 198 controller: 'ActivationLinkDialogController',
@@ -35,6 +35,13 @@ export default function UserDirective($compile, $templateCache, userService) { @@ -35,6 +35,13 @@ export default function UserDirective($compile, $templateCache, userService) {
35 return scope.user && scope.user.authority === 'CUSTOMER_USER'; 35 return scope.user && scope.user.authority === 'CUSTOMER_USER';
36 }; 36 };
37 37
  38 + scope.isUserCredentialsEnabled = function() {
  39 + if (!scope.user || !scope.user.additionalInfo) {
  40 + return true;
  41 + }
  42 + return scope.user.additionalInfo.userCredentialsEnabled === true;
  43 + };
  44 +
38 scope.loginAsUserEnabled = userService.isUserTokenAccessEnabled(); 45 scope.loginAsUserEnabled = userService.isUserTokenAccessEnabled();
39 46
40 $compile(element.contents())(scope); 47 $compile(element.contents())(scope);
@@ -49,7 +56,8 @@ export default function UserDirective($compile, $templateCache, userService) { @@ -49,7 +56,8 @@ export default function UserDirective($compile, $templateCache, userService) {
49 onDisplayActivationLink: '&', 56 onDisplayActivationLink: '&',
50 onResendActivation: '&', 57 onResendActivation: '&',
51 onLoginAsUser: '&', 58 onLoginAsUser: '&',
52 - onDeleteUser: '&' 59 + onDeleteUser: '&',
  60 + onSetUserCredentialsEnabled: '&',
53 } 61 }
54 }; 62 };
55 } 63 }
@@ -28,7 +28,8 @@ @@ -28,7 +28,8 @@
28 on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)" 28 on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
29 on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)" 29 on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
30 on-login-as-user="vm.loginAsUser(vm.grid.detailsConfig.currentItem)" 30 on-login-as-user="vm.loginAsUser(vm.grid.detailsConfig.currentItem)"
31 - on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user> 31 + on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"
  32 + on-set-user-credentials-enabled="vm.setUserCredentialsEnabled(vm.grid.detailsConfig.currentItem, userCredentialsEnabled)"></tb-user>
32 </md-tab> 33 </md-tab>
33 <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}"> 34 <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
34 <tb-audit-log-table flex user-id="vm.grid.operatingItem().id.id" 35 <tb-audit-log-table flex user-id="vm.grid.operatingItem().id.id"