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 | 272 | <groupId>io.springfox.ui</groupId> |
273 | 273 | <artifactId>springfox-swagger-ui-rfc6570</artifactId> |
274 | 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 | 283 | </dependencies> |
276 | 284 | |
277 | 285 | <build> | ... | ... |
... | ... | @@ -28,8 +28,10 @@ import org.thingsboard.server.common.data.AdminSettings; |
28 | 28 | import org.thingsboard.server.common.data.exception.ThingsboardException; |
29 | 29 | import org.thingsboard.server.common.data.id.TenantId; |
30 | 30 | import org.thingsboard.server.dao.settings.AdminSettingsService; |
31 | +import org.thingsboard.server.service.security.model.SecuritySettings; | |
31 | 32 | import org.thingsboard.server.service.security.permission.Operation; |
32 | 33 | import org.thingsboard.server.service.security.permission.Resource; |
34 | +import org.thingsboard.server.service.security.system.SystemSecurityService; | |
33 | 35 | import org.thingsboard.server.service.update.UpdateService; |
34 | 36 | import org.thingsboard.server.service.update.model.UpdateMessage; |
35 | 37 | |
... | ... | @@ -44,6 +46,9 @@ public class AdminController extends BaseController { |
44 | 46 | private AdminSettingsService adminSettingsService; |
45 | 47 | |
46 | 48 | @Autowired |
49 | + private SystemSecurityService systemSecurityService; | |
50 | + | |
51 | + @Autowired | |
47 | 52 | private UpdateService updateService; |
48 | 53 | |
49 | 54 | @PreAuthorize("hasAuthority('SYS_ADMIN')") |
... | ... | @@ -75,6 +80,31 @@ public class AdminController extends BaseController { |
75 | 80 | } |
76 | 81 | |
77 | 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 | 108 | @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) |
79 | 109 | public void sendTestMail(@RequestBody AdminSettings adminSettings) throws ThingsboardException { |
80 | 110 | try { | ... | ... |
... | ... | @@ -24,6 +24,7 @@ import org.springframework.http.HttpHeaders; |
24 | 24 | import org.springframework.http.HttpStatus; |
25 | 25 | import org.springframework.http.ResponseEntity; |
26 | 26 | import org.springframework.security.access.prepost.PreAuthorize; |
27 | +import org.springframework.security.core.Authentication; | |
27 | 28 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
28 | 29 | import org.springframework.web.bind.annotation.RequestBody; |
29 | 30 | import org.springframework.web.bind.annotation.RequestMapping; |
... | ... | @@ -34,15 +35,24 @@ import org.springframework.web.bind.annotation.ResponseStatus; |
34 | 35 | import org.springframework.web.bind.annotation.RestController; |
35 | 36 | import org.thingsboard.rule.engine.api.MailService; |
36 | 37 | import org.thingsboard.server.common.data.User; |
38 | +import org.thingsboard.server.common.data.audit.ActionType; | |
37 | 39 | import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
38 | 40 | import org.thingsboard.server.common.data.exception.ThingsboardException; |
39 | 41 | import org.thingsboard.server.common.data.id.TenantId; |
40 | 42 | import org.thingsboard.server.common.data.security.UserCredentials; |
43 | +import org.thingsboard.server.dao.audit.AuditLogService; | |
41 | 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 | 47 | import org.thingsboard.server.service.security.model.SecurityUser; |
48 | +import org.thingsboard.server.service.security.model.UserPasswordPolicy; | |
43 | 49 | import org.thingsboard.server.service.security.model.UserPrincipal; |
44 | 50 | import org.thingsboard.server.service.security.model.token.JwtToken; |
45 | 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 | 57 | import javax.servlet.http.HttpServletRequest; |
48 | 58 | import java.net.URI; |
... | ... | @@ -65,6 +75,12 @@ public class AuthController extends BaseController { |
65 | 75 | @Autowired |
66 | 76 | private MailService mailService; |
67 | 77 | |
78 | + @Autowired | |
79 | + private SystemSecurityService systemSecurityService; | |
80 | + | |
81 | + @Autowired | |
82 | + private AuditLogService auditLogService; | |
83 | + | |
68 | 84 | @PreAuthorize("isAuthenticated()") |
69 | 85 | @RequestMapping(value = "/auth/user", method = RequestMethod.GET) |
70 | 86 | public @ResponseBody User getUser() throws ThingsboardException { |
... | ... | @@ -77,6 +93,13 @@ public class AuthController extends BaseController { |
77 | 93 | } |
78 | 94 | |
79 | 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 | 103 | @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) |
81 | 104 | @ResponseStatus(value = HttpStatus.OK) |
82 | 105 | public void changePassword ( |
... | ... | @@ -89,8 +112,24 @@ public class AuthController extends BaseController { |
89 | 112 | if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { |
90 | 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 | 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 | 133 | } catch (Exception e) { |
95 | 134 | throw handleException(e); |
96 | 135 | } |
... | ... | @@ -167,6 +206,7 @@ public class AuthController extends BaseController { |
167 | 206 | try { |
168 | 207 | String activateToken = activateRequest.get("activateToken").asText(); |
169 | 208 | String password = activateRequest.get("password").asText(); |
209 | + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password); | |
170 | 210 | String encodedPassword = passwordEncoder.encode(password); |
171 | 211 | UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword); |
172 | 212 | User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId()); |
... | ... | @@ -206,10 +246,14 @@ public class AuthController extends BaseController { |
206 | 246 | String password = resetPasswordRequest.get("password").asText(); |
207 | 247 | UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); |
208 | 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 | 253 | String encodedPassword = passwordEncoder.encode(password); |
210 | 254 | userCredentials.setPassword(encodedPassword); |
211 | 255 | userCredentials.setResetToken(null); |
212 | - userCredentials = userService.saveUserCredentials(TenantId.SYS_TENANT_ID, userCredentials); | |
256 | + userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials); | |
213 | 257 | User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId()); |
214 | 258 | UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); |
215 | 259 | SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal); |
... | ... | @@ -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 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; |
19 | 19 | import lombok.extern.slf4j.Slf4j; |
20 | 20 | import org.springframework.beans.factory.annotation.Autowired; |
21 | +import org.springframework.http.HttpHeaders; | |
21 | 22 | import org.springframework.http.HttpStatus; |
22 | 23 | import org.springframework.http.MediaType; |
23 | 24 | import org.springframework.security.access.AccessDeniedException; |
24 | 25 | import org.springframework.security.authentication.BadCredentialsException; |
26 | +import org.springframework.security.authentication.CredentialsExpiredException; | |
25 | 27 | import org.springframework.security.core.AuthenticationException; |
26 | 28 | import org.springframework.security.web.access.AccessDeniedHandler; |
27 | 29 | import org.springframework.stereotype.Component; |
... | ... | @@ -31,11 +33,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; |
31 | 33 | import org.thingsboard.server.common.msg.tools.TbRateLimitsException; |
32 | 34 | import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; |
33 | 35 | import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; |
36 | +import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; | |
34 | 37 | |
35 | 38 | import javax.servlet.ServletException; |
36 | 39 | import javax.servlet.http.HttpServletRequest; |
37 | 40 | import javax.servlet.http.HttpServletResponse; |
38 | 41 | import java.io.IOException; |
42 | +import java.net.URI; | |
43 | +import java.net.URISyntaxException; | |
39 | 44 | |
40 | 45 | @Component |
41 | 46 | @Slf4j |
... | ... | @@ -141,8 +146,13 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { |
141 | 146 | mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); |
142 | 147 | } else if (authenticationException instanceof AuthMethodNotSupportedException) { |
143 | 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 | 15 | */ |
16 | 16 | package org.thingsboard.server.service.security.auth.rest; |
17 | 17 | |
18 | +import lombok.extern.slf4j.Slf4j; | |
18 | 19 | import org.springframework.beans.factory.annotation.Autowired; |
19 | 20 | import org.springframework.security.authentication.AuthenticationProvider; |
20 | 21 | import org.springframework.security.authentication.BadCredentialsException; |
... | ... | @@ -29,31 +30,41 @@ import org.springframework.stereotype.Component; |
29 | 30 | import org.springframework.util.Assert; |
30 | 31 | import org.thingsboard.server.common.data.Customer; |
31 | 32 | import org.thingsboard.server.common.data.User; |
33 | +import org.thingsboard.server.common.data.audit.ActionType; | |
32 | 34 | import org.thingsboard.server.common.data.id.CustomerId; |
33 | 35 | import org.thingsboard.server.common.data.id.EntityId; |
34 | 36 | import org.thingsboard.server.common.data.id.TenantId; |
35 | 37 | import org.thingsboard.server.common.data.id.UserId; |
36 | 38 | import org.thingsboard.server.common.data.security.Authority; |
37 | 39 | import org.thingsboard.server.common.data.security.UserCredentials; |
40 | +import org.thingsboard.server.dao.audit.AuditLogService; | |
38 | 41 | import org.thingsboard.server.dao.customer.CustomerService; |
39 | 42 | import org.thingsboard.server.dao.user.UserService; |
40 | 43 | import org.thingsboard.server.service.security.model.SecurityUser; |
41 | 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 | 48 | import java.util.UUID; |
44 | 49 | |
45 | 50 | @Component |
51 | +@Slf4j | |
46 | 52 | public class RestAuthenticationProvider implements AuthenticationProvider { |
47 | 53 | |
48 | - private final BCryptPasswordEncoder encoder; | |
54 | + private final SystemSecurityService systemSecurityService; | |
49 | 55 | private final UserService userService; |
50 | 56 | private final CustomerService customerService; |
57 | + private final AuditLogService auditLogService; | |
51 | 58 | |
52 | 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 | 64 | this.userService = userService; |
55 | 65 | this.customerService = customerService; |
56 | - this.encoder = encoder; | |
66 | + this.systemSecurityService = systemSecurityService; | |
67 | + this.auditLogService = auditLogService; | |
57 | 68 | } |
58 | 69 | |
59 | 70 | @Override |
... | ... | @@ -69,37 +80,40 @@ public class RestAuthenticationProvider implements AuthenticationProvider { |
69 | 80 | if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { |
70 | 81 | String username = userPrincipal.getValue(); |
71 | 82 | String password = (String) authentication.getCredentials(); |
72 | - return authenticateByUsernameAndPassword(userPrincipal, username, password); | |
83 | + return authenticateByUsernameAndPassword(authentication, userPrincipal, username, password); | |
73 | 84 | } else { |
74 | 85 | String publicId = userPrincipal.getValue(); |
75 | 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 | 91 | User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username); |
81 | 92 | if (user == null) { |
82 | 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 | 119 | private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) { |
... | ... | @@ -133,4 +147,53 @@ public class RestAuthenticationProvider implements AuthenticationProvider { |
133 | 147 | public boolean supports(Class<?> authentication) { |
134 | 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 | 19 | import lombok.extern.slf4j.Slf4j; |
20 | 20 | import org.apache.commons.lang3.StringUtils; |
21 | 21 | import org.springframework.http.HttpMethod; |
22 | +import org.springframework.security.authentication.AuthenticationDetailsSource; | |
22 | 23 | import org.springframework.security.authentication.AuthenticationServiceException; |
23 | 24 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
24 | 25 | import org.springframework.security.core.Authentication; |
... | ... | @@ -27,6 +28,7 @@ import org.springframework.security.core.context.SecurityContextHolder; |
27 | 28 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; |
28 | 29 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
29 | 30 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
31 | +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | |
30 | 32 | import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; |
31 | 33 | import org.thingsboard.server.service.security.model.UserPrincipal; |
32 | 34 | |
... | ... | @@ -39,6 +41,8 @@ import java.io.IOException; |
39 | 41 | @Slf4j |
40 | 42 | public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { |
41 | 43 | |
44 | + private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new RestAuthenticationDetailsSource(); | |
45 | + | |
42 | 46 | private final AuthenticationSuccessHandler successHandler; |
43 | 47 | private final AuthenticationFailureHandler failureHandler; |
44 | 48 | |
... | ... | @@ -76,7 +80,7 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF |
76 | 80 | UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername()); |
77 | 81 | |
78 | 82 | UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword()); |
79 | - | |
83 | + token.setDetails(authenticationDetailsSource.buildDetails(request)); | |
80 | 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 | +} | ... | ... |
... | ... | @@ -106,18 +106,6 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest { |
106 | 106 | } |
107 | 107 | |
108 | 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 | 109 | public void testSendTestMail() throws Exception { |
122 | 110 | loginSysAdmin(); |
123 | 111 | AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); | ... | ... |
... | ... | @@ -23,4 +23,5 @@ public class CacheConstants { |
23 | 23 | public static final String ASSET_CACHE = "assets"; |
24 | 24 | public static final String ENTITY_VIEW_CACHE = "entityViews"; |
25 | 25 | public static final String CLAIM_DEVICES_CACHE = "claimDevices"; |
26 | + public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; | |
26 | 27 | } | ... | ... |
... | ... | @@ -248,6 +248,17 @@ public class AuditLogServiceImpl implements AuditLogService { |
248 | 248 | EntityRelation relation = extractParameter(EntityRelation.class, 0, additionalInfo); |
249 | 249 | actionData.set("relation", objectMapper.valueToTree(relation)); |
250 | 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 | 263 | return actionData; |
253 | 264 | } | ... | ... |
... | ... | @@ -85,11 +85,5 @@ public abstract class DataValidator<D extends BaseData<?>> { |
85 | 85 | if (!expectedFields.containsAll(actualFields) || !actualFields.containsAll(expectedFields)) { |
86 | 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 | 19 | import org.thingsboard.server.common.data.User; |
20 | 20 | import org.thingsboard.server.common.data.id.CustomerId; |
21 | 21 | import org.thingsboard.server.common.data.id.TenantId; |
22 | +import org.thingsboard.server.common.data.id.UserCredentialsId; | |
22 | 23 | import org.thingsboard.server.common.data.id.UserId; |
23 | 24 | import org.thingsboard.server.common.data.page.TextPageData; |
24 | 25 | import org.thingsboard.server.common.data.page.TextPageLink; |
... | ... | @@ -46,6 +47,10 @@ public interface UserService { |
46 | 47 | |
47 | 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 | 54 | void deleteUser(TenantId tenantId, UserId userId); |
50 | 55 | |
51 | 56 | TextPageData<User> findTenantAdmins(TenantId tenantId, TextPageLink pageLink); | ... | ... |
... | ... | @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.Tenant; |
27 | 27 | import org.thingsboard.server.common.data.User; |
28 | 28 | import org.thingsboard.server.common.data.id.CustomerId; |
29 | 29 | import org.thingsboard.server.common.data.id.TenantId; |
30 | +import org.thingsboard.server.common.data.id.UserCredentialsId; | |
30 | 31 | import org.thingsboard.server.common.data.id.UserId; |
31 | 32 | import org.thingsboard.server.common.data.page.TextPageData; |
32 | 33 | import org.thingsboard.server.common.data.page.TextPageLink; |
... | ... | @@ -176,6 +177,24 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic |
176 | 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 | 199 | @Override |
181 | 200 | public void deleteUser(TenantId tenantId, UserId userId) { | ... | ... |
... | ... | @@ -76,13 +76,4 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest { |
76 | 76 | adminSettings.setJsonValue(json); |
77 | 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 | 89 | <fst.version>2.57</fst.version> |
90 | 90 | <antlr.version>2.7.7</antlr.version> |
91 | 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 | 94 | </properties> |
93 | 95 | |
94 | 96 | <modules> |
... | ... | @@ -840,6 +842,16 @@ |
840 | 842 | <artifactId>jts-core</artifactId> |
841 | 843 | <version>${jts.version}</version> |
842 | 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 | 855 | </dependencies> |
844 | 856 | </dependencyManagement> |
845 | 857 | ... | ... |
... | ... | @@ -17,6 +17,7 @@ |
17 | 17 | |
18 | 18 | import generalSettingsTemplate from '../admin/general-settings.tpl.html'; |
19 | 19 | import outgoingMailSettingsTemplate from '../admin/outgoing-mail-settings.tpl.html'; |
20 | +import securitySettingsTemplate from '../admin/security-settings.tpl.html'; | |
20 | 21 | |
21 | 22 | /* eslint-enable import/no-unresolved, import/default */ |
22 | 23 | |
... | ... | @@ -69,5 +70,23 @@ export default function AdminRoutes($stateProvider) { |
69 | 70 | ncyBreadcrumb: { |
70 | 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 | 22 | |
23 | 23 | import AdminRoutes from './admin.routes'; |
24 | 24 | import AdminController from './admin.controller'; |
25 | +import SecuritySettingsController from './security-settings.controller'; | |
25 | 26 | |
26 | 27 | export default angular.module('thingsboard.admin', [ |
27 | 28 | uiRouter, |
... | ... | @@ -33,4 +34,5 @@ export default angular.module('thingsboard.admin', [ |
33 | 34 | ]) |
34 | 35 | .config(AdminRoutes) |
35 | 36 | .controller('AdminController', AdminController) |
37 | + .controller('SecuritySettingsController', SecuritySettingsController) | |
36 | 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> | ... | ... |
... | ... | @@ -23,6 +23,8 @@ function AdminService($http, $q) { |
23 | 23 | var service = { |
24 | 24 | getAdminSettings: getAdminSettings, |
25 | 25 | saveAdminSettings: saveAdminSettings, |
26 | + getSecuritySettings: getSecuritySettings, | |
27 | + saveSecuritySettings: saveSecuritySettings, | |
26 | 28 | sendTestMail: sendTestMail, |
27 | 29 | checkUpdates: checkUpdates |
28 | 30 | } |
... | ... | @@ -51,6 +53,28 @@ function AdminService($http, $q) { |
51 | 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 | 78 | function sendTestMail(settings) { |
55 | 79 | var deferred = $q.defer(); |
56 | 80 | var url = '/api/admin/settings/testMail'; | ... | ... |
... | ... | @@ -141,7 +141,11 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time |
141 | 141 | } |
142 | 142 | |
143 | 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 | 151 | function clearJwtToken(doLogout) { | ... | ... |
... | ... | @@ -20,6 +20,7 @@ export default angular.module('thingsboard.types', []) |
20 | 20 | general: 2, |
21 | 21 | authentication: 10, |
22 | 22 | jwtTokenExpired: 11, |
23 | + credentialsExpired: 15, | |
23 | 24 | permissionDenied: 20, |
24 | 25 | invalidArguments: 30, |
25 | 26 | badRequestParams: 31, |
... | ... | @@ -212,6 +213,12 @@ export default angular.module('thingsboard.types', []) |
212 | 213 | }, |
213 | 214 | "ALARM_CLEAR": { |
214 | 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 | 224 | auditLogActionStatus: { | ... | ... |
... | ... | @@ -170,7 +170,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { |
170 | 170 | var errorCode = rejectionErrorCode(rejection); |
171 | 171 | if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) { |
172 | 172 | return refreshTokenAndRetry(rejection); |
173 | - } else { | |
173 | + } else if (errorCode !== getTypes().serverErrorCode.credentialsExpired) { | |
174 | 174 | unhandled = true; |
175 | 175 | } |
176 | 176 | } else if (rejection.status === 403) { | ... | ... |
... | ... | @@ -56,6 +56,7 @@ export default angular.module('thingsboard.help', []) |
56 | 56 | { |
57 | 57 | linksMap: { |
58 | 58 | outgoingMailSettings: helpBaseUrl + "/docs/user-guide/ui/mail-settings", |
59 | + securitySettings: helpBaseUrl + "/docs/user-guide/ui/security-settings", | |
59 | 60 | ruleEngine: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/overview/", |
60 | 61 | ruleNodeCheckRelation: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node", |
61 | 62 | ruleNodeJsFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node", | ... | ... |
... | ... | @@ -84,7 +84,22 @@ |
84 | 84 | "timeout-required": "Timeout is required.", |
85 | 85 | "timeout-invalid": "That doesn't look like a valid timeout.", |
86 | 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 | 104 | "alarm": { |
90 | 105 | "alarm": "Alarm", |
... | ... | @@ -306,6 +321,8 @@ |
306 | 321 | "type-relations-delete": "All relation deleted", |
307 | 322 | "type-alarm-ack": "Acknowledged", |
308 | 323 | "type-alarm-clear": "Cleared", |
324 | + "type-login": "Login", | |
325 | + "type-logout": "Logout", | |
309 | 326 | "status-success": "Success", |
310 | 327 | "status-failure": "Failure", |
311 | 328 | "audit-log-details": "Audit log details", |
... | ... | @@ -1183,6 +1200,7 @@ |
1183 | 1200 | "remember-me": "Remember me", |
1184 | 1201 | "forgot-password": "Forgot Password?", |
1185 | 1202 | "password-reset": "Password reset", |
1203 | + "expired-password-reset-message": "Your credentials has been expired! Please create new password.", | |
1186 | 1204 | "new-password": "New password", |
1187 | 1205 | "new-password-again": "New password again", |
1188 | 1206 | "password-link-sent-message": "Password reset link was successfully sent!", | ... | ... |
... | ... | @@ -20,7 +20,7 @@ import logoSvg from '../../svg/logo_title_white.svg'; |
20 | 20 | /* eslint-enable import/no-unresolved, import/default */ |
21 | 21 | |
22 | 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 | 24 | var vm = this; |
25 | 25 | |
26 | 26 | vm.logoSvg = logoSvg; |
... | ... | @@ -37,7 +37,7 @@ export default function LoginController(toast, loginService, userService/*, $roo |
37 | 37 | var token = response.data.token; |
38 | 38 | var refreshToken = response.data.refreshToken; |
39 | 39 | userService.setUserFromJwtToken(token, refreshToken, true); |
40 | - }, function fail(/*response*/) { | |
40 | + }, function fail(response) { | |
41 | 41 | /*if (response && response.data && response.data.message) { |
42 | 42 | toast.showError(response.data.message); |
43 | 43 | } else if (response && response.statusText) { |
... | ... | @@ -45,6 +45,11 @@ export default function LoginController(toast, loginService, userService/*, $roo |
45 | 45 | } else { |
46 | 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 | 63 | data: { |
64 | 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 | 80 | }).state('login.createPassword', { |
67 | 81 | url: '/createPassword?activateToken', |
68 | 82 | module: 'public', | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | /*@ngInject*/ |
17 | -export default function ResetPasswordController($stateParams, $translate, toast, loginService, userService) { | |
17 | +export default function ResetPasswordController($stateParams, $state, $translate, toast, loginService, userService) { | |
18 | 18 | var vm = this; |
19 | 19 | |
20 | 20 | vm.newPassword = ''; |
... | ... | @@ -22,6 +22,8 @@ export default function ResetPasswordController($stateParams, $translate, toast, |
22 | 22 | |
23 | 23 | vm.resetPassword = resetPassword; |
24 | 24 | |
25 | + vm.isExpiredPassword = $state.$current.data.expiredPassword === true; | |
26 | + | |
25 | 27 | function resetPassword() { |
26 | 28 | if (vm.newPassword !== vm.newPassword2) { |
27 | 29 | toast.showError($translate.instant('login.passwords-mismatch-error')); | ... | ... |
... | ... | @@ -20,6 +20,7 @@ |
20 | 20 | <md-card-title> |
21 | 21 | <md-card-title-text> |
22 | 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 | 24 | </md-card-title-text> |
24 | 25 | </md-card-title> |
25 | 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 | 82 | name: 'admin.system-settings', |
83 | 83 | type: 'toggle', |
84 | 84 | state: 'home.settings', |
85 | - height: '80px', | |
85 | + height: '120px', | |
86 | 86 | icon: 'settings', |
87 | 87 | pages: [ |
88 | 88 | { |
... | ... | @@ -96,6 +96,12 @@ function Menu(userService, $state, $rootScope) { |
96 | 96 | type: 'link', |
97 | 97 | state: 'home.settings.outgoing-mail', |
98 | 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 | 138 | name: 'admin.outgoing-mail', |
133 | 139 | icon: 'mail', |
134 | 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 | }]; | ... | ... |