Merge branch 'master' into lwm2m_write_obj_19

This commit is contained in:
nick 2024-10-18 14:34:57 +03:00
commit edad8f4932
24 changed files with 203 additions and 112 deletions

View File

@ -14,3 +14,13 @@
-- 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' - 'userCredentialsEnabled')::text
WHERE additional_info IS NOT NULL AND additional_info != 'null';

View File

@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@ -877,14 +878,18 @@ public abstract class BaseController {
}
protected void checkUserInfo(User user) throws ThingsboardException {
ObjectNode info;
if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) {
checkDashboardInfo(additionalInfo);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) {
additionalInfo.put("userCredentialsEnabled", true);
}
info = additionalInfo;
checkDashboardInfo(info);
} else {
info = JacksonUtil.newObjectNode();
user.setAdditionalInfo(info);
}
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
info.put("userCredentialsEnabled", userCredentials.isEnabled());
info.put("lastLoginTs", userCredentials.getLastLoginTs());
}
protected void checkDashboardInfo(JsonNode additionalInfo) throws ThingsboardException {

View File

@ -78,7 +78,6 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.util.ArrayList;
import java.util.Arrays;
@ -123,7 +122,6 @@ public class UserController extends BaseController {
private final MailService mailService;
private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService;
private final ApplicationEventPublisher eventPublisher;
private final TbUserService tbUserService;
private final EntityQueryService entityQueryService;

View File

@ -170,6 +170,7 @@ public class ThingsboardInstallService {
log.info("Installing DataBase schema for entities...");
entityDatabaseSchemaService.createDatabaseSchema();
entityDatabaseSchemaService.createSchemaVersion();
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);

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.SaveEntityEvent;
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
@ -226,13 +225,10 @@ public class EdgeEventSourcingListener {
}
private void cleanUpUserAdditionalInfo(User user) {
// reset FAILED_LOGIN_ATTEMPTS and LAST_LOGIN_TS - edge is not interested in this information
if (user.getAdditionalInfo() instanceof NullNode) {
user.setAdditionalInfo(null);
}
if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) {
additionalInfo.remove(UserServiceImpl.FAILED_LOGIN_ATTEMPTS);
additionalInfo.remove(UserServiceImpl.LAST_LOGIN_TS);
if (additionalInfo.isEmpty()) {
user.setAdditionalInfo(null);
} else {

View File

@ -23,4 +23,6 @@ public interface EntityDatabaseSchemaService extends DatabaseSchemaService {
void createCustomerTitleUniqueConstraintIfNotExists();
void createSchemaVersion();
}

View File

@ -16,7 +16,10 @@
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
@ -29,6 +32,11 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql";
public static final String SCHEMA_VIEWS_AND_FUNCTIONS_SQL = "schema-views-and-functions.sql";
@Autowired
private BuildProperties buildProperties;
@Autowired
private JdbcTemplate jdbcTemplate;
public SqlEntityDatabaseSchemaService() {
super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL);
}
@ -59,4 +67,26 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
"ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;",
"create 'customer_title_unq_key' constraint if it doesn't already exist!");
}
@Override
public void createSchemaVersion() {
try {
Long schemaVersion = jdbcTemplate.queryForList("SELECT schema_version FROM tb_schema_settings", Long.class).stream().findFirst().orElse(null);
if (schemaVersion == null) {
jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version) VALUES (" + getSchemaVersion() + ")");
}
} catch (Exception e) {
log.warn("Failed to create schema version [{}]!", buildProperties.getVersion(), e);
}
}
private int getSchemaVersion() {
String[] versionParts = buildProperties.getVersion().replaceAll("[^\\d.]", "").split("\\.");
int major = Integer.parseInt(versionParts[0]);
int minor = Integer.parseInt(versionParts[1]);
int patch = versionParts.length > 2 ? Integer.parseInt(versionParts[2]) : 0;
return major * 1000000 + minor * 1000 + patch;
}
}

