Commit 8d5d8b2c234e2f71eeec6a04b6daab572cb50773

Authored by Igor Kulikov
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 +}
... ...
  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 +}
... ...
  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 +}
... ...
  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 269 claimDevices:
270 270 timeToLiveInMinutes: 1
271 271 maxSize: 100000
  272 + securitySettings:
  273 + timeToLiveInMinutes: 1440
  274 + maxSize: 1
272 275
273 276 redis:
274 277 # standalone or cluster
... ...
... ... @@ -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 }
... ...
... ... @@ -37,7 +37,9 @@ public enum ActionType {
37 37 RELATION_DELETED(false),
38 38 RELATIONS_DELETED(false),
39 39 ALARM_ACK(false),
40   - ALARM_CLEAR(false);
  40 + ALARM_CLEAR(false),
  41 + LOGIN(false),
  42 + LOGOUT(false);
41 43
42 44 private final boolean isRead;
43 45
... ...
... ... @@ -22,6 +22,7 @@ public enum ThingsboardErrorCode {
22 22 GENERAL(2),
23 23 AUTHENTICATION(10),
24 24 JWT_TOKEN_EXPIRED(11),
  25 + CREDENTIALS_EXPIRED(15),
25 26 PERMISSION_DENIED(20),
26 27 INVALID_ARGUMENTS(30),
27 28 BAD_REQUEST_PARAMS(31),
... ...
... ... @@ -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 }
... ...
1 1 {
2 2 "name": "thingsboard-js-executor",
3   - "version": "2.4.0",
  3 + "version": "2.4.1",
4 4 "lockfileVersion": 1,
5 5 "requires": true,
6 6 "dependencies": {
... ...
1 1 {
2 2 "name": "thingsboard-web-ui",
3   - "version": "2.4.0",
  3 + "version": "2.4.1",
4 4 "lockfileVersion": 1,
5 5 "requires": true,
6 6 "dependencies": {
... ...
... ... @@ -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
... ...
1 1 {
2 2 "name": "thingsboard",
3   - "version": "2.4.0",
  3 + "version": "2.4.1",
4 4 "lockfileVersion": 1,
5 5 "requires": true,
6 6 "dependencies": {
... ...
... ... @@ -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 +}
... ...
  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 20 @media (min-width: $layout-breakpoint-sm) {
21 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 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 }];
... ...