Browse Source

Merge pull request #15110 from thingsboard/rc

Merge rc to master
pull/15236/head
Viacheslav Klimov 3 months ago
committed by GitHub
parent
commit
da24f4bc9f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  3. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java
  4. 6
      application/src/main/resources/thingsboard.yml
  5. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  6. 7
      common/util/src/main/java/org/thingsboard/common/util/DirectListeningExecutor.java
  7. 247
      common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java
  8. 349
      common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java
  9. 16
      common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java
  10. 27
      dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java
  11. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
  12. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  13. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  14. 7
      dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java
  15. 11
      pom.xml
  16. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java
  17. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java
  18. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java
  19. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  20. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java
  21. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java
  22. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java
  23. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeTest.java
  24. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeTest.java
  25. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeTest.java
  26. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java
  27. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java
  28. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java
  29. 106
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeTest.java
  30. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeTest.java
  31. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java
  32. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNodeTest.java
  33. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeTest.java
  34. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java
  35. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
  36. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java
  37. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java
  38. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java
  39. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java
  40. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java
  41. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java
  42. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeTest.java
  43. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
  44. 13
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java
  45. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java
  46. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java
  47. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java
  48. 2
      ui-ngx/package.json
  49. 1
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  50. 4
      ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts
  51. 6
      ui-ngx/src/app/shared/components/toast.directive.ts
  52. 12748
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  53. 20
      ui-ngx/yarn.lock

10
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -31,6 +31,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService;
@ -144,6 +145,7 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -614,9 +616,17 @@ public class ActorSystemContext {
@Getter
private boolean localCacheType;
@Value("${actors.rule.external.ssrf_protection_enabled:false}")
private boolean ssrfProtectionEnabled;
@Value("${actors.rule.external.ssrf_additional_blocked_hosts:}")
private List<String> ssrfAdditionalBlockedHosts;
@PostConstruct
public void init() {
this.localCacheType = "caffeine".equals(cacheType);
SsrfProtectionValidator.setEnabled(ssrfProtectionEnabled);
SsrfProtectionValidator.setAdditionalBlockedHosts(ssrfAdditionalBlockedHosts);
}
@Value("${actors.tenant.create_components_on_init:true}")

4
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -137,7 +137,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
defaultCtx.tellFailure(msg.getMsg(), e);
}
} else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more than " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
}
}
@ -160,7 +160,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
msg.getCtx().tellFailure(msg.getMsg(), e);
}
} else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more than " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
}
}
}

10
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java

@ -79,9 +79,13 @@ public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implemen
}
}
if (user.has(EMAIL)) {
JsonNode email = user.get(EMAIL);
if (email != null && email.isTextual()) {
updated.put(EMAIL, email.asText());
JsonNode emailNode = user.get(EMAIL);
if (emailNode != null && emailNode.isTextual()) {
Object tokenEmail = attributes.get(EMAIL);
if (tokenEmail != null && !emailNode.asText().equals(tokenEmail.toString())) {
log.warn("Apple OAuth2 callback: ignoring email [{}] from user POST parameter " +
"that differs from validated ID token email [{}]", emailNode.asText(), tokenEmail);
}
}
}
}

6
application/src/main/resources/thingsboard.yml

@ -511,6 +511,12 @@ actors:
# Force acknowledgment of the incoming message for external rule nodes to decrease processing latency.
# Enqueue the result of external node processing as a separate message to the rule engine.
force_ack: "${ACTORS_RULE_EXTERNAL_NODE_FORCE_ACK:false}"
# Enable Server-Side Request Forgery (SSRF) protection for external HTTP rule nodes (rest api call).
# When enabled, requests to private/internal network addresses are blocked.
ssrf_protection_enabled: "${SSRF_PROTECTION_ENABLED:false}"
# Comma-separated list of additional blocked destinations (IPs, CIDR subnets, or hostnames).
# Example: "198.51.100.0/24,metadata.tencentyun.com,rancher-metadata"
ssrf_additional_blocked_hosts: "${SSRF_ADDITIONAL_BLOCKED_HOSTS:}"
rpc:
# Maximum number of persistent RPC call retries in case of failed request delivery.
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"