View File

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.install;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.SqlTsDao;
@ -25,9 +24,6 @@ import org.thingsboard.server.dao.util.SqlTsDao;
@Profile("install")
public class SqlTsDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements TsDatabaseSchemaService {
@Value("${sql.postgres.ts_key_value_partitioning:MONTHS}")
private String partitionType;
public SqlTsDatabaseSchemaService() {
super("schema-ts-psql.sql", null);
}

View File

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

View File

@ -27,6 +27,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserActivationLink;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
@ -67,31 +68,30 @@ public class AuthControllerTest extends AbstractControllerTest {
.andExpect(status().isUnauthorized());
loginSysAdmin();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
User user = getCurrentUser();
assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN);
assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL);
loginTenantAdmin();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email", is(TENANT_ADMIN_EMAIL)));
user = getCurrentUser();
assertThat(user.getAuthority()).isEqualTo(Authority.TENANT_ADMIN);
assertThat(user.getEmail()).isEqualTo(TENANT_ADMIN_EMAIL);
loginCustomerUser();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.CUSTOMER_USER.name())))
.andExpect(jsonPath("$.email", is(CUSTOMER_USER_EMAIL)));
user = getCurrentUser();
assertThat(user.getAuthority()).isEqualTo(Authority.CUSTOMER_USER);
assertThat(user.getEmail()).isEqualTo(CUSTOMER_USER_EMAIL);
user = getUser(customerUserId);
assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isTrue();
assertThat(user.getAdditionalInfo().get("lastLoginTs").asLong()).isCloseTo(System.currentTimeMillis(), within(10000L));
}
@Test
public void testLoginLogout() throws Exception {
loginSysAdmin();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
User user = getCurrentUser();
assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN);
assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL);
TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed
@ -102,19 +102,45 @@ public class AuthControllerTest extends AbstractControllerTest {
resetTokens();
}
@Test
public void testFailedLogin() throws Exception {
int maxFailedLoginAttempts = 3;
loginSysAdmin();
updateSecuritySettings(securitySettings -> {
securitySettings.setMaxFailedLoginAttempts(maxFailedLoginAttempts);
});
loginTenantAdmin();
for (int i = 0; i < maxFailedLoginAttempts; i++) {
String error = getErrorMessage(doPost("/api/auth/login",
new LoginRequest(CUSTOMER_USER_EMAIL, "IncorrectPassword"))
.andExpect(status().isUnauthorized()));
assertThat(error).containsIgnoringCase("invalid username or password");
}
User user = getUser(customerUserId);
assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isTrue();
String error = getErrorMessage(doPost("/api/auth/login",
new LoginRequest(CUSTOMER_USER_EMAIL, "IncorrectPassword4"))
.andExpect(status().isUnauthorized()));
assertThat(error).containsIgnoringCase("account is locked");
user = getUser(customerUserId);
assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isFalse();
}
@Test
public void testRefreshToken() throws Exception {
loginSysAdmin();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
User user = getCurrentUser();
assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN);
assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL);
refreshToken();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
user = getCurrentUser();
assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN);
assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL);
}
@Test
@ -277,6 +303,14 @@ public class AuthControllerTest extends AbstractControllerTest {
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
}
private User getCurrentUser() throws Exception {
return doGet("/api/auth/user", User.class);
}
private User getUser(UserId id) throws Exception {
return doGet("/api/user/" + id, User.class);
}
private String getActivationLink(User user) throws Exception {
return doGet("/api/user/" + user.getId() + "/activationLink", String.class);
}

View File

@ -319,7 +319,9 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS);
assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username);
});
assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo()
loginTenantAdmin();
assertThat(doGet("/api/user/" + user.getId(), User.class).getAdditionalInfo()
.get("lastLoginTs").asLong())
.isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3));
}

View File

@ -113,6 +113,7 @@ public class UserControllerTest extends AbstractControllerTest {
Assert.assertEquals(email, savedUser.getEmail());
User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class);
foundUser.setAdditionalInfo(savedUser.getAdditionalInfo());
Assert.assertEquals(foundUser, savedUser);
testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundUser, foundUser,
@ -265,6 +266,7 @@ public class UserControllerTest extends AbstractControllerTest {
User savedUser = doPost("/api/user", user, User.class);
User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class);
Assert.assertNotNull(foundUser);
foundUser.setAdditionalInfo(savedUser.getAdditionalInfo());
Assert.assertEquals(savedUser, foundUser);
}

