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 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import org.apache.commons.lang3.StringUtils;
18 19 import org.springframework.security.access.prepost.PreAuthorize;
19 20 import org.springframework.web.bind.annotation.PathVariable;
20 21 import org.springframework.web.bind.annotation.RequestMapping;
... ... @@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
22 23 import org.springframework.web.bind.annotation.RequestParam;
23 24 import org.springframework.web.bind.annotation.ResponseBody;
24 25 import org.springframework.web.bind.annotation.RestController;
  26 +import org.thingsboard.server.common.data.audit.ActionType;
25 27 import org.thingsboard.server.common.data.audit.AuditLog;
26 28 import org.thingsboard.server.common.data.exception.ThingsboardException;
27 29 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -31,7 +33,10 @@ import org.thingsboard.server.common.data.id.UserId;
31 33 import org.thingsboard.server.common.data.page.TimePageData;
32 34 import org.thingsboard.server.common.data.page.TimePageLink;
33 35
  36 +import java.util.Arrays;
  37 +import java.util.List;
34 38 import java.util.UUID;
  39 +import java.util.stream.Collectors;
35 40
36 41 @RestController
37 42 @RequestMapping("/api")
... ... @@ -46,12 +51,14 @@ public class AuditLogController extends BaseController {
46 51 @RequestParam(required = false) Long startTime,
47 52 @RequestParam(required = false) Long endTime,
48 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 56 try {
51 57 checkParameter("CustomerId", strCustomerId);
52 58 TenantId tenantId = getCurrentUser().getTenantId();
53 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 62 } catch (Exception e) {
56 63 throw handleException(e);
57 64 }
... ... @@ -66,12 +73,14 @@ public class AuditLogController extends BaseController {
66 73 @RequestParam(required = false) Long startTime,
67 74 @RequestParam(required = false) Long endTime,
68 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 78 try {
71 79 checkParameter("UserId", strUserId);
72 80 TenantId tenantId = getCurrentUser().getTenantId();
73 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 84 } catch (Exception e) {
76 85 throw handleException(e);
77 86 }
... ... @@ -87,13 +96,15 @@ public class AuditLogController extends BaseController {
87 96 @RequestParam(required = false) Long startTime,
88 97 @RequestParam(required = false) Long endTime,
89 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 101 try {
92 102 checkParameter("EntityId", strEntityId);
93 103 checkParameter("EntityType", strEntityType);
94 104 TenantId tenantId = getCurrentUser().getTenantId();
95 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 108 } catch (Exception e) {
98 109 throw handleException(e);
99 110 }
... ... @@ -107,13 +118,24 @@ public class AuditLogController extends BaseController {
107 118 @RequestParam(required = false) Long startTime,
108 119 @RequestParam(required = false) Long endTime,
109 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 123 try {
112 124 TenantId tenantId = getCurrentUser().getTenantId();
113 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 128 } catch (Exception e) {
116 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 112 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
113 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 116 if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) {
117 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 206 try {
207 207 String activateToken = activateRequest.get("activateToken").asText();
208 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 210 String encodedPassword = passwordEncoder.encode(password);
211 211 UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword);
212 212 User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
... ... @@ -246,7 +246,7 @@ public class AuthController extends BaseController {
246 246 String password = resetPasswordRequest.get("password").asText();
247 247 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
248 248 if (userCredentials != null) {
249   - systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password);
  249 + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, userCredentials);
250 250 if (passwordEncoder.matches(password, userCredentials.getPassword())) {
251 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 126
127 127 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
128 128 @RequestMapping(value = "/user", method = RequestMethod.POST)
129   - @ResponseBody
  129 + @ResponseBody
130 130 public User saveUser(@RequestBody User user,
131 131 @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
132 132 HttpServletRequest request) throws ThingsboardException {
... ... @@ -285,5 +285,23 @@ public class UserController extends BaseController {
285 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 18 import com.fasterxml.jackson.databind.ObjectMapper;
19 19 import lombok.extern.slf4j.Slf4j;
20 20 import org.springframework.beans.factory.annotation.Autowired;
21   -import org.springframework.http.HttpHeaders;
22 21 import org.springframework.http.HttpStatus;
23 22 import org.springframework.http.MediaType;
24 23 import org.springframework.security.access.AccessDeniedException;
25 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 27 import org.springframework.security.core.AuthenticationException;
28 28 import org.springframework.security.web.access.AccessDeniedHandler;
29 29 import org.springframework.stereotype.Component;
30   -import org.thingsboard.server.common.data.EntityType;
31 30 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
32 31 import org.thingsboard.server.common.data.exception.ThingsboardException;
33 32 import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
... ... @@ -39,8 +38,6 @@ import javax.servlet.ServletException;
39 38 import javax.servlet.http.HttpServletRequest;
40 39 import javax.servlet.http.HttpServletResponse;
41 40 import java.io.IOException;
42   -import java.net.URI;
43   -import java.net.URISyntaxException;
44 41
45 42 @Component
46 43 @Slf4j
... ... @@ -142,6 +139,10 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
142 139 response.setStatus(HttpStatus.UNAUTHORIZED.value());
143 140 if (authenticationException instanceof BadCredentialsException) {
144 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 146 } else if (authenticationException instanceof JwtExpiredTokenException) {
146 147 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
147 148 } else if (authenticationException instanceof AuthMethodNotSupportedException) {
... ...
... ... @@ -212,6 +212,21 @@ public class DefaultMailService implements MailService {
212 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 230 private void sendMail(JavaMailSenderImpl mailSender,
216 231 String mailFrom, String email,
217 232 String subject, String message) throws ThingsboardException {
... ...
... ... @@ -19,13 +19,12 @@ import lombok.extern.slf4j.Slf4j;
19 19 import org.springframework.beans.factory.annotation.Autowired;
20 20 import org.springframework.security.authentication.AuthenticationProvider;
21 21 import org.springframework.security.authentication.BadCredentialsException;
22   -import org.springframework.security.authentication.DisabledException;
23 22 import org.springframework.security.authentication.InsufficientAuthenticationException;
  23 +import org.springframework.security.authentication.LockedException;
24 24 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
25 25 import org.springframework.security.core.Authentication;
26 26 import org.springframework.security.core.AuthenticationException;
27 27 import org.springframework.security.core.userdetails.UsernameNotFoundException;
28   -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
29 28 import org.springframework.stereotype.Component;
30 29 import org.springframework.util.Assert;
31 30 import org.thingsboard.server.common.data.Customer;
... ... @@ -100,18 +99,21 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
100 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 109 if (user.getAuthority() == null)
106 110 throw new InsufficientAuthenticationException("User has no authority assigned");
107 111
108 112 SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
109   -
110   - logLoginAction(user, authentication, null);
111   -
  113 + logLoginAction(user, authentication, ActionType.LOGIN, null);
112 114 return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
113 115 } catch (Exception e) {
114   - logLoginAction(user, authentication, e);
  116 + logLoginAction(user, authentication, ActionType.LOGIN, e);
115 117 throw e;
116 118 }
117 119 }
... ... @@ -148,7 +150,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
148 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 154 String clientAddress = "Unknown";
153 155 String browser = "Unknown";
154 156 String os = "Unknown";
... ... @@ -194,6 +196,6 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
194 196 }
195 197 auditLogService.logEntityAction(
196 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 24
25 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 29 private Integer minimumSpecialCharacters;
30 30
31 31 private Integer passwordExpirationPeriodDays;
  32 + private Integer passwordReuseFrequencyDays;
32 33
33 34 }
... ...
... ... @@ -15,24 +15,38 @@
15 15 */
16 16 package org.thingsboard.server.service.security.system;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
18 19 import com.fasterxml.jackson.databind.ObjectMapper;
  20 +import com.fasterxml.jackson.databind.node.ObjectNode;
19 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 30 import org.springframework.beans.factory.annotation.Autowired;
22 31 import org.springframework.cache.annotation.CacheEvict;
23 32 import org.springframework.cache.annotation.Cacheable;
24 33 import org.springframework.security.authentication.BadCredentialsException;
25   -import org.springframework.security.authentication.CredentialsExpiredException;
26 34 import org.springframework.security.authentication.DisabledException;
  35 +import org.springframework.security.authentication.LockedException;
27 36 import org.springframework.security.core.AuthenticationException;
28 37 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
29 38 import org.springframework.stereotype.Service;
  39 +import org.thingsboard.rule.engine.api.MailService;
30 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 43 import org.thingsboard.server.common.data.id.TenantId;
32 44 import org.thingsboard.server.common.data.security.UserCredentials;
  45 +import org.thingsboard.server.dao.audit.AuditLogService;
33 46 import org.thingsboard.server.dao.exception.DataValidationException;
34 47 import org.thingsboard.server.dao.settings.AdminSettingsService;
35 48 import org.thingsboard.server.dao.user.UserService;
  49 +import org.thingsboard.server.dao.user.UserServiceImpl;
36 50 import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
37 51 import org.thingsboard.server.service.security.model.SecuritySettings;
38 52 import org.thingsboard.server.service.security.model.UserPasswordPolicy;
... ... @@ -40,9 +54,9 @@ import org.thingsboard.server.service.security.model.UserPasswordPolicy;
40 54 import javax.annotation.Resource;
41 55 import java.util.ArrayList;
42 56 import java.util.List;
  57 +import java.util.Map;
43 58 import java.util.concurrent.TimeUnit;
44 59
45   -import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;
46 60 import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
47 61
48 62 @Service
... ... @@ -60,6 +74,9 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
60 74 @Autowired
61 75 private UserService userService;
62 76
  77 + @Autowired
  78 + private MailService mailService;
  79 +
63 80 @Resource
64 81 private SystemSecurityService self;
65 82
... ... @@ -100,9 +117,23 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
100 117 }
101 118
102 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 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 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 141 throw new DisabledException("User is not active");
111 142 }
112 143
  144 + userService.onUserLoginSuccessful(tenantId, userCredentials.getUserId());
  145 +
113 146 SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
114 147 if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
115 148 if ((userCredentials.getCreatedTime()
... ... @@ -122,7 +155,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
122 155 }
123 156
124 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 159 SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
127 160 UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
128 161
... ... @@ -147,6 +180,22 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
147 180 String message = String.join("\n", validator.getMessages(result));
148 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 201 private static boolean isPositiveInteger(Integer val) {
... ...
... ... @@ -27,8 +27,8 @@ public interface SystemSecurityService {
27 27
28 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 account.activated.subject=Thingsboard - your account has been activated
4 4 reset.password.subject=Thingsboard - Password reset has been requested
5 5 password.was.reset.subject=Thingsboard - your account password has been reset
  6 +account.lockout.subject=Thingsboard - User account has been lockout
\ No newline at end of file
... ...
  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 32
33 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 43 <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction(
44 44 TenantId tenantId,
... ...
... ... @@ -60,5 +60,10 @@ public interface UserService {
60 60 TextPageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
61 61
62 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 39 ALARM_ACK(false),
40 40 ALARM_CLEAR(false),
41 41 LOGIN(false),
42   - LOGOUT(false);
  42 + LOGOUT(false),
  43 + LOCKOUT(false);
43 44
44 45 private final boolean isRead;
45 46
... ...
... ... @@ -16,6 +16,7 @@
16 16 package org.thingsboard.server.dao.audit;
17 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import org.thingsboard.server.common.data.audit.ActionType;
19 20 import org.thingsboard.server.common.data.audit.AuditLog;
20 21 import org.thingsboard.server.common.data.id.CustomerId;
21 22 import org.thingsboard.server.common.data.id.EntityId;
... ... @@ -37,11 +38,11 @@ public interface AuditLogDao {
37 38
38 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 81 private AuditLogSink auditLogSink;
82 82
83 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 85 log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink);
86 86 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
87 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 89 return new TimePageData<>(auditLogs, pageLink);
90 90 }
91 91
92 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 94 log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink);
95 95 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
96 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 98 return new TimePageData<>(auditLogs, pageLink);
99 99 }
100 100
101 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 103 log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink);
104 104 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
105 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 107 return new TimePageData<>(auditLogs, pageLink);
108 108 }
109 109
110 110 @Override
111   - public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) {
  111 + public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, List<ActionType> actionTypes, TimePageLink pageLink) {
112 112 log.trace("Executing findAuditLogs [{}]", pageLink);
113 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 115 return new TimePageData<>(auditLogs, pageLink);
116 116 }
117 117
... ... @@ -250,6 +250,7 @@ public class AuditLogServiceImpl implements AuditLogService {
250 250 break;
251 251 case LOGIN:
252 252 case LOGOUT:
  253 + case LOCKOUT:
253 254 String clientAddress = extractParameter(String.class, 0, additionalInfo);
254 255 String browser = extractParameter(String.class, 1, additionalInfo);
255 256 String os = extractParameter(String.class, 2, additionalInfo);
... ...
... ... @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired;
29 29 import org.springframework.beans.factory.annotation.Value;
30 30 import org.springframework.core.env.Environment;
31 31 import org.springframework.stereotype.Component;
  32 +import org.thingsboard.server.common.data.audit.ActionType;
32 33 import org.thingsboard.server.common.data.audit.AuditLog;
33 34 import org.thingsboard.server.common.data.id.CustomerId;
34 35 import org.thingsboard.server.common.data.id.EntityId;
... ... @@ -273,7 +274,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
273 274 }
274 275
275 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 278 log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink);
278 279 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_ENTITY_ID_CF,
279 280 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
... ... @@ -285,7 +286,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
285 286 }
286 287
287 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 290 log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink);
290 291 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_CUSTOMER_ID_CF,
291 292 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
... ... @@ -296,7 +297,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
296 297 }
297 298
298 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 301 log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink);
301 302 List<AuditLogEntity> entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_USER_ID_CF,
302 303 Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
... ... @@ -307,7 +308,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
307 308 }
308 309
309 310 @Override
310   - public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
  311 + public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, List<ActionType> actionTypes, TimePageLink pageLink) {
311 312 log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink);
312 313
313 314 long minPartition;
... ...
... ... @@ -37,22 +37,22 @@ import java.util.List;
37 37 public class DummyAuditLogServiceImpl implements AuditLogService {
38 38
39 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 41 return new TimePageData<>(null, pageLink);
42 42 }
43 43
44 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 46 return new TimePageData<>(null, pageLink);
47 47 }
48 48
49 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 51 return new TimePageData<>(null, pageLink);
52 52 }
53 53
54 54 @Override
55   - public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) {
  55 + public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, List<ActionType> actionTypes, TimePageLink pageLink) {
56 56 return new TimePageData<>(null, pageLink);
57 57 }
58 58
... ...
... ... @@ -73,7 +73,9 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
73 73 if (!existentAdminSettings.getKey().equals(adminSettings.getKey())) {
74 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 26 import org.springframework.data.repository.CrudRepository;
27 27 import org.springframework.stereotype.Component;
28 28 import org.thingsboard.server.common.data.UUIDConverter;
  29 +import org.thingsboard.server.common.data.audit.ActionType;
29 30 import org.thingsboard.server.common.data.audit.AuditLog;
30 31 import org.thingsboard.server.common.data.id.CustomerId;
31 32 import org.thingsboard.server.common.data.id.EntityId;
... ... @@ -101,34 +102,34 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
101 102 }
102 103
103 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 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 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 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 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 127 Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC;
127 128 Pageable pageable = new PageRequest(0, pageLink.getLimit(), sortDirection, ID_PROPERTY);
128 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 133 return (root, criteriaQuery, criteriaBuilder) -> {
133 134 List<Predicate> predicates = new ArrayList<>();
134 135 if (tenantId != null) {
... ... @@ -142,12 +143,15 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
142 143 predicates.add(entityIdPredicate);
143 144 }
144 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 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 156 return criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
153 157 };
... ...
... ... @@ -15,6 +15,9 @@
15 15 */
16 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 21 import com.google.common.util.concurrent.ListenableFuture;
19 22 import lombok.extern.slf4j.Slf4j;
20 23 import org.apache.commons.lang3.RandomStringUtils;
... ... @@ -42,7 +45,9 @@ import org.thingsboard.server.dao.service.DataValidator;
42 45 import org.thingsboard.server.dao.service.PaginatedRemover;
43 46 import org.thingsboard.server.dao.tenant.TenantDao;
44 47
  48 +import java.util.HashMap;
