Move lastLoginTs and failedLoginAttempts from user's additionalInfo

This commit is contained in:
ViacheslavKlimov 2024-09-16 17:38:26 +03:00
parent a4370d9b87
commit c676ebb267
12 changed files with 98 additions and 46 deletions

View File

@ -0,0 +1,25 @@
--
-- Copyright © 2016-2024 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS last_login_ts BIGINT;
UPDATE user_credentials c SET last_login_ts = (SELECT (additional_info::json ->> 'lastLoginTs')::bigint FROM tb_user u WHERE u.id = c.user_id)
WHERE last_login_ts IS NULL;
ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS failed_login_attempts INT;
UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info::json ->> 'failedLoginAttempts')::int FROM tb_user u WHERE u.id = c.user_id)
WHERE failed_login_attempts IS NULL;
UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts')::text WHERE additional_info IS NOT NULL AND additional_info != 'null';

View File

@ -109,6 +109,8 @@ import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARA
import static org.thingsboard.server.controller.ControllerConstants.USER_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.USER_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.dao.entity.BaseEntityService.NULL_CUSTOMER_ID; import static org.thingsboard.server.dao.entity.BaseEntityService.NULL_CUSTOMER_ID;
import static org.thingsboard.server.dao.user.UserServiceImpl.LAST_LOGIN_TS;
import static org.thingsboard.server.dao.user.UserServiceImpl.USER_CREDENTIALS_ENABLED;
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@ -151,9 +153,10 @@ public class UserController extends BaseController {
processDashboardIdFromAdditionalInfo(additionalInfo, DEFAULT_DASHBOARD); processDashboardIdFromAdditionalInfo(additionalInfo, DEFAULT_DASHBOARD);
processDashboardIdFromAdditionalInfo(additionalInfo, HOME_DASHBOARD); processDashboardIdFromAdditionalInfo(additionalInfo, HOME_DASHBOARD);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) { if (userCredentials.isEnabled() && !additionalInfo.has(USER_CREDENTIALS_ENABLED)) {
additionalInfo.put("userCredentialsEnabled", true); additionalInfo.put(USER_CREDENTIALS_ENABLED, true);
} }
additionalInfo.put(LAST_LOGIN_TS, userCredentials.getLastLoginTs());
} }
return user; return user;
} }

View File

