Commit 3935f616c7a30846211ec128879cca6653bafce7
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 | } | ... | ... |
... | ... | @@ -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 | + — 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 | } | ... | ... |
... | ... | @@ -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; | ... | ... |
... | ... | @@ -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" | ... | ... |