5
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -107,11 +107,10 @@ public final class TbMsg implements Serializable {
.build();
}
public TbMsg copyWithNewCtx() {
public TbMsgBuilder copyWithNewCtx() {
return copy()
.ctx(ctx.copy())
.callback(TbMsgCallback.EMPTY)
.build();
.callback(TbMsgCallback.EMPTY);
}
private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,

7
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java → common/util/src/main/java/org/thingsboard/common/util/DirectListeningExecutor.java

@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine;
package org.thingsboard.common.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.common.util.ListeningExecutor;
import java.util.concurrent.Callable;
public class TestDbCallbackExecutor implements ListeningExecutor {
public enum DirectListeningExecutor implements ListeningExecutor {
INSTANCE;
@Override
public <T> ListenableFuture<T> executeAsync(Callable<T> task) {

247
common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java

@ -0,0 +1,247 @@
/**
* 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.common.util;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SsrfProtectionValidator {
private static volatile boolean enabled;
private static final Set<String> ALLOWED_SCHEMES = Set.of("http", "https");
private static final Set<String> BLOCKED_HOSTNAMES = Set.of("localhost");
private static final Set<String> BLOCKED_HOSTNAME_SUFFIXES = Set.of(".internal", ".local");
private static volatile AdditionalBlockedHosts additionalBlocked = AdditionalBlockedHosts.EMPTY;
// Well-known cloud metadata endpoints not covered by the JDK checks (isLoopback, isSiteLocal, isLinkLocal)
private static final List<CidrRange> CLOUD_METADATA_RANGES = List.of(
CidrRange.of("100.64.0.0", 10), // Carrier-Grade NAT (RFC 6598); Alibaba Cloud and Tencent Cloud metadata
CidrRange.of("192.0.0.0", 24), // IANA reserved (RFC 6890); Oracle Cloud alternate metadata endpoint
CidrRange.of("168.63.129.16", 32) // Azure WireServer
);
public static void validateUri(URI uri) {
validateUri(uri, enabled);
}
static void validateUri(URI uri, boolean ssrfProtectionEnabled) {
if (!ssrfProtectionEnabled) {
return;
}
String scheme = uri.getScheme();
if (scheme == null || !ALLOWED_SCHEMES.contains(scheme.toLowerCase())) {
throw new RuntimeException("URI is invalid: only HTTP and HTTPS schemes are allowed, got: " + scheme);
}
String host = uri.getHost();
if (host == null || host.isEmpty()) {
throw new RuntimeException("URI is invalid: hostname is missing");
}
String hostLower = host.toLowerCase();
if (BLOCKED_HOSTNAMES.contains(hostLower) || additionalBlocked.hostnames.contains(hostLower)) {
throwBlockedHost(host);
}
for (String suffix : BLOCKED_HOSTNAME_SUFFIXES) {
if (hostLower.endsWith(suffix)) {
throwBlockedHost(host);
}
}
// Block IPv6 loopback literal in URL (e.g. http://[::1]/)
if ("[::1]".equals(host) || "::1".equals(host)) {
throwBlockedHost(host);
}
validateResolvedAddresses(host);
}
private static void validateResolvedAddresses(String host) {
InetAddress[] addresses;
try {
addresses = InetAddress.getAllByName(host);
} catch (UnknownHostException e) {
throw new RuntimeException("URI is invalid: unable to resolve hostname '" + host + "'", e);
}
for (InetAddress address : addresses) {
if (isBlockedAddress(address)) {
log.debug("Blocked request to host '{}' resolved to '{}'", host, address.getHostAddress());
throwBlockedHost(host);
}
}
}
private static boolean isBlockedAddress(InetAddress address) {
// Covers 127.0.0.0/8 and ::1
if (address.isLoopbackAddress()) {
return true;
}
// Covers 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
if (address.isSiteLocalAddress()) {
return true;
}
// Covers 169.254.0.0/16 and fe80::/10
if (address.isLinkLocalAddress()) {
return true;
}
// Covers 0.0.0.0
if (address.isAnyLocalAddress()) {
return true;
}
// Additional check for IPv6 unique local addresses (fc00::/7)
byte[] addr = address.getAddress();
if (addr.length == 16) {
int firstByte = addr[0] & 0xFF;
// fc00::/7 means first 7 bits are 1111110, so first byte is 0xFC or 0xFD
if (firstByte == 0xFC || firstByte == 0xFD) {
return true;
}
}
for (CidrRange cidr : CLOUD_METADATA_RANGES) {
if (cidr.contains(address)) {
return true;
}
}
// Check additional configured CIDR ranges
for (CidrRange cidr : additionalBlocked.cidrRanges) {
if (cidr.contains(address)) {
return true;
}
}
return false;
}
private static void throwBlockedHost(String host) {
throw new RuntimeException("URI is invalid: host '" + host + "' is not allowed");
}
public static void setEnabled(boolean enabled) {
SsrfProtectionValidator.enabled = enabled;
}
public static void setAdditionalBlockedHosts(List<String> entries) {
if (entries == null || entries.isEmpty()) {
additionalBlocked = AdditionalBlockedHosts.EMPTY;
return;
}
List<CidrRange> cidrRanges = new ArrayList<>();
Set<String> hostnames = new HashSet<>();
for (String entry : entries) {
String trimmed = entry.trim();
if (trimmed.isEmpty()) {
continue;
}
if (trimmed.contains("/") || isIpLiteral(trimmed)) {
try {
cidrRanges.add(CidrRange.parse(trimmed));
} catch (Exception e) {
log.warn("Failed to parse CIDR/IP entry '{}': {}", trimmed, e.getMessage());
}
} else {
hostnames.add(trimmed.toLowerCase());
}
}
additionalBlocked = new AdditionalBlockedHosts(
Collections.unmodifiableList(cidrRanges),
Collections.unmodifiableSet(hostnames));
log.info("SSRF additional blocked hosts configured: {} CIDR range(s), {} hostname(s)", cidrRanges.size(), hostnames.size());
}
private static boolean isIpLiteral(String entry) {
// IPv4 starts with a digit, IPv6 contains ':'
return !entry.isEmpty() && (Character.isDigit(entry.charAt(0)) || entry.contains(":"));
}
record AdditionalBlockedHosts(List<CidrRange> cidrRanges, Set<String> hostnames) {
static final AdditionalBlockedHosts EMPTY = new AdditionalBlockedHosts(Collections.emptyList(), Collections.emptySet());
}
record CidrRange(byte[] network, int prefixLength) {
static CidrRange of(String ip, int prefixLength) {
try {
byte[] addr = InetAddress.getByName(ip).getAddress();
if (prefixLength < 0 || prefixLength > addr.length * 8) {
throw new IllegalArgumentException("Invalid prefix length: " + prefixLength + " for " + ip);
}
return new CidrRange(addr, prefixLength);
} catch (UnknownHostException e) {
throw new IllegalArgumentException("Invalid IP: " + ip, e);
}
}
static CidrRange parse(String entry) throws UnknownHostException {
int slashIndex = entry.indexOf('/');
if (slashIndex >= 0) {
String ip = entry.substring(0, slashIndex);
int prefix = Integer.parseInt(entry.substring(slashIndex + 1));
byte[] addr = InetAddress.getByName(ip).getAddress();
if (prefix < 0 || prefix > addr.length * 8) {
throw new IllegalArgumentException("Invalid prefix length: " + prefix + " for " + entry);
}
return new CidrRange(addr, prefix);
} else {
byte[] addr = InetAddress.getByName(entry).getAddress();
return new CidrRange(addr, addr.length * 8);
}
}
boolean contains(InetAddress address) {
byte[] addr = address.getAddress();
if (addr.length != network.length) {
return false;
}
int fullBytes = prefixLength / 8;
int remainingBits = prefixLength % 8;
for (int i = 0; i < fullBytes; i++) {
if (addr[i] != network[i]) {
return false;
}
}
if (remainingBits > 0 && fullBytes < addr.length) {
int mask = 0xFF << (8 - remainingBits);
if ((addr[fullBytes] & mask) != (network[fullBytes] & mask)) {
return false;
}
}
return true;
}
@Override
public String toString() {
try {
return InetAddress.getByAddress(network).getHostAddress() + "/" + prefixLength;
} catch (UnknownHostException e) {
return "invalid/" + prefixLength;
}
}
}
}

349
common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java

@ -0,0 +1,349 @@
/**
* 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.common.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class SsrfProtectionValidatorTest {
// JUnit 5 @ResourceLock ensures that tests modifying SsrfProtectionValidator's static
// additional blocked hosts never run concurrently with each other (parallel execution is enabled).
private static final String SYNC_LOCK = "SsrfProtectionValidatorTest";
@ParameterizedTest
@ValueSource(strings = {
"http://example.com",
"https://example.com:8443/path",
"https://8.8.8.8/dns-query"
})
void testAllowedUrls(String url) {
URI uri = URI.create(url);
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true));
}
@ParameterizedTest
@ValueSource(strings = {
"http://127.0.0.1",
"http://127.0.0.1:8080/path",
"http://127.1.2.3"
})
void testBlockedLoopbackIpv4(String url) {
URI uri = URI.create(url);
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@Test
void testBlockedLocalhost() {
URI uri = URI.create("http://localhost/path");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@Test
void testBlockedIpv6Loopback() {
URI uri = URI.create("http://[::1]/path");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@ParameterizedTest
@ValueSource(strings = {
"http://169.254.169.254/latest/meta-data/",
"http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"http://169.254.1.1"
})
void testBlockedLinkLocalImds(String url) {
URI uri = URI.create(url);
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@ParameterizedTest
@ValueSource(strings = {
"http://10.0.0.1",
"http://10.255.255.255",
"http://172.16.0.1",
"http://172.31.255.255",
"http://192.168.1.1",
"http://192.168.0.100:8080/api"
})
void testBlockedPrivateRfc1918(String url) {
URI uri = URI.create(url);
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@ParameterizedTest
@ValueSource(strings = {
// 100.64.0.0/10 — Carrier-Grade NAT (RFC 6598): Alibaba Cloud metadata (100.100.100.200), Tencent Cloud (100.88.222.5)
"http://100.100.100.200",
"http://100.88.222.5",
"http://100.64.0.1",
"http://100.127.255.255",
// 192.0.0.0/24 — IANA reserved (RFC 6890): Oracle Cloud alternate metadata (192.0.0.192)
"http://192.0.0.192",
"http://192.0.0.1",
// 168.63.129.16 — Azure WireServer
"http://168.63.129.16"
})
void testBlockedCloudMetadataEndpoints(String url) {
URI uri = URI.create(url);
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@ParameterizedTest
@ValueSource(strings = {
// Just outside 100.64.0.0/10
"http://100.128.0.1",
// Just outside 192.0.0.0/24
"http://192.0.1.1",
// Adjacent to Azure WireServer
"http://168.63.129.17"
})
void testAllowedNearCloudMetadataBoundaries(String url) {
URI uri = URI.create(url);
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true));
}
@ParameterizedTest
@ValueSource(strings = {
"file:///etc/passwd",
"ftp://internal.host/file"
})
void testBlockedSchemes(String url) {
URI uri = URI.create(url);
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("only HTTP and HTTPS schemes are allowed");
}
@Test
void testBlockedZeroAddress() {
URI uri = URI.create("http://0.0.0.0");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@ParameterizedTest
@ValueSource(strings = {
"http://server.internal",
"http://app.local"
})
void testBlockedHostnameSuffixes(String url) {
URI uri = URI.create(url);
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@Test
void testBlockedNullScheme() {
URI uri = URI.create("//example.com/path");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("only HTTP and HTTPS schemes are allowed");
}
@Test
void testBlockedEmptyHost() {
URI uri = URI.create("http:///path");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("hostname is missing");
}
@Test
void testBlockedUnresolvableHostname() {
URI uri = URI.create("http://host.invalid.tld.that.does.not.exist/path");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("unable to resolve hostname");
}
@Test
void testBlockedLocalhostCaseInsensitive() {
URI uri = URI.create("http://LOCALHOST/path");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@Test
void testDisabledAllowsPrivateAddresses() {
URI uri = URI.create("http://127.0.0.1");
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(uri, false));
}
@Test
@ResourceLock(SYNC_LOCK)
void testAdditionalBlockedSingleIp() {
try {
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("8.8.8.8"));
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.8/dns-query"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Adjacent IP is not blocked
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.9"), true));
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
}
}
@Test
@ResourceLock(SYNC_LOCK)
void testAdditionalBlockedCidrSlash10() {
try {
// Use 44.0.0.0/10 (not blocked by default) to verify CIDR /10 matching
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("44.0.0.0/10"));
// Inside the range
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://44.0.1.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Last address in the range
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://44.63.255.255"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Outside the range
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://44.64.0.1"), true));
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
}
}
@Test
@ResourceLock(SYNC_LOCK)
void testAdditionalBlockedCidrSlash24() {
try {
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("198.51.100.0/24"));
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://198.51.100.0"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://198.51.100.255"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Outside the range
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://198.51.101.0"), true));
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
}
}
@Test
@ResourceLock(SYNC_LOCK)
void testAdditionalBlockedHostnameViaValidateUri() {
try {
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("evil.corp"));
URI uri = URI.create("http://evil.corp/api");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
}
}
@Test
@ResourceLock(SYNC_LOCK)
void testAdditionalBlockedHostnameCaseInsensitive() {
try {
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("My-Service.Corp"));
URI uri = URI.create("http://my-service.corp/api");
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
}
}
@Test
@ResourceLock(SYNC_LOCK)
void testSetAdditionalBlockedHostsEmptyAndNull() {
// Should not throw
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
SsrfProtectionValidator.setAdditionalBlockedHosts(null);
}
@Test
void testCidrRangeInvalidPrefixLength() {
assertThatThrownBy(() -> SsrfProtectionValidator.CidrRange.parse("10.0.0.0/999"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid prefix length");
assertThatThrownBy(() -> SsrfProtectionValidator.CidrRange.parse("10.0.0.0/-1"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid prefix length");
}
@Test
@ResourceLock(SYNC_LOCK)
void testAdditionalBlockedCidrViaValidateUri() {
// 203.0.113.0/24 (TEST-NET-3) is not blocked by default
URI uri = URI.create("http://203.0.113.1");
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true));
try {
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("203.0.113.0/24"));
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(uri, true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
}
}
@Test
@ResourceLock(SYNC_LOCK)
void testAdditionalBlockedMixedConfig() {
try {
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("203.0.113.0/24", "evil.corp", "8.8.8.8"));
// CIDR range
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://203.0.113.50"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Hostname
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://evil.corp/api"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Single IP
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.8"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Not in any additional block list
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://1.1.1.1"), true));
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
}
}
}

16
common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java

@ -48,7 +48,7 @@ public class DefaultGitSyncService implements GitSyncService {
private final Map<String, GitRepository> repositories = new ConcurrentHashMap<>();
private final Map<String, Runnable> updateListeners = new ConcurrentHashMap<>();
private RevCommit lastCommit;
private final Map<String, RevCommit> lastCommits = new ConcurrentHashMap<>();
@Override
public void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate) {
@ -87,7 +87,7 @@ public class DefaultGitSyncService implements GitSyncService {
@Override
public List<RepoFile> listFiles(String key, String path, int depth, FileType type) {
GitRepository repository = getRepository(key);
return repository.listFilesAtCommit(lastCommit, path, depth).stream()
return repository.listFilesAtCommit(getLastCommit(key), path, depth).stream()
.filter(file -> type == null || file.type() == type)
.toList();
}
@ -96,7 +96,7 @@ public class DefaultGitSyncService implements GitSyncService {
@Override
public byte[] getFileContent(String key, String path) {
GitRepository repository = getRepository(key);
return repository.getFileContentAtCommit(path, lastCommit);
return repository.getFileContentAtCommit(path, getLastCommit(key));
}
@Override
@ -143,7 +143,7 @@ public class DefaultGitSyncService implements GitSyncService {
GitRepository repository = getRepository(key);
String branchRef = getBranchRef(repository);
try {
lastCommit = repository.resolveCommit(branchRef);
lastCommits.put(key, repository.resolveCommit(branchRef));
} catch (Throwable e) {
log.error("[{}] Failed to resolve commit for ref {}", key, branchRef, e);
return;
@ -166,6 +166,14 @@ public class DefaultGitSyncService implements GitSyncService {
return Path.of(repositoriesFolder, name);
}
private RevCommit getLastCommit(String key) {
RevCommit commit = lastCommits.get(key);
if (commit == null) {
throw new IllegalStateException(key + " repository has no resolved commit");
}
return commit;
}
private String getBranchRef(GitRepository repository) {
return "refs/remotes/origin/" + repository.getSettings().getDefaultBranch();
}

27
dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java

@ -35,12 +35,17 @@ public class Oauth2ClientDataValidator extends DataValidator<OAuth2Client> {
@Override
protected void validateDataImpl(TenantId tenantId, OAuth2Client oAuth2Client) {
OAuth2MapperConfig mapperConfig = oAuth2Client.getMapperConfig();
if (mapperConfig.getType() == MapperType.BASIC) {
MapperType type = mapperConfig.getType();
if (type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE) {
OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic();
if (basicConfig == null) {
throw new DataValidationException("Basic config should be specified!");
}
if (StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) {
if (type == MapperType.GITHUB) {
if (!StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) {
throw new DataValidationException("Email attribute key cannot be configured for GITHUB mapper type!");
}
} else if (StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) {
throw new DataValidationException("Email attribute key should be specified!");
}
if (basicConfig.getTenantNameStrategy() == null) {
@ -51,23 +56,7 @@ public class Oauth2ClientDataValidator extends DataValidator<OAuth2Client> {
throw new DataValidationException("Tenant name pattern should be specified!");
}
}
if (mapperConfig.getType() == MapperType.GITHUB) {
OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic();
if (basicConfig == null) {
throw new DataValidationException("Basic config should be specified!");
}
if (!StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) {
throw new DataValidationException("Email attribute key cannot be configured for GITHUB mapper type!");
}
if (basicConfig.getTenantNameStrategy() == null) {
throw new DataValidationException("Tenant name strategy should be specified!");
}
if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM
&& StringUtils.isEmpty(basicConfig.getTenantNamePattern())) {
throw new DataValidationException("Tenant name pattern should be specified!");
}
}
if (mapperConfig.getType() == MapperType.CUSTOM) {
if (type == MapperType.CUSTOM) {
OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom();
if (customConfig == null) {
throw new DataValidationException("Custom config should be specified!");

5
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java

@ -17,8 +17,10 @@ package org.thingsboard.server.dao.sql;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.common.util.ListeningExecutor;
import javax.sql.DataSource;
import java.sql.SQLException;
@ -29,7 +31,8 @@ import java.sql.Statement;
public abstract class JpaAbstractDaoListeningExecutorService {
@Autowired
protected JpaExecutorService service;
@Qualifier("jpaExecutorService")
protected ListeningExecutor service;
@Autowired
protected DataSource dataSource;

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java

@ -120,7 +120,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
var aggParams = query.getAggParameters();
if (Aggregation.NONE.equals(aggParams.getAggregation()) || aggParams.getInterval() < 1) {
return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query));
return service.submit(() -> findAllWithLimit(entityId, query));
} else {
List<ListenableFuture<Optional<TsKvEntity>>> futures = new ArrayList<>();
var intervalType = aggParams.getIntervalType();
@ -144,7 +144,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
}
}
ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) {
ReadTsKvQueryResult findAllWithLimit(EntityId entityId, ReadTsKvQuery query) {
Integer keyId = keyDictionaryDao.getOrSaveKeyId(query.getKey());
List<TsKvEntity> tsKvEntities = tsKvRepository.findAllWithLimit(
entityId.getId(),

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java

@ -152,7 +152,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
var aggParams = query.getAggParameters();
var intervalType = aggParams.getIntervalType();
if (query.getAggregation() == Aggregation.NONE) {
return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query));
return service.submit(() -> findAllWithLimit(entityId, query));
} else if (IntervalType.MILLISECONDS.equals(intervalType)) {
long startTs = query.getStartTs();
long endTs = Math.max(query.getStartTs() + 1, query.getEndTs());
@ -179,7 +179,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
super.cleanup(systemTtl);
}
private ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) {
private ReadTsKvQueryResult findAllWithLimit(EntityId entityId, ReadTsKvQuery query) {
String strKey = query.getKey();
Integer keyId = keyDictionaryDao.getOrSaveKeyId(strKey);
List<TimescaleTsKvEntity> timescaleTsKvEntities = tsKvRepository.findAllWithLimit(

7
dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java

@ -18,6 +18,8 @@ package org.thingsboard.server.dao.sqlts;
import com.google.common.util.concurrent.Futures;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
@ -48,10 +50,11 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
@Before
public void setUp() throws Exception {
tsDao = spy(AbstractChunkedAggregationTimeseriesDao.class);
ReflectionTestUtils.setField(tsDao, "service", DirectListeningExecutor.INSTANCE);
Optional<TsKvEntry> optionalListenableFuture = Optional.of(mock(TsKvEntry.class));
willReturn(Futures.immediateFuture(optionalListenableFuture)).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any());
willReturn(Futures.immediateFuture(mock(ReadTsKvQueryResult.class))).given(tsDao).getReadTsKvQueryResultFuture(any(), any());
willReturn(mock(ReadTsKvQueryResult.class)).given(tsDao).findAllAsyncWithLimit(any(), any());
willReturn(mock(ReadTsKvQueryResult.class)).given(tsDao).findAllWithLimit(any(), any());
}
@Test
@ -161,7 +164,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, interval, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1)).findAllAsyncWithLimit(any(), any());
verify(tsDao, times(1)).findAllWithLimit(any(), any());
verify(tsDao, times(0)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
}

11
pom.xml

@ -40,6 +40,7 @@
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<spring-boot.version>3.4.13</spring-boot.version>
<tomcat.version>10.1.52</tomcat.version> <!-- to fix CVE-2026-24734 and CVE-2025-66614. TODO: remove when fixed in spring-boot-dependencies -->
<jackson.version>2.18.6</jackson.version> <!-- to fix CWE-770. TODO: remove when fixed in spring-boot-dependencies -->
<javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version>
<jedis.version>5.1.5</jedis.version>
<jjwt.version>0.12.5</jjwt.version>
@ -920,6 +921,16 @@
</dependency>
<!-- End of Tomcat version override -->
<!-- Temporary Jackson version override -->
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- End of Jackson version override -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java

@ -41,6 +41,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -177,15 +178,11 @@ public class TbMsgDeduplicationNode implements TbNode {
}
}
if (resultMsg != null) {
String queueName1 = queueName != null ? queueName : resultMsg.getQueueName();
deduplicationResults.add(TbMsg.newMsg()
.queueName(queueName1)
.type(resultMsg.getType())
.originator(resultMsg.getOriginator())
.customerId(resultMsg.getCustomerId())
.copyMetaData(resultMsg.getMetaData())
.data(resultMsg.getData())
.build());
var msgBuilder = resultMsg.copyWithNewCtx().id(UUID.randomUUID());
if (queueName != null) {
msgBuilder.queueName(queueName);
}
deduplicationResults.add(msgBuilder.build());
}
}
packBoundsOpt = findValidPack(msgList, deduplicationTimeoutMs);

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java

@ -62,17 +62,9 @@ public class TbMsgDelayNode implements TbNode {
if (msg.isTypeOf(TbMsgType.DELAY_TIMEOUT_SELF_MSG)) {
TbMsg pendingMsg = pendingMsgs.remove(UUID.fromString(msg.getData()));
if (pendingMsg != null) {
ctx.enqueueForTellNext(
TbMsg.newMsg()
.queueName(pendingMsg.getQueueName())
.type(pendingMsg.getType())
.originator(pendingMsg.getOriginator())
.customerId(pendingMsg.getCustomerId())
.copyMetaData(pendingMsg.getMetaData())
.data(pendingMsg.getData())
.build(),
TbNodeConnectionType.SUCCESS
);
ctx.enqueueForTellNext(pendingMsg.copyWithNewCtx()
.id(UUID.randomUUID())
.build(), TbNodeConnectionType.SUCCESS);
}
} else {
if (pendingMsgs.size() < config.getMaxPendingMsgs()) {

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java

@ -30,7 +30,7 @@ public abstract class TbAbstractExternalNode implements TbNode {
protected void tellSuccess(TbContext ctx, TbMsg tbMsg) {
if (forceAck) {
ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbNodeConnectionType.SUCCESS);
ctx.enqueueForTellNext(tbMsg.copyWithNewCtx().build(), TbNodeConnectionType.SUCCESS);
} else {
ctx.tellSuccess(tbMsg);
}
@ -39,9 +39,9 @@ public abstract class TbAbstractExternalNode implements TbNode {
protected void tellFailure(TbContext ctx, TbMsg tbMsg, Throwable t) {
if (forceAck) {
if (t == null) {
ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbNodeConnectionType.FAILURE);
ctx.enqueueForTellNext(tbMsg.copyWithNewCtx().build(), TbNodeConnectionType.FAILURE);
} else {
ctx.enqueueForTellFailure(tbMsg.copyWithNewCtx(), t);
ctx.enqueueForTellFailure(tbMsg.copyWithNewCtx().build(), t);
}
} else {
if (t == null) {
@ -55,7 +55,7 @@ public abstract class TbAbstractExternalNode implements TbNode {
protected TbMsg ackIfNeeded(TbContext ctx, TbMsg msg) {
if (forceAck) {
ctx.ack(msg);
return msg.copyWithNewCtx();
return msg.copyWithNewCtx().build();
} else {
return msg;
}

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -32,6 +32,7 @@ import org.springframework.web.reactive.function.client.WebClient.RequestBodySpe
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
@ -281,6 +282,8 @@ public class TbHttpClient {
throw new RuntimeException("Url string is invalid!");
}
SsrfProtectionValidator.validateUri(uri);
return uri;
}

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java

@ -27,7 +27,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext;
@ -84,7 +84,7 @@ class TbClearAlarmNodeTest {
@BeforeEach
void before() {
dbExecutor = new TestDbCallbackExecutor();
dbExecutor = DirectListeningExecutor.INSTANCE;
metadata = new TbMsgMetaData();
}

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java

@ -29,7 +29,7 @@ import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext;
@ -99,7 +99,7 @@ class TbCreateAlarmNodeTest {
@BeforeEach
void before() {
dbExecutor = new TestDbCallbackExecutor();
dbExecutor = DirectListeningExecutor.INSTANCE;
metadata = new TbMsgMetaData();
config = new TbCreateAlarmNodeConfiguration();

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java

@ -29,7 +29,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -142,7 +142,7 @@ public class TbCreateRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private final DeviceId originatorId = new DeviceId(UUID.fromString("860634b1-8a1e-4693-9ae8-e779c7f5f4da"));
private final RuleNodeId ruleNodeId = new RuleNodeId(UUID.fromString("d05a0491-ee7a-484a-8c1b-91111ef39287"));
private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor();
private final ListeningExecutor dbExecutor = DirectListeningExecutor.INSTANCE;
@Mock
private TbContext ctxMock;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeTest.java

@ -29,7 +29,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -130,7 +130,7 @@ public class TbDeleteRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private final DeviceId originatorId = new DeviceId(UUID.fromString("574c9840-0885-4d12-be69-f557d7471a78"));
private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor();
private final ListeningExecutor dbExecutor = DirectListeningExecutor.INSTANCE;
@Mock
private TbContext ctxMock;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeTest.java

@ -41,7 +41,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -88,7 +88,7 @@ public class TbSaveToCustomCassandraTableNodeTest extends AbstractRuleNodeUpgrad
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("ac4ca02e-2ae6-404a-8f7e-c4ae31c56aa7"));
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("64ad971e-9cfa-49e4-9f59-faa1a2350c6e"));
private final ListeningExecutor dbCallbackExecutor = new TestDbCallbackExecutor();
private final ListeningExecutor dbCallbackExecutor = DirectListeningExecutor.INSTANCE;
private TbSaveToCustomCassandraTableNode node;
private TbSaveToCustomCassandraTableNodeConfiguration config;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeTest.java

@ -28,7 +28,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -100,7 +100,7 @@ class TbUnassignFromCustomerNodeTest extends AbstractRuleNodeUpgradeTest {
private final TenantId TENANT_ID = new TenantId(UUID.fromString("06fcc15f-2677-436d-a1cb-7754bd0bcccf"));
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
private static Stream<Arguments> givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown() {
return unsupportedEntityTypes.stream().flatMap(type -> Stream.of(Arguments.of(type)));

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java

@ -39,7 +39,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat;
import org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonSchemaResponseFormat;
import org.thingsboard.rule.engine.ai.TbResponseFormat.TbTextResponseFormat;
@ -164,7 +164,7 @@ class TbAiNodeTest {
lenient().when(ctxMock.getTenantId()).thenReturn(tenantId);
lenient().when(ctxMock.getAiModelService()).thenReturn(aiModelServiceMock);
lenient().when(ctxMock.getAiChatModelService()).thenReturn(aiChatModelServiceMock);
lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(new TestDbCallbackExecutor());
lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(DirectListeningExecutor.INSTANCE);
lenient().when(ctxMock.getTbResourceDataCache()).thenReturn(tbResourceDataCacheMock);
lenient().when(ctxMock.getResourceService()).thenReturn(resourceServiceMock);
}

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java

@ -32,7 +32,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.id.DeviceId;
@ -59,7 +59,7 @@ import static org.mockito.BDDMockito.verifyNoMoreInteractions;
class TbSnsNodeTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("fccfdf2e-6a88-4a94-81dd-5cbb557019cf"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private final ListeningExecutor executor = DirectListeningExecutor.INSTANCE;
private TbSnsNode node;
private TbSnsNodeConfiguration config;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java

@ -33,7 +33,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.aws.sqs.TbSqsNodeConfiguration.QueueType;
@ -63,7 +63,7 @@ import static org.mockito.BDDMockito.verifyNoMoreInteractions;
class TbSqsNodeTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("764de824-929f-4114-95ea-0ea0401ffa3d"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private final ListeningExecutor executor = DirectListeningExecutor.INSTANCE;
private final String messageId = "msgId-1d186a16-80c7-44b3-a245-a1fc835f20c7";
private final String requestId = "reqId-bef0799b-dde9-4aa0-855b-86bbafaeaf31";

106
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeTest.java

@ -0,0 +1,106 @@
/**
* 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.rule.engine.delay;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.TbMsgProcessingCtx;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class)
class TbMsgDelayNodeTest {
final DeviceId deviceId = new DeviceId(UUID.fromString("5770153d-6ca2-4447-8a54-5d8a4538e052"));
final RuleNodeId ruleNodeId = new RuleNodeId(UUID.fromString("ee682a85-7f5a-4182-91bc-46e555138fe2"));
TbMsgDelayNode node;
@Mock
TbContext ctxMock;
@BeforeEach
void setUp() throws TbNodeException {
node = new TbMsgDelayNode();
var config = new TbMsgDelayNodeConfiguration().defaultConfiguration();
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
lenient().when(ctxMock.getSelfId()).thenReturn(ruleNodeId);
}
@Test
void shouldPreserveRuleNodeCounterAndResetCallbackWhenEnqueuingDelayedMsg() {
// GIVEN
int ruleNodeExecCounter = 5;
var originalMsg = TbMsg.newMsg()
.id(UUID.randomUUID())
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(deviceId)
.metaData(TbMsgMetaData.EMPTY)
.data("{\"temperature\":42}")
.ctx(new TbMsgProcessingCtx(ruleNodeExecCounter))
.build();
String originalMsgId = originalMsg.getId().toString();
var tickMsg = TbMsg.newMsg()
.type(TbMsgType.DELAY_TIMEOUT_SELF_MSG)
.originator(ruleNodeId)
.metaData(TbMsgMetaData.EMPTY)
.data(originalMsgId)
.build();
given(ctxMock.newMsg(null, TbMsgType.DELAY_TIMEOUT_SELF_MSG, ruleNodeId, null, TbMsgMetaData.EMPTY, originalMsgId)).willReturn(tickMsg);
node.onMsg(ctxMock, originalMsg);
// WHEN
node.onMsg(ctxMock, tickMsg);
// THEN
var msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().enqueueForTellNext(msgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS));
var enqueuedMsg = msgCaptor.getValue();
assertThat(enqueuedMsg).usingRecursiveComparison()
.ignoringFields("id", "ts", "callback")
.isEqualTo(originalMsg);
assertThat(enqueuedMsg.getId()).isNotNull().isNotEqualTo(originalMsg.getId());
assertThat(enqueuedMsg.getAndIncrementRuleNodeCounter()).isEqualTo(ruleNodeExecCounter);
assertThat(enqueuedMsg.getCallback()).isSameAs(TbMsgCallback.EMPTY);
}
}

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeTest.java

@ -21,7 +21,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -53,7 +53,7 @@ class TbCheckAlarmStatusNodeTest {
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID());
private static final AlarmId ALARM_ID = new AlarmId(UUID.randomUUID());
private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final DirectListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
private TbCheckAlarmStatusNode node;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java

@ -24,7 +24,7 @@ import org.junit.jupiter.params.provider.Arguments;
import org.mockito.ArgumentCaptor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -65,7 +65,7 @@ class TbCheckRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private final DeviceId ORIGINATOR_ID = new DeviceId(UUID.randomUUID());
private final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private final DirectListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
private final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg()
.type(TbMsgType.POST_ATTRIBUTES_REQUEST)
.originator(ORIGINATOR_ID)

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNodeTest.java

@ -32,7 +32,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -65,7 +65,7 @@ import static org.mockito.BDDMockito.willThrow;
class TbPubSubNodeTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("d29849c2-3f21-48e2-8557-74cdd6403290"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private final ListeningExecutor executor = DirectListeningExecutor.INSTANCE;
private TbPubSubNode node;
private TbPubSubNodeConfiguration config;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeTest.java

@ -40,7 +40,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -79,7 +79,7 @@ public class TbKafkaNodeTest extends AbstractRuleNodeUpgradeTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5f2eac08-bd1f-4635-a6c2-437369f996cf"));
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("d46bb666-ecab-4d89-a28f-5abdca23ac29"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private final ListeningExecutor executor = DirectListeningExecutor.INSTANCE;
private final long OFFSET = 1;
private final int PARTITION = 0;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java

@ -30,10 +30,10 @@ import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -91,7 +91,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
private final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.fromString("2ba3ded4-882b-40cf-999a-89da9ccd58f9"));
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("3842e740-0d89-43a9-8d52-ae44023847ba"));
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
private static final int RULE_DISPATCHER_POOL_SIZE = 2;
private static final int DB_CALLBACK_POOL_SIZE = 3;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java

@ -28,7 +28,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -85,7 +85,7 @@ public class TbGetCustomerAttributeNodeTest {
private final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID());
private final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID());
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
@Mock
private TbContext ctxMock;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java

@ -26,7 +26,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -77,7 +77,7 @@ public class TbGetCustomerDetailsNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
@Mock
private TbContext ctxMock;
@Mock

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java

@ -26,7 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -62,7 +62,7 @@ public class TbGetDeviceAttrNodeTest extends AbstractRuleNodeUpgradeTest {
private final TenantId TENANT_ID = new TenantId(UUID.fromString("5aea576c-66c4-4732-86b8-dc6bfcde7443"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("40b6b393-6ddf-47f9-973a-18550ca70384"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private final ListeningExecutor executor = DirectListeningExecutor.INSTANCE;
private TbGetDeviceAttrNode node;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java

@ -25,7 +25,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -60,7 +60,7 @@ public class TbGetOriginatorFieldsNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId DUMMY_TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
@Mock
private TbContext ctxMock;
@Mock

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java

@ -28,7 +28,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -92,7 +92,7 @@ public class TbGetRelatedAttributeNodeTest {
private static final EntityId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
@Mock
private TbContext ctxMock;
@Mock

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java

@ -29,7 +29,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -72,7 +72,7 @@ public class TbGetTelemetryNodeTest extends AbstractRuleNodeUpgradeTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5738401b-9dba-422b-b656-a62fe7431917"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("8a8fd749-b2ec-488b-a6c6-fc66614d8686"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private final ListeningExecutor executor = DirectListeningExecutor.INSTANCE;
private TbGetTelemetryNode node;
private TbGetTelemetryNodeConfiguration config;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java

@ -27,7 +27,7 @@ import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -71,7 +71,7 @@ public class TbGetTenantAttributeNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final DirectListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
@Mock
private TbContext ctxMock;
@Mock

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeTest.java

@ -36,7 +36,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -78,7 +78,7 @@ public class TbRabbitMqNodeTest {
);
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("b3d6f9dd-15cc-4e61-acc0-13197a090406"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private final ListeningExecutor executor = DirectListeningExecutor.INSTANCE;
private TbRabbitMqNode node;
private TbRabbitMqNodeConfiguration config;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java

@ -30,7 +30,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -89,7 +89,7 @@ public class TbChangeOriginatorNodeTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("990605a4-db46-4ed4-942f-e18200453571"));
private final AssetId ASSET_ID = new AssetId(UUID.fromString("55de3f10-1b55-4950-b711-ed132896b260"));
private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor();
private final ListeningExecutor dbExecutor = DirectListeningExecutor.INSTANCE;
private TbChangeOriginatorNode node;
private TbChangeOriginatorNodeConfiguration config;

13
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java

@ -47,6 +47,8 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.TbMsgProcessingCtx;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import java.util.ArrayList;
import java.util.List;
@ -79,6 +81,8 @@ public class TbMsgDeduplicationNodeTest extends AbstractRuleNodeUpgradeTest {
private TbContext ctx;
private static final int RULE_NODE_EXEC_COUNTER = 5;
private final ScheduledExecutorService executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("de-duplication-node-test");
private final int deduplicationInterval = 1;
@ -204,6 +208,8 @@ public class TbMsgDeduplicationNodeTest extends AbstractRuleNodeUpgradeTest {
Assertions.assertEquals(firstMsg.getData(), actualMsg.getData());
Assertions.assertEquals(firstMsg.getMetaData(), actualMsg.getMetaData());
Assertions.assertEquals(firstMsg.getType(), actualMsg.getType());
Assertions.assertEquals(RULE_NODE_EXEC_COUNTER, actualMsg.getAndIncrementRuleNodeCounter());
Assertions.assertSame(TbMsgCallback.EMPTY, actualMsg.getCallback());
if (queueName == null) {
Assertions.assertEquals(firstMsg.getQueueName(), actualMsg.getQueueName());
@ -257,6 +263,8 @@ public class TbMsgDeduplicationNodeTest extends AbstractRuleNodeUpgradeTest {
Assertions.assertEquals(msgWithLatestTs.getData(), actualMsg.getData());
Assertions.assertEquals(msgWithLatestTs.getMetaData(), actualMsg.getMetaData());
Assertions.assertEquals(msgWithLatestTs.getType(), actualMsg.getType());
Assertions.assertEquals(RULE_NODE_EXEC_COUNTER, actualMsg.getAndIncrementRuleNodeCounter());
Assertions.assertSame(TbMsgCallback.EMPTY, actualMsg.getCallback());
}
@Test
@ -402,6 +410,8 @@ public class TbMsgDeduplicationNodeTest extends AbstractRuleNodeUpgradeTest {
Assertions.assertEquals(msgWithLatestTsInFirstPack.getData(), actualMsg.getData());
Assertions.assertEquals(msgWithLatestTsInFirstPack.getMetaData(), actualMsg.getMetaData());
Assertions.assertEquals(msgWithLatestTsInFirstPack.getType(), actualMsg.getType());
Assertions.assertEquals(RULE_NODE_EXEC_COUNTER, actualMsg.getAndIncrementRuleNodeCounter());
Assertions.assertSame(TbMsgCallback.EMPTY, actualMsg.getCallback());
// verify that newMsg is called but content of messages is the same as in the last msg for the second pack.
actualMsg = resultMsgs.get(1);
@ -411,6 +421,8 @@ public class TbMsgDeduplicationNodeTest extends AbstractRuleNodeUpgradeTest {
Assertions.assertEquals(msgWithLatestTsInSecondPack.getData(), actualMsg.getData());
Assertions.assertEquals(msgWithLatestTsInSecondPack.getMetaData(), actualMsg.getMetaData());
Assertions.assertEquals(msgWithLatestTsInSecondPack.getType(), actualMsg.getType());
Assertions.assertEquals(RULE_NODE_EXEC_COUNTER, actualMsg.getAndIncrementRuleNodeCounter());
Assertions.assertSame(TbMsgCallback.EMPTY, actualMsg.getCallback());
}
@Test
@ -539,6 +551,7 @@ public class TbMsgDeduplicationNodeTest extends AbstractRuleNodeUpgradeTest {
.originator(deviceId)
.copyMetaData(metaData)
.data(JacksonUtil.toString(dataNode))
.ctx(new TbMsgProcessingCtx(RULE_NODE_EXEC_COUNTER))
.build();
}

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java

@ -22,8 +22,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -76,7 +76,7 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class EntitiesFieldsAsyncLoaderTest {
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
private static EnumSet<EntityType> SUPPORTED_ENTITY_TYPES;
private static UUID RANDOM_UUID;
private static TenantId TENANT_ID;

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java

@ -21,7 +21,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.data.DeviceRelationsQuery;
import org.thingsboard.server.common.data.Device;
@ -49,7 +49,7 @@ public class EntitiesRelatedDeviceIdAsyncLoaderTest {
private static final EntityId DUMMY_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
@Mock
private TbContext ctxMock;
@Mock

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java

@ -21,7 +21,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.common.util.DirectListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.Device;
@ -56,7 +56,7 @@ public class EntitiesRelatedEntityIdAsyncLoaderTest {
private static final EntityId ASSET_ORIGINATOR_ID = new AssetId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final ListeningExecutor DB_EXECUTOR = DirectListeningExecutor.INSTANCE;
private TbContext ctxMock;
private RelationService relationServiceMock;

2
ui-ngx/package.json

@ -142,6 +142,6 @@
"esbuild": "0.25.9",
"rollup": "4.52.3",
"jquery.terminal/**/form-data": ">=4.0.4",
"js-beautify/**/minimatch": "^9.0.6"
"js-beautify/**/minimatch": "^9.0.7"
}
}

1
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -243,6 +243,7 @@ export class DashboardUtilsService {
row: widget.row,
col: widget.col,
};
dashboard.configuration.timewindow = initModelFromDefaultTimewindow(null, false, false, this.timeService, true, false);
return dashboard;
}

4
ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts

@ -83,8 +83,6 @@ export class TbImageMap extends TbMap<ImageMapSettings> {
this.onResize(true);
} else {
this.onResize();
this.initMapSubject.next(this.map);
this.initMapSubject.complete();
}
});
return this.initMapSubject.asObservable();
@ -227,6 +225,8 @@ export class TbImageMap extends TbMap<ImageMapSettings> {
attributionControl: false
});
this.updateMaxBounds(updateImage);
this.initMapSubject.next(this.map);
this.initMapSubject.complete();
}
}