@ -50,7 +50,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserServiceImpl;
/** /**
* This event listener does not support async event processing because relay on ThreadLocal * This event listener does not support async event processing because relay on ThreadLocal
@ -231,8 +230,6 @@ public class EdgeEventSourcingListener {
user.setAdditionalInfo(null); user.setAdditionalInfo(null);
} }
if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) { if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) {
additionalInfo.remove(UserServiceImpl.FAILED_LOGIN_ATTEMPTS);
additionalInfo.remove(UserServiceImpl.LAST_LOGIN_TS);
if (additionalInfo.isEmpty()) { if (additionalInfo.isEmpty()) {
user.setAdditionalInfo(null); user.setAdditionalInfo(null);
} else { } else {

View File

@ -265,7 +265,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
} }
} }
if (actionType == ActionType.LOGIN && e == null) { if (actionType == ActionType.LOGIN && e == null) {
userService.setLastLoginTs(user.getTenantId(), user.getId()); userService.updateLastLoginTs(user.getTenantId(), user.getId());
} }
auditLogService.logEntityAction( auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(), user.getTenantId(), user.getCustomerId(), user.getId(),

View File

@ -97,7 +97,7 @@ public interface UserService extends EntityDaoService {
int increaseFailedLoginAttempts(TenantId tenantId, UserId userId); int increaseFailedLoginAttempts(TenantId tenantId, UserId userId);
void setLastLoginTs(TenantId tenantId, UserId userId); void updateLastLoginTs(TenantId tenantId, UserId userId);
void saveMobileSession(TenantId tenantId, UserId userId, String mobileToken, MobileSessionInfo sessionInfo); void saveMobileSession(TenantId tenantId, UserId userId, String mobileToken, MobileSessionInfo sessionInfo);

View File

@ -40,6 +40,8 @@ public class UserCredentials extends BaseDataWithAdditionalInfo<UserCredentialsI
private Long activateTokenExpTime; private Long activateTokenExpTime;
private String resetToken; private String resetToken;
private Long resetTokenExpTime; private Long resetTokenExpTime;
private Long lastLoginTs;
private Integer failedLoginAttempts;
public UserCredentials() { public UserCredentials() {
super(); super();

View File

@ -82,7 +82,8 @@ public class ModelConstants {
public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_EXP_TIME_PROPERTY = "activate_token_exp_time"; public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_EXP_TIME_PROPERTY = "activate_token_exp_time";
public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token"; public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token";
public static final String USER_CREDENTIALS_RESET_TOKEN_EXP_TIME_PROPERTY = "reset_token_exp_time"; public static final String USER_CREDENTIALS_RESET_TOKEN_EXP_TIME_PROPERTY = "reset_token_exp_time";
public static final String USER_CREDENTIALS_ADDITIONAL_PROPERTY = "additional_info"; public static final String USER_CREDENTIALS_LAST_LOGIN_TS_PROPERTY = "last_login_ts";
public static final String USER_CREDENTIALS_FAILED_LOGIN_ATTEMPTS_PROPERTY = "failed_login_attempts";
/** /**
* User settings constants. * User settings constants.

View File

@ -60,18 +60,21 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
private Long resetTokenExpTime; private Long resetTokenExpTime;
@Convert(converter = JsonConverter.class) @Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.USER_CREDENTIALS_ADDITIONAL_PROPERTY) @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo; private JsonNode additionalInfo;
@Column(name = ModelConstants.USER_CREDENTIALS_LAST_LOGIN_TS_PROPERTY)
private Long lastLoginTs;
@Column(name = ModelConstants.USER_CREDENTIALS_FAILED_LOGIN_ATTEMPTS_PROPERTY)
private Integer failedLoginAttempts;
public UserCredentialsEntity() { public UserCredentialsEntity() {
super(); super();
} }
public UserCredentialsEntity(UserCredentials userCredentials) { public UserCredentialsEntity(UserCredentials userCredentials) {
if (userCredentials.getId() != null) { super(userCredentials);
this.setUuid(userCredentials.getId().getId());
}
this.setCreatedTime(userCredentials.getCreatedTime());
if (userCredentials.getUserId() != null) { if (userCredentials.getUserId() != null) {
this.userId = userCredentials.getUserId().getId(); this.userId = userCredentials.getUserId().getId();
} }
@ -82,6 +85,8 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
this.resetToken = userCredentials.getResetToken(); this.resetToken = userCredentials.getResetToken();
this.resetTokenExpTime = userCredentials.getResetTokenExpTime(); this.resetTokenExpTime = userCredentials.getResetTokenExpTime();
this.additionalInfo = userCredentials.getAdditionalInfo(); this.additionalInfo = userCredentials.getAdditionalInfo();
this.lastLoginTs = userCredentials.getLastLoginTs();
this.failedLoginAttempts = userCredentials.getFailedLoginAttempts();
} }
@Override @Override
@ -98,6 +103,8 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
userCredentials.setResetToken(resetToken); userCredentials.setResetToken(resetToken);
userCredentials.setResetTokenExpTime(resetTokenExpTime); userCredentials.setResetTokenExpTime(resetTokenExpTime);
userCredentials.setAdditionalInfo(additionalInfo); userCredentials.setAdditionalInfo(additionalInfo);
userCredentials.setLastLoginTs(lastLoginTs);
userCredentials.setFailedLoginAttempts(failedLoginAttempts);
return userCredentials; return userCredentials;
} }

View File

@ -69,4 +69,19 @@ public class JpaUserCredentialsDao extends JpaAbstractDao<UserCredentialsEntity,
userCredentialsRepository.removeByUserId(userId.getId()); userCredentialsRepository.removeByUserId(userId.getId());
} }
@Override
public void setLastLoginTs(TenantId tenantId, UserId userId, long lastLoginTs) {
userCredentialsRepository.updateLastLoginTsByUserId(userId.getId(), lastLoginTs);
}
@Override
public int incrementFailedLoginAttempts(TenantId tenantId, UserId userId) {
return userCredentialsRepository.incrementFailedLoginAttemptsByUserId(userId.getId());
}
@Override
public void setFailedLoginAttempts(TenantId tenantId, UserId userId, int failedLoginAttempts) {
userCredentialsRepository.updateFailedLoginAttemptsByUserId(userId.getId(), failedLoginAttempts);
}
} }

View File

@ -16,6 +16,8 @@
package org.thingsboard.server.dao.sql.user; package org.thingsboard.server.dao.sql.user;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.UserCredentialsEntity; import org.thingsboard.server.dao.model.sql.UserCredentialsEntity;
@ -35,4 +37,20 @@ public interface UserCredentialsRepository extends JpaRepository<UserCredentials
@Transactional @Transactional
void removeByUserId(UUID userId); void removeByUserId(UUID userId);
@Transactional
@Modifying
@Query("UPDATE UserCredentialsEntity SET lastLoginTs = :lastLoginTs WHERE userId = :userId")
void updateLastLoginTsByUserId(UUID userId, long lastLoginTs);
@Transactional
@Modifying
@Query(value = "UPDATE user_credentials SET failed_login_attempts = coalesce(failed_login_attempts, 0) + 1 " +
"WHERE user_id = :userId RETURNING failed_login_attempts", nativeQuery = true)
int incrementFailedLoginAttemptsByUserId(UUID userId);
@Transactional
@Modifying
@Query("UPDATE UserCredentialsEntity SET failedLoginAttempts = :failedLoginAttempts WHERE userId = :userId")
void updateFailedLoginAttemptsByUserId(UUID userId, int failedLoginAttempts);
} }

View File

@ -61,4 +61,10 @@ public interface UserCredentialsDao extends Dao<UserCredentials> {
void removeByUserId(TenantId tenantId, UserId userId); void removeByUserId(TenantId tenantId, UserId userId);
void setLastLoginTs(TenantId tenantId, UserId userId, long lastLoginTs);
int incrementFailedLoginAttempts(TenantId tenantId, UserId userId);
void setFailedLoginAttempts(TenantId tenantId, UserId userId, int failedLoginAttempts);
} }

View File

@ -18,8 +18,6 @@ package org.thingsboard.server.dao.user;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -86,16 +84,13 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, User, UserCacheEvictEvent> implements UserService { public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, User, UserCacheEvictEvent> implements UserService {
public static final String USER_PASSWORD_HISTORY = "userPasswordHistory"; public static final String USER_PASSWORD_HISTORY = "userPasswordHistory";
public static final String USER_CREDENTIALS_ENABLED = "userCredentialsEnabled";
public static final String LAST_LOGIN_TS = "lastLoginTs"; public static final String LAST_LOGIN_TS = "lastLoginTs";
public static final String FAILED_LOGIN_ATTEMPTS = "failedLoginAttempts";
private static final int DEFAULT_TOKEN_LENGTH = 30; private static final int DEFAULT_TOKEN_LENGTH = 30;
public static final String INCORRECT_USER_ID = "Incorrect userId "; public static final String INCORRECT_USER_ID = "Incorrect userId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final String USER_CREDENTIALS_ENABLED = "userCredentialsEnabled";
@Value("${security.user_login_case_sensitive:true}") @Value("${security.user_login_case_sensitive:true}")
private boolean userLoginCaseSensitive; private boolean userLoginCaseSensitive;
@ -428,6 +423,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
customerUsersRemover.removeEntities(tenantId, customerId); customerUsersRemover.removeEntities(tenantId, customerId);
} }
@Transactional
@Override @Override
public void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled) { public void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled) {
log.trace("Executing setUserCredentialsEnabled [{}], [{}]", userId, enabled); log.trace("Executing setUserCredentialsEnabled [{}], [{}]", userId, enabled);
@ -438,30 +434,22 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
User user = findUserById(tenantId, userId); User user = findUserById(tenantId, userId);
user.setAdditionalInfoField(USER_CREDENTIALS_ENABLED, BooleanNode.valueOf(enabled)); user.setAdditionalInfoField(USER_CREDENTIALS_ENABLED, BooleanNode.valueOf(enabled));
if (enabled) {
resetFailedLoginAttempts(user);
}
saveUser(tenantId, user); saveUser(tenantId, user);
if (enabled) {
resetFailedLoginAttempts(tenantId, userId);
}
} }
@Override @Override
public void resetFailedLoginAttempts(TenantId tenantId, UserId userId) { public void resetFailedLoginAttempts(TenantId tenantId, UserId userId) {
log.trace("Executing onUserLoginSuccessful [{}]", userId); log.trace("Executing resetFailedLoginAttempts [{}]", userId);
User user = findUserById(tenantId, userId); userCredentialsDao.setFailedLoginAttempts(tenantId, userId, 0);
resetFailedLoginAttempts(user);
saveUser(tenantId, user);
}
private void resetFailedLoginAttempts(User user) {
user.setAdditionalInfoField(FAILED_LOGIN_ATTEMPTS, IntNode.valueOf(0));
} }
@Override @Override
public void setLastLoginTs(TenantId tenantId, UserId userId) { public void updateLastLoginTs(TenantId tenantId, UserId userId) {
User user = findUserById(tenantId, userId); userCredentialsDao.setLastLoginTs(tenantId, userId, System.currentTimeMillis());
user.setAdditionalInfoField(LAST_LOGIN_TS, new LongNode(System.currentTimeMillis()));
saveUser(tenantId, user);
} }
@Override @Override
@ -502,18 +490,8 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override @Override
public int increaseFailedLoginAttempts(TenantId tenantId, UserId userId) { public int increaseFailedLoginAttempts(TenantId tenantId, UserId userId) {
log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId); log.trace("Executing increaseFailedLoginAttempts [{}]", userId);
User user = findUserById(tenantId, userId); return userCredentialsDao.incrementFailedLoginAttempts(tenantId, userId);
int failedLoginAttempts = increaseFailedLoginAttempts(user);
saveUser(tenantId, user);
return failedLoginAttempts;
}
private int increaseFailedLoginAttempts(User user) {
int failedLoginAttempts = user.getAdditionalInfoField(FAILED_LOGIN_ATTEMPTS, JsonNode::asInt, 0);
failedLoginAttempts++;
user.setAdditionalInfoField(FAILED_LOGIN_ATTEMPTS, new IntNode(failedLoginAttempts));
return failedLoginAttempts;
} }
private void updatePasswordHistory(UserCredentials userCredentials) { private void updatePasswordHistory(UserCredentials userCredentials) {