45 49 import java.util.List;
  50 +import java.util.Map;
46 51
47 52 import static org.thingsboard.server.dao.service.Validator.validateId;
48 53 import static org.thingsboard.server.dao.service.Validator.validatePageLink;
... ... @@ -52,10 +57,19 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
52 57 @Slf4j
53 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 65 private static final int DEFAULT_TOKEN_LENGTH = 30;
56 66 public static final String INCORRECT_USER_ID = "Incorrect userId ";
57 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 73 @Value("${security.user_login_case_sensitive:true}")
60 74 private boolean userLoginCaseSensitive;
61 75
... ... @@ -109,7 +123,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
109 123 userCredentials.setEnabled(false);
110 124 userCredentials.setActivateToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
111 125 userCredentials.setUserId(new UserId(savedUser.getUuidId()));
112   - userCredentialsDao.save(user.getTenantId(), userCredentials);
  126 + saveUserCredentialsAndPasswordHistory(user.getTenantId(), userCredentials);
113 127 }
114 128 return savedUser;
115 129 }
... ... @@ -139,7 +153,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
139 153 public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
140 154 log.trace("Executing saveUserCredentials [{}]", userCredentials);
141 155 userCredentialsValidator.validate(userCredentials, data -> tenantId);
142   - return userCredentialsDao.save(tenantId, userCredentials);
  156 + return saveUserCredentialsAndPasswordHistory(tenantId, userCredentials);