View File

@ -47,7 +47,7 @@ public class UserEdgeTest extends AbstractEdgeTest {
@Test
public void testCreateUpdateDeleteTenantUser() throws Exception {
// create user
edgeImitator.expectMessageAmount(6);
edgeImitator.expectMessageAmount(3);
User newTenantAdmin = new User();
newTenantAdmin.setAuthority(Authority.TENANT_ADMIN);
newTenantAdmin.setTenantId(tenantId);
@ -55,9 +55,9 @@ public class UserEdgeTest extends AbstractEdgeTest {
newTenantAdmin.setFirstName("Boris");
newTenantAdmin.setLastName("Johnson");
User savedTenantAdmin = createUser(newTenantAdmin, "tenant");
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user)
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent());
UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get();
@ -133,7 +133,7 @@ public class UserEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(edgeImitator.waitForMessages());
// create user
edgeImitator.expectMessageAmount(6);
edgeImitator.expectMessageAmount(3);
User customerUser = new User();
customerUser.setAuthority(Authority.CUSTOMER_USER);
customerUser.setTenantId(tenantId);
@ -142,9 +142,9 @@ public class UserEdgeTest extends AbstractEdgeTest {
customerUser.setFirstName("John");
customerUser.setLastName("Edwards");
User savedCustomerUser = createUser(customerUser, "customer");
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user)
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent());
UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get();

View File

