diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index d3c26e2640..d344a9d92d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -41,6 +41,7 @@ import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldException; import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.cache.limits.RateLimitService; @@ -97,8 +98,8 @@ import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleNodeStateService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -824,6 +825,18 @@ public class ActorSystemContext { Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); } + public void persistCalculatedFieldDebugError(CalculatedFieldException cfe) { + String message; + if (cfe.getErrorMessage() != null) { + message = cfe.getErrorMessage(); + } else if (cfe.getCause() != null) { + message = cfe.getCause().getMessage(); + } else { + message = "N/A"; + } + persistCalculatedFieldDebugEvent(cfe.getCtx().getTenantId(), cfe.getCtx().getCfId(), cfe.getEventEntity(), cfe.getArguments(), cfe.getMsgId(), cfe.getMsgType(), null, message); + } + public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map arguments, UUID tbMsgId, TbMsgType tbMsgType, String result, String errorMessage) { if (checkLimits(tenantId)) { try { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/AbstractCalculatedFieldActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/AbstractCalculatedFieldActor.java index 9f82b536fd..29c7fe6587 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/AbstractCalculatedFieldActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/AbstractCalculatedFieldActor.java @@ -41,15 +41,7 @@ public abstract class AbstractCalculatedFieldActor extends ContextAwareActor { return doProcessCfMsg(cfm); } catch (CalculatedFieldException cfe) { if (DebugModeUtil.isDebugFailuresAvailable(cfe.getCtx().getCalculatedField())) { - String message; - if (cfe.getErrorMessage() != null) { - message = cfe.getErrorMessage(); - } else if (cfe.getCause() != null) { - message = cfe.getCause().getMessage(); - } else { - message = "N/A"; - } - systemContext.persistCalculatedFieldDebugEvent(tenantId, cfe.getCtx().getCfId(), cfe.getEventEntity(), cfe.getArguments(), cfe.getMsgId(), cfe.getMsgType(), null, message); + systemContext.persistCalculatedFieldDebugError(cfe); } cause = cfe.getCause(); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 752e8a1c11..4f59628656 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -66,7 +66,6 @@ import java.util.stream.Collectors; import static org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry.getValueForTsRecord; - /** * @author Andrew Shvayka */ @@ -131,7 +130,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state.isSizeOk()) { processStateIfReady(ctx, Collections.singletonList(ctx.getCfId()), state, null, null, msg.getCallback()); } else { - throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); } } catch (Exception e) { if (e instanceof CalculatedFieldException cfe) { @@ -200,6 +199,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } } catch (Exception e) { + if (e instanceof CalculatedFieldException cfe) { + throw cfe; + } throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } } @@ -223,7 +225,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } catch (Exception e) { if (e instanceof CalculatedFieldException cfe) { - throw cfe; + if (DebugModeUtil.isDebugFailuresAvailable(cfe.getCtx().getCalculatedField())) { + systemContext.persistCalculatedFieldDebugError(cfe); + } + callback.onSuccess(); + return; } throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 5d0ff5c2ff..373e2f588f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -310,7 +310,7 @@ public class CalculatedFieldCtx { } public String getSizeExceedsLimitMessage() { - return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; + return "State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index e3defd5883..339767f32f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -80,11 +80,15 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @Override public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException { + if (!userCredentials.isEnabled()) { + throw new DisabledException("User is not active"); + } + if (!encoder.matches(password, userCredentials.getPassword())) { int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId()); SecuritySettings securitySettings = securitySettingsService.getSecuritySettings(); if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) { - if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { + if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts()) { lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); throw new LockedException("Authentication Failed. Username was locked due to security policy."); } @@ -92,10 +96,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService { throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); } - if (!userCredentials.isEnabled()) { - throw new DisabledException("User is not active"); - } - userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId()); SecuritySettings securitySettings = securitySettingsService.getSecuritySettings(); diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index a21b081086..7f905ec0ae 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -23,6 +23,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; @@ -36,7 +37,9 @@ import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedField import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -875,6 +878,96 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testCalculatedFieldsWhenOneIsInvalid() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + long now = System.currentTimeMillis(); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5}}", now - TimeUnit.MINUTES.toMillis(3)))); + + // Script CF - invalid + CalculatedField invalidCF = new CalculatedField(); + invalidCF.setEntityId(testDevice.getId()); + invalidCF.setType(CalculatedFieldType.SCRIPT); + invalidCF.setName("Script CF"); + invalidCF.setDebugSettings(DebugSettings.all()); + + ScriptCalculatedFieldConfiguration scriptConfig = new ScriptCalculatedFieldConfiguration(); + + ReferencedEntityKey refEntityKeyA = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null); + Argument argumentA = new Argument(); + argumentA.setRefEntityKey(refEntityKeyA); + scriptConfig.setArguments(Map.of("a", argumentA)); + scriptConfig.setExpression(""" + return { + "temperature": temp + }; + """); + + Output scriptOutput = new Output(); + scriptOutput.setType(OutputType.TIME_SERIES); + scriptConfig.setOutput(scriptOutput); + + invalidCF.setConfiguration(scriptConfig); + + invalidCF = doPost("/api/calculatedField", invalidCF, CalculatedField.class); + CalculatedFieldId invalidCfId = invalidCF.getId(); + + await().alias("create invalid CF -> check error").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + PageData debugEvents = getDebugEvents(tenantId, invalidCfId, 1); + if (!debugEvents.getData().isEmpty()) { + EventInfo eventInfo = debugEvents.getData().get(0); + assertThat(eventInfo.getBody().has("error")).isTrue(); + } + }); + + // Simple CF - valid + CalculatedField validCF = new CalculatedField(); + validCF.setEntityId(testDevice.getId()); + validCF.setType(CalculatedFieldType.SIMPLE); + validCF.setName("Simple CF"); + validCF.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration simpleConfig = new SimpleCalculatedFieldConfiguration(); + simpleConfig.setArguments(Map.of("a", argumentA)); + simpleConfig.setExpression("a+1"); + + Output simpleOutput = new Output(); + simpleOutput.setName("a+1"); + simpleOutput.setType(OutputType.TIME_SERIES); + simpleOutput.setDecimalsByDefault(0); + simpleConfig.setOutput(simpleOutput); + + validCF.setConfiguration(simpleConfig); + + validCF = doPost("/api/calculatedField", validCF, CalculatedField.class); + CalculatedFieldId validCfId = validCF.getId(); + + await().alias("create CF -> check initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + PageData debugEvents = getDebugEvents(tenantId, validCfId, 1); + if (!debugEvents.getData().isEmpty()) { + EventInfo eventInfo = debugEvents.getData().get(0); + assertThat(eventInfo.getBody().has("error")).isFalse(); + } + ObjectNode result = getLatestTelemetry(testDevice.getId(), "a+1"); + assertThat(result).isNotNull(); + assertThat(result.get("a+1").get(0).get("value").asText()).isEqualTo("6"); + }); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":6}")); + + await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(testDevice.getId(), "a+1"); + assertThat(result).isNotNull(); + assertThat(result.get("a+1").get(0).get("value").asText()).isEqualTo("7"); + }); + } + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 44c383a5f0..3b7286cf01 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -80,6 +80,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResourceInfo; @@ -99,6 +100,7 @@ import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; @@ -1298,4 +1300,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { doPost("/api/job/" + jobId + "/reprocess").andExpect(status().isOk()); } + protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { + return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE, limit); + } + + protected PageData getEvents(TenantId tenantId, EntityId entityId, EventType eventType, int limit) throws Exception { + TimePageLink pageLink = new TimePageLink(limit); + return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", + new TypeReference>() { + }, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityServiceTest.java new file mode 100644 index 0000000000..0933dfd942 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityServiceTest.java @@ -0,0 +1,204 @@ +/** + * Copyright © 2016-2026 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. + */ +package org.thingsboard.server.service.security.system; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.model.SecuritySettings; +import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; +import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.settings.SecuritySettingsService; +import org.thingsboard.server.dao.user.UserService; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultSystemSecurityServiceTest { + + @Mock + private AdminSettingsService adminSettingsService; + @Mock + private BCryptPasswordEncoder encoder; + @Mock + private UserService userService; + @Mock + private MailService mailService; + @Mock + private AuditLogService auditLogService; + @Mock + private SecuritySettingsService securitySettingsService; + + private DefaultSystemSecurityService systemSecurityService; + + private TenantId tenantId; + private UserId userId; + private UserCredentials userCredentials; + private SecuritySettings securitySettings; + private String username; + private String password; + private String encodedPassword; + + @Before + public void setUp() { + systemSecurityService = new DefaultSystemSecurityService(adminSettingsService, encoder, userService, mailService, auditLogService, securitySettingsService); + + tenantId = TenantId.fromUUID(UUID.randomUUID()); + userId = new UserId(UUID.randomUUID()); + username = "tenant@example.com"; + password = "correctPassword"; + encodedPassword = "$2a$10$encodedPasswordHash"; + + userCredentials = new UserCredentials(); + userCredentials.setUserId(userId); + userCredentials.setEnabled(true); + userCredentials.setPassword(encodedPassword); + userCredentials.setCreatedTime(System.currentTimeMillis()); + + securitySettings = new SecuritySettings(); + securitySettings.setMaxFailedLoginAttempts(5); + securitySettings.setPasswordPolicy(new UserPasswordPolicy()); + } + + @Test + public void testValidateUserCredentials_successfulLogin() { + when(encoder.matches(password, encodedPassword)).thenReturn(true); + when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); + + systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password); + + verify(encoder).matches(password, encodedPassword); + verify(userService).resetFailedLoginAttempts(tenantId, userId); + verify(userService, never()).increaseFailedLoginAttempts(any(), any()); + } + + @Test + public void testValidateUserCredentials_wrongPassword_incrementsFailedAttempts() { + when(encoder.matches(password, encodedPassword)).thenReturn(false); + when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(3); + when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); + + assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) + .isInstanceOf(BadCredentialsException.class) + .hasMessageContaining("Authentication Failed"); + + verify(userService).increaseFailedLoginAttempts(tenantId, userId); + verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false)); + } + + @Test + public void testValidateUserCredentials_wrongPassword_accountLocked() { + when(encoder.matches(password, encodedPassword)).thenReturn(false); + when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(6); + when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); + + assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) + .isInstanceOf(LockedException.class) + .hasMessageContaining("locked due to security policy"); + + verify(userService).increaseFailedLoginAttempts(tenantId, userId); + verify(userService).setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); + } + + @Test + public void testValidateUserCredentials_wrongPassword_exactlyAtThreshold_noLock() { + when(encoder.matches(password, encodedPassword)).thenReturn(false); + when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(5); + when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); + + assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) + .isInstanceOf(BadCredentialsException.class) + .hasMessageContaining("Authentication Failed"); + + verify(userService).increaseFailedLoginAttempts(tenantId, userId); + verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false)); + } + + @Test + public void testValidateUserCredentials_correctPassword_disabledUser_throwsDisabledException() { + userCredentials.setEnabled(false); + + assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) + .isInstanceOf(DisabledException.class) + .hasMessage("User is not active"); + + verify(encoder, never()).matches(any(), any()); + verify(userService, never()).increaseFailedLoginAttempts(any(), any()); + } + + @Test + public void testValidateUserCredentials_wrongPassword_maxAttemptsDisabled() { + securitySettings.setMaxFailedLoginAttempts(null); + when(encoder.matches(password, encodedPassword)).thenReturn(false); + when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(100); + when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); + + assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) + .isInstanceOf(BadCredentialsException.class); + + verify(userService).increaseFailedLoginAttempts(tenantId, userId); + verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false)); + } + + @Test + public void testValidateUserCredentials_wrongPassword_maxAttemptsSetToZero() { + securitySettings.setMaxFailedLoginAttempts(0); + when(encoder.matches(password, encodedPassword)).thenReturn(false); + when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(100); + when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); + + assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) + .isInstanceOf(BadCredentialsException.class); + + verify(userService).increaseFailedLoginAttempts(tenantId, userId); + verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false)); + } + + @Test + public void testValidateUserCredentials_wrongPassword_withNotificationEmail() throws ThingsboardException { + String notificationEmail = "admin@example.com"; + securitySettings.setUserLockoutNotificationEmail(notificationEmail); + when(encoder.matches(password, encodedPassword)).thenReturn(false); + when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(6); + when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); + + assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) + .isInstanceOf(LockedException.class); + + verify(userService).setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); + verify(mailService).sendAccountLockoutEmail(eq(username), eq(notificationEmail), eq(5)); + } + +} diff --git a/pom.xml b/pom.xml index 8f5708bcec..352122f057 100755 --- a/pom.xml +++ b/pom.xml @@ -172,16 +172,6 @@ true - - default-surefire-java-opts - - - !env.SUREFIRE_JAVA_OPTS - - - - - @@ -676,7 +666,6 @@ --add-opens=java.base/java.lang.reflect=ALL-UNNAMED -Dqueue.edqs.local.rocksdb_path="target/rocksdb/fork_${surefire.forkNumber}/edqs" -Dqueue.calculated_fields.rocks_db_path="target/rocksdb/fork_${surefire.forkNumber}/cf" - ${env.SUREFIRE_JAVA_OPTS}