6
ui-ngx/src/app/shared/components/toast.directive.ts

@ -135,8 +135,10 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
notification: notificationMessage,
panelClass,
destroyToastComponent: () => {
this.viewContainerRef.detach(0);
this.toastComponentRef.destroy();
if (this.toastComponentRef) {
this.viewContainerRef.detach(0);
this.toastComponentRef.destroy();
}
}
};
const providers: StaticProvider[] = [

12748
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

File diff suppressed because it is too large

20
ui-ngx/yarn.lock

@ -7732,24 +7732,24 @@ minimalistic-assert@^1.0.0:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimatch@9.0.1, minimatch@^9.0.4, minimatch@^9.0.5, minimatch@~9.0.6:
version "9.0.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.6.tgz#a7e3bccfcb3d78ec1bf8d51c9ba749080237a5c8"
integrity sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==
minimatch@9.0.1, minimatch@^9.0.4, minimatch@^9.0.5, minimatch@^9.0.7:
version "9.0.7"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.7.tgz#d76c4d0b3b527877016d6cc1b9922fc8e0ffe7b0"
integrity sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==
dependencies:
brace-expansion "^5.0.2"
minimatch@^10.0.3, minimatch@^10.1.1:
version "10.2.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
version "10.2.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.3.tgz#c0ef582f21071b0123a5bbf275252ebda921fbf6"
integrity sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==
dependencies:
brace-expansion "^5.0.2"
minimatch@^3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
version "3.1.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.4.tgz#89d910ea3970a77ac8edfd30340ccd038b758079"
integrity sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==
dependencies:
brace-expansion "^1.1.7"

Loading…
Cancel
Save