143 157 }
144 158
145 159 @Override
... ... @@ -193,7 +207,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
193 207 userCredentialsValidator.validate(userCredentials, data -> tenantId);
194 208 userCredentialsDao.removeById(tenantId, userCredentials.getUuidId());
195 209 userCredentials.setId(null);
196   - return userCredentialsDao.save(tenantId, userCredentials);
  210 + return saveUserCredentialsAndPasswordHistory(tenantId, userCredentials);
197 211 }
198 212
199 213 @Override
... ... @@ -240,6 +254,109 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
240 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 360 private DataValidator<User> userValidator =
244 361 new DataValidator<User>() {
245 362 @Override
... ...
... ... @@ -15,12 +15,10 @@
15 15 */
16 16 package org.thingsboard.rule.engine.api;
17 17
18   -import org.thingsboard.server.common.data.exception.ThingsboardException;
19   -
20 18 import com.fasterxml.jackson.databind.JsonNode;
  19 +import org.thingsboard.server.common.data.exception.ThingsboardException;
21 20
22 21 import javax.mail.MessagingException;
23   -import javax.mail.internet.MimeMessage;
24 22
25 23 public interface MailService {
26 24
... ... @@ -39,4 +37,6 @@ public interface MailService {
39 37 void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException;
40 38
41 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 30 <form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm">
31 31 <fieldset ng-disabled="$root.loading">
32 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 64 <md-expansion-panel md-component-id="passwordPolicyPanel" id="passwordPolicyPanel">
34 65 <md-expansion-panel-collapsed>
35 66 <div class="tb-panel-title" translate>admin.password-policy</div>
... ... @@ -111,6 +142,17 @@
111 142 <div translate ng-message="min">admin.password-expiration-period-days-range</div>
112 143 </div>
113 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 156 </md-expansion-panel-content>
115 157 </md-expansion-panel-expanded>
116 158 </md-expansion-panel>
... ...
... ... @@ -64,7 +64,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
64 64 logout: logout,
65 65 reloadUser: reloadUser,
66 66 isUserTokenAccessEnabled: isUserTokenAccessEnabled,
67   - loginAsUser: loginAsUser
  67 + loginAsUser: loginAsUser,
  68 + setUserCredentialsEnabled: setUserCredentialsEnabled
68 69 }
69 70
70 71 reloadUser();
... ... @@ -496,6 +497,20 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
496 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 514 function getUser(userId, ignoreErrors, config) {
500 515 var deferred = $q.defer();
501 516 var url = '/api/user/' + userId;
... ...
... ... @@ -219,6 +219,9 @@ export default angular.module('thingsboard.types', [])
219 219 },
220 220 "LOGOUT": {
221 221 name: "audit-log.type-logout"
  222 + },
  223 + "LOCKOUT": {
  224 + name: "audit-log.type-lockout"
222 225 }
223 226 },
224 227 auditLogActionStatus: {
... ...
... ... @@ -100,7 +100,13 @@
100 100 "minimum-special-characters": "Minimum number of special characters",
101 101 "minimum-special-characters-range": "Minimum number of special characters can't be negative",
102 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 111 "alarm": {
106 112 "alarm": "Alarm",
... ... @@ -324,6 +330,7 @@
324 330 "type-alarm-clear": "Cleared",
325 331 "type-login": "Login",
326 332 "type-logout": "Logout",
  333 + "type-lockout": "Lockout",
327 334 "status-success": "Success",
328 335 "status-failure": "Failure",
329 336 "audit-log-details": "Audit log details",
... ... @@ -1216,6 +1223,7 @@
1216 1223 },
1217 1224 "profile": {
1218 1225 "profile": "Profile",
  1226 + "last-login-time": "Last Login",
1219 1227 "change-password": "Change Password",
1220 1228 "current-password": "Current password"
1221 1229 },
... ... @@ -1456,7 +1464,11 @@
1456 1464 "activation-link-copied-message": "User activation link has been copied to clipboard",
1457 1465 "details": "Details",
1458 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 1473 "value": {
1462 1474 "type": "Value type",
... ...
... ... @@ -22,6 +22,10 @@
22 22 <span translate class="md-headline">profile.profile</span>
23 23 <span style='opacity: 0.7;'>{{ vm.profileUser.email }}</span>
24 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 29 </md-card-title>
26 30 <md-progress-linear md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
27 31 <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
... ...
... ... @@ -15,6 +15,12 @@
15 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 24 <md-button ng-click="onDisplayActivationLink({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{
19 25 'user.display-activation-link' | translate }}
20 26 </md-button>
... ...
... ... @@ -90,6 +90,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
90 90 vm.displayActivationLink = displayActivationLink;
91 91 vm.resendActivation = resendActivation;
92 92 vm.loginAsUser = loginAsUser;
  93 + vm.setUserCredentialsEnabled = setUserCredentialsEnabled;
93 94
94 95 initController();
95 96
... ... @@ -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 196 function openActivationLinkDialog(event, activationLink) {
180 197 $mdDialog.show({
181 198 controller: 'ActivationLinkDialogController',
... ...
... ... @@ -35,6 +35,13 @@ export default function UserDirective($compile, $templateCache, userService) {
35 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 45 scope.loginAsUserEnabled = userService.isUserTokenAccessEnabled();
39 46
40 47 $compile(element.contents())(scope);
... ... @@ -49,7 +56,8 @@ export default function UserDirective($compile, $templateCache, userService) {
49 56 onDisplayActivationLink: '&',
50 57 onResendActivation: '&',
51 58 onLoginAsUser: '&',
52   - onDeleteUser: '&'
  59 + onDeleteUser: '&',
  60 + onSetUserCredentialsEnabled: '&',
53 61 }
54 62 };
55 63 }
... ...
... ... @@ -28,7 +28,8 @@
28 28 on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
29 29 on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
30 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 33 </md-tab>
33 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 35 <tb-audit-log-table flex user-id="vm.grid.operatingItem().id.id"
... ...