@ -114,6 +114,13 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, false, protobuf);
CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap);
String awaitAlias = "await Two Way Rpc (client.getObserveRelation)";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> processTwoWayRpcTestWithAwait(callbackCoap, observeRelation, expectedResponseResult));
}
private boolean processTwoWayRpcTestWithAwait(CoapTestCallback callbackCoap, CoapObserveRelation observeRelation, String expectedResponseResult) throws Exception {
String awaitAlias = "await Two Way Rpc (client.getObserveRelation)";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -146,7 +153,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
validateTwoWayStateChangedNotification(callbackCoap, expectedResponseResult, actualResult);
observeRelation.proactiveCancel();
assertTrue(observeRelation.isCanceled());
return observeRelation.isCanceled();
}
protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) {

View File

@ -97,7 +97,7 @@ public interface UserService extends EntityDaoService {
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);

View File

@ -40,6 +40,8 @@ public class UserCredentials extends BaseDataWithAdditionalInfo<UserCredentialsI
private Long activateTokenExpTime;
private String resetToken;
private Long resetTokenExpTime;
private Long lastLoginTs;
private Integer failedLoginAttempts;
public UserCredentials() {
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_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_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.

View File

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

View File

@ -69,4 +69,19 @@ public class JpaUserCredentialsDao extends JpaAbstractDao<UserCredentialsEntity,
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;
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.thingsboard.server.dao.model.sql.UserCredentialsEntity;
@ -35,4 +37,19 @@ public interface UserCredentialsRepository extends JpaRepository<UserCredentials
@Transactional
void removeByUserId(UUID userId);
@Transactional
@Modifying
@Query("UPDATE UserCredentialsEntity SET lastLoginTs = :lastLoginTs WHERE userId = :userId")
void updateLastLoginTsByUserId(UUID userId, long lastLoginTs);
@Transactional
@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 setLastLoginTs(TenantId tenantId, UserId userId, long lastLoginTs);
int incrementFailedLoginAttempts(TenantId tenantId, UserId userId);
void setFailedLoginAttempts(TenantId tenantId, UserId userId, int failedLoginAttempts);
}

View File

@ -17,9 +17,6 @@ package org.thingsboard.server.dao.user;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
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.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
@ -87,15 +84,10 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
public static final String USER_PASSWORD_HISTORY = "userPasswordHistory";
public static final String LAST_LOGIN_TS = "lastLoginTs";
public static final String FAILED_LOGIN_ATTEMPTS = "failedLoginAttempts";
private static final int DEFAULT_TOKEN_LENGTH = 30;
public static final String INCORRECT_USER_ID = "Incorrect userId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final String USER_CREDENTIALS_ENABLED = "userCredentialsEnabled";
@Value("${security.user_login_case_sensitive:true}")
private boolean userLoginCaseSensitive;
@ -428,40 +420,28 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
customerUsersRemover.removeEntities(tenantId, customerId);
}
@Transactional
@Override
public void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled) {
log.trace("Executing setUserCredentialsEnabled [{}], [{}]", userId, enabled);
validateId(userId, id -> INCORRECT_USER_ID + id);
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId());
userCredentials.setEnabled(enabled);
saveUserCredentials(tenantId, userCredentials);
User user = findUserById(tenantId, userId);
user.setAdditionalInfoField(USER_CREDENTIALS_ENABLED, BooleanNode.valueOf(enabled));
if (enabled) {
resetFailedLoginAttempts(user);
userCredentials.setFailedLoginAttempts(0);
}
saveUser(tenantId, user);
saveUserCredentials(tenantId, userCredentials);
}
@Override
public void resetFailedLoginAttempts(TenantId tenantId, UserId userId) {
log.trace("Executing onUserLoginSuccessful [{}]", userId);
User user = findUserById(tenantId, userId);
resetFailedLoginAttempts(user);
saveUser(tenantId, user);
}
private void resetFailedLoginAttempts(User user) {
user.setAdditionalInfoField(FAILED_LOGIN_ATTEMPTS, IntNode.valueOf(0));
log.trace("Executing resetFailedLoginAttempts [{}]", userId);
userCredentialsDao.setFailedLoginAttempts(tenantId, userId, 0);
}
@Override
public void setLastLoginTs(TenantId tenantId, UserId userId) {
User user = findUserById(tenantId, userId);
user.setAdditionalInfoField(LAST_LOGIN_TS, new LongNode(System.currentTimeMillis()));
saveUser(tenantId, user);
public void updateLastLoginTs(TenantId tenantId, UserId userId) {
userCredentialsDao.setLastLoginTs(tenantId, userId, System.currentTimeMillis());
}
@Override
@ -502,18 +482,8 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override
public int increaseFailedLoginAttempts(TenantId tenantId, UserId userId) {
log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId);
User user = findUserById(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;
log.trace("Executing increaseFailedLoginAttempts [{}]", userId);
return userCredentialsDao.incrementFailedLoginAttempts(tenantId, userId);
}
private void updatePasswordHistory(UserCredentials userCredentials) {

View File

@ -20,18 +20,6 @@ CREATE TABLE IF NOT EXISTS tb_schema_settings
CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version)
);
CREATE OR REPLACE PROCEDURE insert_tb_schema_settings()
LANGUAGE plpgsql AS
$$
BEGIN
IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN
INSERT INTO tb_schema_settings (schema_version) VALUES (3006004);
END IF;
END;
$$;
call insert_tb_schema_settings();
CREATE TABLE IF NOT EXISTS admin_settings (
id uuid NOT NULL CONSTRAINT admin_settings_pkey PRIMARY KEY,
tenant_id uuid NOT NULL,
@ -497,7 +485,9 @@ CREATE TABLE IF NOT EXISTS user_credentials (
reset_token varchar(255) UNIQUE,
reset_token_exp_time BIGINT,
user_id uuid UNIQUE,
additional_info varchar DEFAULT '{}'
additional_info varchar DEFAULT '{}',
last_login_ts BIGINT,
failed_login_attempts INT
);
CREATE TABLE IF NOT EXISTS widget_type (

View File

@ -220,8 +220,8 @@ export interface ScadaSymbolMetadata {
export const emptyMetadata = (width?: number, height?: number): ScadaSymbolMetadata => ({
title: '',
widgetSizeX: width ? width/100 : 3,
widgetSizeY: height ? height/100 : 3,
widgetSizeX: width ? Math.max(Math.round(width/100), 1) : 3,
widgetSizeY: height ? Math.max(Math.round(height/100), 1) : 3,
tags: [],
behavior: [],
properties: []