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