Browse Source

Merge branch 'lts-4.3' into release-4.3

# Conflicts:
#	application/pom.xml
#	common/actor/pom.xml
#	common/cache/pom.xml
#	common/cluster-api/pom.xml
#	common/coap-server/pom.xml
#	common/dao-api/pom.xml
#	common/data/pom.xml
#	common/discovery-api/pom.xml
#	common/edge-api/pom.xml
#	common/edqs/pom.xml
#	common/message/pom.xml
#	common/pom.xml
#	common/proto/pom.xml
#	common/queue/pom.xml
#	common/script/pom.xml
#	common/script/remote-js-client/pom.xml
#	common/script/script-api/pom.xml
#	common/stats/pom.xml
#	common/transport/coap/pom.xml
#	common/transport/http/pom.xml
#	common/transport/lwm2m/pom.xml
#	common/transport/mqtt/pom.xml
#	common/transport/pom.xml
#	common/transport/snmp/pom.xml
#	common/transport/transport-api/pom.xml
#	common/util/pom.xml
#	common/version-control/pom.xml
#	dao/pom.xml
#	edqs/pom.xml
#	monitoring/pom.xml
#	msa/black-box-tests/pom.xml
#	msa/edqs/pom.xml
#	msa/js-executor/pom.xml
#	msa/monitoring/pom.xml
#	msa/pom.xml
#	msa/tb-node/pom.xml
#	msa/tb/pom.xml
#	msa/transport/coap/pom.xml
#	msa/transport/http/pom.xml
#	msa/transport/lwm2m/pom.xml
#	msa/transport/mqtt/pom.xml
#	msa/transport/pom.xml
#	msa/transport/snmp/pom.xml
#	msa/vc-executor-docker/pom.xml
#	msa/vc-executor/pom.xml
#	msa/web-ui/pom.xml
#	netty-mqtt/pom.xml
#	pom.xml
#	rest-client/pom.xml
#	rule-engine/pom.xml
#	rule-engine/rule-engine-api/pom.xml
#	rule-engine/rule-engine-components/pom.xml
#	tools/pom.xml
#	transport/coap/pom.xml
#	transport/http/pom.xml
#	transport/lwm2m/pom.xml
#	transport/mqtt/pom.xml
#	transport/pom.xml
#	transport/snmp/pom.xml
#	ui-ngx/pom.xml
pull/15304/head
Viacheslav Klimov 7 months ago
parent
commit
a68f0fb0dc
Failed to extract signature
  1. 4
      .github/release.yml
  2. 1
      .gitignore
  3. 39
      TEST_FAST.md
  4. 2
      application/pom.xml
  5. 2
      application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
  6. 2
      application/src/main/data/json/system/widget_types/range_chart.json
  7. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  8. 15
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  9. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  10. 10
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  11. 2
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  12. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  13. 34
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java
  15. 10
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java
  16. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java
  17. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java
  18. 14
      application/src/main/resources/thingsboard.yml
  19. 43
      application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java
  20. 23
      application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
  21. 79
      application/src/test/java/org/thingsboard/server/edge/AdminSettingsEdgeTest.java
  22. 34
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java
  23. 54
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
  24. 60
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java
  25. 19
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java
  26. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java
  27. 15
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java
  28. 9
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/rpc/MqttV5CloseTransportSessionOnRpcDeliveryTimeoutIntegrationTest.java
  29. 15
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java
  30. 2
      common/actor/pom.xml
  31. 2
      common/cache/pom.xml
  32. 2
      common/cluster-api/pom.xml
  33. 2
      common/coap-server/pom.xml
  34. 2
      common/dao-api/pom.xml
  35. 32
      common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/ResultSetSizeLimitExceededException.java
  36. 24
      common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java
  37. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java
  38. 140
      common/dao-api/src/test/java/org/thingsboard/server/dao/nosql/TbResultSetTest.java
  39. 2
      common/data/pom.xml
  40. 2
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
  41. 1
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  42. 2
      common/discovery-api/pom.xml
  43. 2
      common/edge-api/pom.xml
  44. 4
      common/edge-api/src/main/proto/edge.proto
  45. 2
      common/edqs/pom.xml
  46. 2
      common/message/pom.xml
  47. 9
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  48. 12
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java
  49. 89
      common/message/src/test/java/org/thingsboard/server/common/msg/TbMsgProcessingCtxTest.java
  50. 4
      common/message/src/test/java/org/thingsboard/server/common/msg/tools/TbRateLimitsTest.java
  51. 2
      common/pom.xml
  52. 2
      common/proto/pom.xml
  53. 4
      common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java
  54. 2
      common/queue/pom.xml
  55. 2
      common/script/pom.xml
  56. 2
      common/script/remote-js-client/pom.xml
  57. 2
      common/script/script-api/pom.xml
  58. 8
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  59. 95
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  60. 2
      common/stats/pom.xml
  61. 2
      common/transport/coap/pom.xml
  62. 2
      common/transport/http/pom.xml
  63. 2
      common/transport/lwm2m/pom.xml
  64. 2
      common/transport/mqtt/pom.xml
  65. 2
      common/transport/pom.xml
  66. 2
      common/transport/snmp/pom.xml
  67. 2
      common/transport/transport-api/pom.xml
  68. 2
      common/util/pom.xml
  69. 7
      common/util/src/main/java/org/thingsboard/common/util/DirectListeningExecutor.java
  70. 247
      common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java
  71. 338
      common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java
  72. 2
      common/version-control/pom.xml
  73. 24
      common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java
  74. 35
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  75. 2
      dao/pom.xml
  76. 3
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java
  77. 27
      dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java
  78. 4
      dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java
  79. 19
      dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java
  80. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
  81. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  82. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  83. 92
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  84. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  85. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/SimpleListenableFuture.java
  86. 25
      dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java
  87. 42
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java
  88. 7
      dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java
  89. 2
      edqs/pom.xml
  90. 2
      edqs/src/main/resources/edqs.yml
  91. 2
      monitoring/pom.xml
  92. 2
      msa/black-box-tests/pom.xml
  93. 2
      msa/edqs/pom.xml
  94. 7
      msa/js-executor/package.json
  95. 15
      msa/js-executor/pom.xml
  96. 44
      msa/js-executor/yarn.lock
  97. 2
      msa/monitoring/pom.xml
  98. 4
      msa/pom.xml
  99. 2
      msa/tb-node/pom.xml
  100. 2
      msa/tb/pom.xml

4
.github/release.yml

@ -19,6 +19,10 @@ changelog:
labels:
- Ignore for release
categories:
- title: 'Security'
labels:
- 'Security'
- title: 'Major Core & Rule Engine'
labels:
- 'Major Core'

1
.gitignore

@ -37,3 +37,4 @@ rebuild-docker.sh
*/.run/**
.run/**
.run
.claude/

39
TEST_FAST.md

@ -10,22 +10,43 @@ mvn clean install -T6 -DskipTests
mvn test -pl='!application,!dao,!ui-ngx,!msa/js-executor,!msa/web-ui' -T4
mvn test -pl dao -Dparallel=packages -DforkCount=4
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.controller.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.edge.**' -DforkCount=4 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.service.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.mqtt.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.coap.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.lwm2m.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='**/*TestSuite.java' -DforkCount=4 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.controller.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.edge.**' -DforkCount=4 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.service.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.transport.mqtt.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.transport.coap.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.transport.lwm2m.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dtest='**/*TestSuite.java' -DforkCount=4 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
#the rest of application tests
mvn test -pl application -Dtest='
!**/nosql/*Test.java,
!**/nosql/**,
!org.thingsboard.server.controller.**,
!org.thingsboard.server.edge.**,
!org.thingsboard.server.service.**,
!org.thingsboard.server.transport.mqtt.**,
!org.thingsboard.server.transport.coap.**,
!org.thingsboard.server.transport.lwm2m.**
!org.thingsboard.server.transport.lwm2m.**,
!**/*TestSuite.java
' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
```
## Testcontainers compatibility with the Docker API workaround
In case your tests failed to run testcontainers due to unsupported Docker API version
:coffee: testcontainers (Docker API 1.32) + :whale: docker 29 (min API 1.44) workaround
Add to /etc/docker/daemon.json and restart docker
```json
{
"min-api-version": "1.32"
}
```
Same works on Mac, except `daemon.json` are located in another folder and required to be edited from Docker Desktop UI.
Tip: If your testcontainer are struggling to find any Docker. You can try to remove the testcontainers property file. It will be recreated on the next testcontainers run.
```bash
rm ~/.testcontainers.properties
```

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>

2
application/src/main/data/json/system/widget_types/bar_chart_with_labels.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-bar-chart-with-labels-widget \n [ctx]=\"ctx\">\n</tb-bar-chart-with-labels-widget>",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n",
"settingsForm": [],
"dataKeySettingsForm": [],
"latestDataKeySettingsForm": [],

2
application/src/main/data/json/system/widget_types/range_chart.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-range-chart-widget \n [ctx]=\"ctx\">\n</tb-range-chart-widget>",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n",
"settingsForm": [],
"dataKeySettingsForm": [],
"latestDataKeySettingsForm": [],

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}")

15
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -184,11 +184,24 @@ public class DefaultTbContext implements TbContext {
if (!msg.isValid()) {
return;
}
RuleChainId selfRuleChainId = nodeCtx.getSelf().getRuleChainId();
RuleNodeId selfId = nodeCtx.getSelf().getId();
if (msg.isAlreadyInStack(selfRuleChainId, selfId)) {
log.warn("[{}] Detected rule chain processing loop for rule node [{}] in rule chain [{}]. " +
"The message will be failed to prevent infinite loop. " +
"Please check the rule chain configuration for circular references.",
nodeCtx.getTenantId(), selfId, selfRuleChainId);
tellFailure(msg, new RuntimeException(
"Detected rule chain processing loop for rule node [" + selfId + "] " +
"in rule chain [" + selfRuleChainId + "]. " +
"Please check the rule chain configuration for circular references."));
return;
}
TbMsg tbMsg = msg.copy()
.ruleChainId(ruleChainId)
.resetRuleNodeId()
.build();
tbMsg.pushToStack(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId());
tbMsg.pushToStack(selfRuleChainId, selfId);
TopicPartitionInfo tpi = resolvePartition(msg);
doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(md -> ack(msg), t -> tellFailure(msg, t)));
}

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/config/WebSocketConfiguration.java

@ -17,6 +17,7 @@ package org.thingsboard.server.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
@ -40,11 +41,16 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
private final WebSocketHandler wsHandler;
@Value("${server.ws.max_text_message_buffer_size:32768}")
private int maxTextMessageBufferSize;
@Value("${server.ws.max_binary_message_buffer_size:32768}")
private int maxBinaryMessageBufferSize;
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(32768);
container.setMaxBinaryMessageBufferSize(32768);
container.setMaxTextMessageBufferSize(maxTextMessageBufferSize);
container.setMaxBinaryMessageBufferSize(maxBinaryMessageBufferSize);
return container;
}

2
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -284,7 +284,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"keys", "startTs", "endTs"})
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"startTs", "endTs"})
public DeferredResult<ResponseEntity> getTimeseries(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -19,6 +19,7 @@ import lombok.Getter;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.AiModelEdgeEventFetcher;
@ -63,7 +64,8 @@ public class EdgeSyncCursor {
fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService()));
fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService()));
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), TenantId.SYS_TENANT_ID));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), edge.getTenantId()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService()));
fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService()));

34
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java

@ -17,50 +17,32 @@ package org.thingsboard.server.service.edge.rpc.fetch;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.util.ArrayList;
import java.util.List;
@AllArgsConstructor
@Slf4j
public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
public class AdminSettingsEdgeEventFetcher extends BasePageableEdgeEventFetcher<AdminSettings> {
private final AdminSettingsService adminSettingsService;
private final TenantId fetcherTenantId;
@Override
public PageLink getPageLink(int pageSize) {
return null;
}
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
List<EdgeEvent> result = fetchAdminSettingsForKeys(tenantId, edge.getId(), List.of("general", "mail", "connectivity", "jwt"));
// return PageData object to be in sync with other fetchers
return new PageData<>(result, 1, result.size(), false);
PageData<AdminSettings> fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) {
return adminSettingsService.findAllByTenantId(fetcherTenantId, pageLink);
}
private List<EdgeEvent> fetchAdminSettingsForKeys(TenantId tenantId, EdgeId edgeId, List<String> keys) {
List<EdgeEvent> result = new ArrayList<>();
for (String key : keys) {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key);
if (adminSettings != null) {
result.add(EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(adminSettings)));
}
}
return result;
@Override
EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, AdminSettings adminSettings) {
return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, adminSettings.getId(), null);
}
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java

@ -41,7 +41,7 @@ public class OAuth2EdgeEventFetcher extends BasePageableEdgeEventFetcher<DomainI
@Override
EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, DomainInfo domainInfo) {
return EdgeUtils.constructEdgeEvent(TenantId.SYS_TENANT_ID, edge.getId(), EdgeEventType.DOMAIN,
return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.DOMAIN,
EdgeEventActionType.ADDED, domainInfo.getId(), null);
}

10
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AdminSettingsId;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@ -35,7 +36,14 @@ public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AdminSettings adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class);
AdminSettings adminSettings = null;
if (edgeEvent.getEntityId() != null) {
AdminSettingsId adminSettingsId = new AdminSettingsId(edgeEvent.getEntityId());
adminSettings = edgeCtx.getAdminSettingsService().findAdminSettingsById(edgeEvent.getTenantId(), adminSettingsId);
} else if (edgeEvent.getBody() != null && !edgeEvent.getBody().isEmpty()) {
// legacy
adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class);
}
if (adminSettings == null) {
return null;
}

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);
}
}
}
}

10
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java

@ -29,18 +29,10 @@ public class RestAuthenticationDetails implements Serializable {
private final Client userAgent;
public RestAuthenticationDetails(HttpServletRequest request) {
this.clientAddress = getClientIP(request);
this.clientAddress = request.getRemoteAddr();
this.userAgent = getUserAgent(request);
}
private static String getClientIP(HttpServletRequest request) {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader == null) {
return request.getRemoteAddr();
}
return xfHeader.split(",")[0];
}
private static Client getUserAgent(HttpServletRequest request) {
Parser uaParser = new Parser();
return uaParser.parse(request.getHeader("User-Agent"));

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

@ -78,6 +78,10 @@ server:
max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}"
# Maximum number of alarms returned for single alarm subscription. For example, no more than 10,000 alarms on the alarm widget
max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}"
# Maximum size (bytes) of incoming WS text message
max_text_message_buffer_size: "${TB_SERVER_WS_MAX_TEXT_MESSAGE_BUFFER_SIZE:32768}"
# Maximum size (bytes) of incoming WS binary message
max_binary_message_buffer_size: "${TB_SERVER_WS_MAX_BINARY_MESSAGE_BUFFER_SIZE:32768}"
# Maximum queue size of the websocket updates per session. This restriction prevents infinite updates of WS
max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}"
# Maximum time between WS session opening and sending auth command
@ -339,6 +343,8 @@ cassandra:
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}"
# log one of cassandra queries with specified frequency (0 - logging is disabled)
print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
# Maximum total size in bytes of a Cassandra query result set across all pages. Default is 50MB. 0 means unlimited
max_result_set_size_in_bytes: "${CASSANDRA_QUERY_MAX_RESULT_SET_SIZE_IN_BYTES:52428800}"
tenant_rate_limits:
# Whether to print rate-limited tenant names when printing Cassandra query queue statistic
print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}"
@ -507,6 +513,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}"
@ -2036,7 +2048,7 @@ management:
web:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'
include: "${METRICS_ENDPOINTS_EXPOSE:info}"
health:
elasticsearch:
# Enable the org.springframework.boot.actuate.elasticsearch.ElasticsearchRestClientHealthIndicator.doHealthCheck

43
application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.TbMsgProcessingStackItem;
@ -134,6 +135,48 @@ class DefaultTbContextTest {
then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any());
}
@Test
public void givenMsgWithCurrentNodeAlreadyInStack_whenInput_thenTellFailureToPreventLoop() {
// GIVEN - simulate the second iteration: current node is already in the return stack,
// which happens when forwardMsgToDefaultRuleChain=true and the device's default rule chain
// is the same as the rule chain containing this node
var callbackMock = mock(TbMsgCallback.class);
given(callbackMock.isMsgValid()).willReturn(true);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.off());
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
var msg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(TENANT_ID)
.copyMetaData(TbMsgMetaData.EMPTY)
.data(TbMsg.EMPTY_STRING)
.callback(callbackMock)
.build();
// Push current node into the stack - simulates a previous iteration that already forwarded to this rule chain
msg.pushToStack(RULE_CHAIN_ID, RULE_NODE_ID);
var targetRuleChainId = new RuleChainId(UUID.randomUUID());
// WHEN
defaultTbContext.input(msg, targetRuleChainId);
// THEN - loop detected: tellFailure is called and no message is enqueued
ArgumentCaptor<TbActorMsg> tellCaptor = ArgumentCaptor.forClass(TbActorMsg.class);
then(chainActorMock).should().tell(tellCaptor.capture());
TbActorMsg capturedTellMsg = tellCaptor.getValue();
assertThat(capturedTellMsg).isInstanceOf(RuleNodeToRuleChainTellNextMsg.class);
RuleNodeToRuleChainTellNextMsg failureMsg = (RuleNodeToRuleChainTellNextMsg) capturedTellMsg;
assertThat(failureMsg.getRelationTypes()).containsOnly(TbNodeConnectionType.FAILURE);
assertThat(failureMsg.getFailureMessage()).containsIgnoringCase("loop");
then(mainCtxMock).shouldHaveNoInteractions(); // no message was enqueued
}
@MethodSource
@ParameterizedTest
public void givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) {

23
application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java

@ -102,24 +102,29 @@ public class TelemetryControllerTest extends AbstractControllerTest {
Assert.assertEquals(11L, thirdIntervalResult.get("value").asLong());
Assert.assertEquals(thirdIntervalTs, thirdIntervalResult.get("ts").asLong());
result = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() +
ObjectNode resultByMonth = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() +
"/values/timeseries?keys=t&startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}",
ObjectNode.class, startTs, endTs, "SUM", "MONTH", "Europe/Kyiv");
Assert.assertNotNull(result);
Assert.assertNotNull(result.get("t"));
Assert.assertEquals(1, result.get("t").size());
Assert.assertNotNull(resultByMonth);
Assert.assertNotNull(resultByMonth.get("t"));
Assert.assertEquals(1, resultByMonth.get("t").size());
var monthResult = result.get("t").get(0);
var monthResult = resultByMonth.get("t").get(0);
Assert.assertEquals(22L, monthResult.get("value").asLong());
Assert.assertEquals(middleOfTheInterval, monthResult.get("ts").asLong());
// get all latest (without keys parameter)
ObjectNode allLatest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() +
// check timeseries history with key parameter (instead of keys) has the same result as with keys parameter
ObjectNode resultByKey = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() +
"/values/timeseries?key=t&startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}",
ObjectNode.class, startTs, endTs, "SUM", "WEEK_ISO", "Europe/Kyiv");
Assert.assertEquals(result, resultByKey);
// check timeseries history without keys and key results into empty object
ObjectNode resultWithoutKeyAndKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() +
"/values/timeseries?startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}",
ObjectNode.class, startTs, endTs, "SUM", "WEEK_ISO", "Europe/Kyiv");
Assert.assertNotNull(allLatest);
Assert.assertNotNull(allLatest.get("t"));
Assert.assertTrue(resultWithoutKeyAndKeys.isEmpty());
}
@Test

79
application/src/test/java/org/thingsboard/server/edge/AdminSettingsEdgeTest.java

@ -0,0 +1,79 @@
/**
* 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.edge;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.protobuf.AbstractMessage;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
@DaoSqlTest
public class AdminSettingsEdgeTest extends AbstractEdgeTest {
@Autowired
private AdminSettingsService adminSettingsService;
@Test
public void testAdminSettings() throws Exception {
loginSysAdmin();
// save
AdminSettings adminSettings = new AdminSettings();
adminSettings.setKey("edgeTest");
ObjectNode jsonValue = JacksonUtil.newObjectNode();
jsonValue.put("key1", "value1");
adminSettings.setJsonValue(jsonValue);
edgeImitator.expectMessageAmount(1);
AdminSettings savedAdminSettings = doPost("/api/admin/settings", adminSettings, AdminSettings.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AdminSettingsUpdateMsg);
AdminSettingsUpdateMsg adminSettingsUpdateMsg = (AdminSettingsUpdateMsg) latestMessage;
AdminSettings adminSettingsMsg = JacksonUtil.fromString(adminSettingsUpdateMsg.getEntity(), AdminSettings.class, true);
Assert.assertNotNull(adminSettingsMsg);
Assert.assertEquals("edgeTest", adminSettingsMsg.getKey());
Assert.assertEquals("value1", adminSettingsMsg.getJsonValue().get("key1").asText());
// update
ObjectNode updatedJsonValue = (ObjectNode) savedAdminSettings.getJsonValue();
updatedJsonValue.put("key2", "value2");
savedAdminSettings.setJsonValue(updatedJsonValue);
edgeImitator.expectMessageAmount(1);
doPost("/api/admin/settings", savedAdminSettings, AdminSettings.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AdminSettingsUpdateMsg);
adminSettingsUpdateMsg = (AdminSettingsUpdateMsg) latestMessage;
adminSettingsMsg = JacksonUtil.fromString(adminSettingsUpdateMsg.getEntity(), AdminSettings.class, true);
Assert.assertNotNull(adminSettingsMsg);
Assert.assertEquals("edgeTest", adminSettingsMsg.getKey());
Assert.assertEquals("value1", adminSettingsMsg.getJsonValue().get("key1").asText());
Assert.assertEquals("value2", adminSettingsMsg.getJsonValue().get("key2").asText());
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(savedAdminSettings.getTenantId(), "edgeTest");
}
}

34
application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.script;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbDate;
@ -779,7 +780,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
assertEquals(expected.toString(), actual.toString());
}
@Test
@Test
public void setsCreateNewSetFromCreateSetTbMethod_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["B", "A", "C", "A"]}
@ -800,7 +801,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
assertEquals(expected.toString(), actual.toString());
}
@Test
@Test
public void setsForeachForLoop_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["A", "B", "C"]}
@ -975,13 +976,13 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
ArrayList<Object> listSortDesc = new ArrayList<>(List.of("hello", "C", "B", "A", 34567, 34));
Set<Object> expectedDesc = new LinkedHashSet<>(listSortDesc);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>)actual).get("set1").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>)actual).get("set1_asc").toString());
assertEquals(expectedDesc.toString(), ((LinkedHashMap<?, ?>)actual).get("set1_desc").toString());
assertEquals(expected.toString(), ((LinkedHashMap<?, ?>)actual).get("set2").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>)actual).get("set3").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>)actual).get("set3_asc").toString());
assertEquals(expectedDesc.toString(), ((LinkedHashMap<?, ?>)actual).get("set3_desc").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>) actual).get("set1").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>) actual).get("set1_asc").toString());
assertEquals(expectedDesc.toString(), ((LinkedHashMap<?, ?>) actual).get("set1_desc").toString());
assertEquals(expected.toString(), ((LinkedHashMap<?, ?>) actual).get("set2").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>) actual).get("set3").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>) actual).get("set3_asc").toString());
assertEquals(expectedDesc.toString(), ((LinkedHashMap<?, ?>) actual).get("set3_desc").toString());
}
@Test
@ -1002,9 +1003,9 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
List<Object> listOrigin = new ArrayList<>(List.of("C", "B", "A", 34567, "B", "C", "hello", 34));
Set<Object> expectedSet = new LinkedHashSet<>(listOrigin);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expectedSet.toString(), ((LinkedHashMap<?, ?>)actual).get("set1").toString());
assertEquals(true, ((LinkedHashMap<?, ?>)actual).get("result1"));
assertEquals(false, ((LinkedHashMap<?, ?>)actual).get("result2"));
assertEquals(expectedSet.toString(), ((LinkedHashMap<?, ?>) actual).get("set1").toString());
assertEquals(true, ((LinkedHashMap<?, ?>) actual).get("result1"));
assertEquals(false, ((LinkedHashMap<?, ?>) actual).get("result2"));
}
@Test
@ -1025,9 +1026,9 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
Set<Object> expectedSet = new LinkedHashSet<>(listOrigin);
List<Object> expectedToList = new ArrayList<>(expectedSet);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(listOrigin.toString(), ((LinkedHashMap<?, ?>)actual).get("list").toString());
assertEquals(expectedSet.toString(), ((LinkedHashMap<?, ?>)actual).get("set1").toString());
assertEquals(expectedToList.toString(), ((LinkedHashMap<?, ?>)actual).get("tolist").toString());
assertEquals(listOrigin.toString(), ((LinkedHashMap<?, ?>) actual).get("list").toString());
assertEquals(expectedSet.toString(), ((LinkedHashMap<?, ?>) actual).get("set1").toString());
assertEquals(expectedToList.toString(), ((LinkedHashMap<?, ?>) actual).get("tolist").toString());
}
@Test
@ -1713,7 +1714,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
assertEquals(expected, actual);
}
@Test
@RepeatedTest(value = 3, name = "{displayName} {currentRepetition}/{totalRepetitions}")
public void parseBytes_Test() throws ExecutionException, InterruptedException {
byte[] bytesExecutionArrayList = new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD};
msgStr = "{}";
@ -2821,5 +2822,6 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
}
return list;
}
}

54
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java

@ -140,45 +140,63 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
}
private void startDownloading() {
long delay = 500;
// Step 1: state = 1
scheduler.schedule(() -> {
try {
state.set(1);
fireResourceChange(3);
Thread.sleep(100);
state.set(2);
fireResourceChange(3);
} catch (Exception e) {
}
}, 100, TimeUnit.MILLISECONDS);
state.set(1); // DOWNLOADING
fireResourceChange(3);
log.info("Downloading started: state=[{}]", state.get());
}, delay, TimeUnit.MILLISECONDS); // 500 ms
delay += 1000; // next step after 100 ms
// Step 2: state = 2
scheduler.schedule(() -> {
state.set(2); // DOWNLOADED
fireResourceChange(3);
log.info("Downloading in progress: state=[{}]", state.get());
}, delay, TimeUnit.MILLISECONDS); // 1500 ms
}
private void startUpdating(LwM2mServer identity) {
scheduler.schedule(() -> {
try {
// Update state + result
state.set(3);
fireResourceChange(3);
Thread.sleep(100);
updateResult.set(1);
fireResourceChange(5);
this.pkgName = TITLE;
fireResourceChange(6);
this.pkgVersion = TARGET_FW_VERSION;
fireResourceChange(7);
if (this.leshanClient != null) {
log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.stop(false);
log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.start();
this.pkgName = this.pkgNameDef;
this.pkgVersion = this.pkgVersionDef;
// Delayed reset pkgName/pkgVersion, after reboot + registration
// If a client stops, ThingsBoard can mark it as Offline.
// A new registration may take a few seconds.
scheduler.schedule(() -> {
this.pkgName = this.pkgNameDef;
fireResourceChange(6);
this.pkgVersion = this.pkgVersionDef;
fireResourceChange(7);
log.info("FW resources updating to new values: pkgName=[{}], pkgVersion=[{}]",
this.pkgName, this.pkgVersion);
}, 5, TimeUnit.SECONDS); // 5 sec — safe timing
}
} catch (Exception e) {
log.error("Error during firmware update", e);
}
}, 100, TimeUnit.MILLISECONDS);
}, 0, TimeUnit.SECONDS); // start immediately, without further delay
}
protected void setLeshanClient(LeshanClient leshanClient) {
this.leshanClient = leshanClient;
}
}

60
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java

@ -33,6 +33,8 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.controller.AbstractWebTest.TIMEOUT;
@Slf4j
public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
@ -85,10 +87,7 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId);
switch (resourceId) {
case 2:
startDownloading();
return WriteResponse.success();
case 3:
case 2, 3:
startDownloading();
return WriteResponse.success();
default:
@ -123,25 +122,34 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
}
private void startDownloading() {
long delay = 0;
// Step 1: start downloading
scheduler.schedule(() -> {
try {
state.set(1);
updateResult.set(1);
fireResourceChange(7);
fireResourceChange(9);
Thread.sleep(100);
state.set(2);
fireResourceChange(7);
Thread.sleep(100);
state.set(3);
fireResourceChange(7);
Thread.sleep(100);
updateResult.set(3);
fireResourceChange(9);
} catch (Exception e) {
}
}, 100, TimeUnit.MILLISECONDS);
state.set(1);
updateResult.set(1);
fireResourceChange(7);
fireResourceChange(9);
}, delay, TimeUnit.MILLISECONDS);
delay += 100;
// Step 2: downloading in progress
scheduler.schedule(() -> {
state.set(2);
fireResourceChange(7);
}, delay, TimeUnit.MILLISECONDS);
delay += 100;
// Step 3: downloading finished
scheduler.schedule(() -> {
state.set(3);
fireResourceChange(7);
updateResult.set(3);
fireResourceChange(9);
}, delay, TimeUnit.MILLISECONDS);
}
private void startUpdating() {
@ -150,7 +158,13 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
updateResult.set(2);
fireResourceChange(7);
fireResourceChange(9);
// Optional: delayed log about FW update
scheduler.schedule(() -> {
log.info("FW resources updating to new values: state=[{}], updateResult=[{}]",
state.get(), updateResult.get());
}, 500, TimeUnit.MILLISECONDS);
}, 100, TimeUnit.MILLISECONDS);
}
}

19
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java

@ -196,12 +196,23 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
}
protected boolean predicateForStatuses(List<TsKvEntry> ts) {
List<OtaPackageUpdateStatus> statuses = ts.stream()
if (ts == null || ts.isEmpty()) return false;
List<String> statuses = ts.stream()
.sorted(Comparator.comparingLong(TsKvEntry::getTs))
.map(KvEntry::getValueAsString)
.map(OtaPackageUpdateStatus::valueOf)
.collect(Collectors.toList());
log.warn("{}", statuses);
return statuses.containsAll(expectedStatuses);
// If we haven't received UPDATED yet
if (!statuses.contains(OtaPackageUpdateStatus.UPDATED.name())) {
return false;
}
log.warn("Captured statuses: {}", statuses);
// Перевірка, що всі очікувані статуси хоча б раз промайнули
return expectedStatuses.stream()
.map(Enum::name)
.allMatch(statuses::contains);
}
}

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java

@ -103,8 +103,10 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, UPDATING, UPDATED);
List<TsKvEntry> ts = await("await on timeseries for FW")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.atMost(60, TimeUnit.SECONDS) // Increase the timeout to 60 sec
.pollInterval(1, TimeUnit.SECONDS) // Poll once per second
.until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "fw_state"), this::predicateForStatuses);
log.warn("Object5: Got the ts: {}", ts);
}
}

15
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java

@ -158,6 +158,20 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
clientCredentials.setKey(keyPsk);
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
// Wait for the profile to become available. This synchronizes asynchronous processes and prevents TenantNotFoundException during tearDown.
org.awaitility.Awaitility.await()
.atMost(10, java.util.concurrent.TimeUnit.SECONDS)
.pollInterval(200, java.util.concurrent.TimeUnit.MILLISECONDS)
.until(() -> {
try {
String url = "/api/deviceProfile/" + deviceProfile.getId().getId().toString();
return doGet(url, DeviceProfile.class) != null;
} catch (Exception e) {
return false;
}
});
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint, deviceProfile.getId());
assertEquals(HttpServletResponse.SC_BAD_REQUEST, result.getResponse().getStatus());
@ -165,7 +179,6 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
assertTrue(result.getResponse().getContentAsString().contains(msgExpected));
}
// Bootstrap + Lwm2m
@Test
public void testWithPskConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception {

9
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/rpc/MqttV5CloseTransportSessionOnRpcDeliveryTimeoutIntegrationTest.java

@ -42,6 +42,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC;
@ -97,7 +98,13 @@ public class MqttV5CloseTransportSessionOnRpcDeliveryTimeoutIntegrationTest exte
callback.getDisconnectLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(callback.getReturnCode()).isEqualTo(MqttReturnCode.RETURN_CODE_ADMINISTRITIVE_ACTION);
Rpc persistedRpc = doGet("/api/rpc/persistent/" + response.get("rpcId").asText(), Rpc.class);
// The server re-queues the RPC asynchronously after closing the session.
// Poll until the status transitions from SENT to QUEUED.
String rpcId = response.get("rpcId").asText();
Rpc persistedRpc = await("RPC re-queued after session close")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> doGet("/api/rpc/persistent/" + rpcId, Rpc.class),
rpc -> RpcStatus.QUEUED.equals(rpc.getStatus()));
assertThat(persistedRpc).isNotNull();
assertThat(persistedRpc.getStatus()).isEqualTo(RpcStatus.QUEUED);
assertThat(persistedRpc.getResponse()).isInstanceOf(NullNode.class);

15
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java

@ -159,14 +159,17 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
protected List<Device> connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices, long ts) throws Exception {
List<Device> devices = new ArrayList<>();
clientWithCorrectNodeAccessTokenWithNDEATH();
MetricDataType metricDataType = Int32;
String key = "Node Metric int32";
String keyInt = "Node Metric int32";
int valueDeviceInt32 = 1024;
SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType, -1L);
SparkplugBProto.Payload.Metric metricInt = createMetric(valueDeviceInt32, ts, keyInt, Int32, -1L);
String keyStringEmpty = "Node Metric String Empty";
String valueDeviceStringEmpty = "";
SparkplugBProto.Payload.Metric metricStringEmpty = createMetric(valueDeviceStringEmpty, ts, keyStringEmpty, MetricDataType.String, -1L);
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts)
.setSeq(getBdSeqNum());
payloadBirthNode.addMetrics(metric);
payloadBirthNode.addMetrics(metricInt);
payloadBirthNode.addMetrics(metricStringEmpty);
payloadBirthNode.setTimestamp(ts);
if (client.isConnected()) {
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode,
@ -174,14 +177,14 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
}
valueDeviceInt32 = 4024;
metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, -1L);
metricInt = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, -1L);
for (int i = 0; i < cntDevices; i++) {
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts)
.setSeq(getSeqNum());
String deviceName = deviceId + "_" + i;
payloadBirthDevice.addMetrics(metric);
payloadBirthDevice.addMetrics(metricInt);
if (client.isConnected()) {
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName,
payloadBirthDevice.build().toByteArray(), 0, false);

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

32
common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/ResultSetSizeLimitExceededException.java

@ -0,0 +1,32 @@
/**
* 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.dao.nosql;
import lombok.Getter;
@Getter
public class ResultSetSizeLimitExceededException extends IllegalArgumentException {
private final long limitBytes;
private final long actualBytes;
public ResultSetSizeLimitExceededException(long limitBytes, long actualBytes) {
super("Result set size exceeds the maximum allowed limit. Please narrow your query");
this.limitBytes = limitBytes;
this.actualBytes = actualBytes;
}
}

24
common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java

@ -34,6 +34,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
public class TbResultSet implements AsyncResultSet {
@ -89,9 +90,14 @@ public class TbResultSet implements AsyncResultSet {
}
public ListenableFuture<List<Row>> allRows(Executor executor) {
return allRows(executor, 0);
}
public ListenableFuture<List<Row>> allRows(Executor executor, long maxResultSetSizeBytes) {
List<Row> allRows = new ArrayList<>();
SettableFuture<List<Row>> resultFuture = SettableFuture.create();
this.processRows(originalStatement, delegate, allRows, resultFuture, executor);
AtomicLong accumulatedBytes = new AtomicLong(0);
this.processRows(originalStatement, delegate, allRows, resultFuture, executor, maxResultSetSizeBytes, accumulatedBytes);
return resultFuture;
}
@ -99,7 +105,19 @@ public class TbResultSet implements AsyncResultSet {
AsyncResultSet resultSet,
List<Row> allRows,
SettableFuture<List<Row>> resultFuture,
Executor executor) {
Executor executor,
long maxResultSetSizeBytes,
AtomicLong accumulatedBytes) {
if (maxResultSetSizeBytes > 0) {
int pageSizeInBytes = resultSet.getExecutionInfo().getResponseSizeInBytes();
if (pageSizeInBytes > 0) {
accumulatedBytes.addAndGet(pageSizeInBytes);
}
if (accumulatedBytes.get() > maxResultSetSizeBytes) {
resultFuture.setException(new ResultSetSizeLimitExceededException(maxResultSetSizeBytes, accumulatedBytes.get()));
return;
}
}
allRows.addAll(loadRows(resultSet));
if (resultSet.hasMorePages()) {
ByteBuffer nextPagingState = resultSet.getExecutionInfo().getPagingState();
@ -110,7 +128,7 @@ public class TbResultSet implements AsyncResultSet {
@Override
public void onSuccess(@Nullable TbResultSet result) {
processRows(nextStatement, result,
allRows, resultFuture, executor);
allRows, resultFuture, executor, maxResultSetSizeBytes, accumulatedBytes);
}
@Override

4
common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java

@ -18,6 +18,8 @@ package org.thingsboard.server.dao.settings;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.AdminSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.EntityDaoService;
public interface AdminSettingsService extends EntityDaoService {
@ -28,6 +30,8 @@ public interface AdminSettingsService extends EntityDaoService {
AdminSettings findAdminSettingsByTenantIdAndKey(TenantId tenantId, String key);
PageData<AdminSettings> findAllByTenantId(TenantId tenantId, PageLink pageLink);
AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings);
boolean deleteAdminSettingsByTenantIdAndKey(TenantId tenantId, String key);

140
common/dao-api/src/test/java/org/thingsboard/server/dao/nosql/TbResultSetTest.java

@ -0,0 +1,140 @@
/**
* 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.dao.nosql;
import com.datastax.oss.driver.api.core.cql.AsyncResultSet;
import com.datastax.oss.driver.api.core.cql.ColumnDefinitions;
import com.datastax.oss.driver.api.core.cql.ExecutionInfo;
import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.cql.Statement;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class TbResultSetTest {
@Test
void allRows_withinLimit_returnsAllRows() throws Exception {
Row row = mock(Row.class);
AsyncResultSet asyncResultSet = createMockResultSet(List.of(row), false, 1000);
Statement<?> statement = mock(Statement.class);
TbResultSet tbResultSet = new TbResultSet(statement, asyncResultSet, s -> null);
ListenableFuture<List<Row>> future = tbResultSet.allRows(MoreExecutors.directExecutor(), 5000);
List<Row> result = future.get();
assertThat(result).hasSize(1);
assertThat(result.get(0)).isSameAs(row);
}
@Test
void allRows_exceedsLimitOnFirstPage_failsWithException() {
Row row = mock(Row.class);
AsyncResultSet asyncResultSet = createMockResultSet(List.of(row), false, 6000);
Statement<?> statement = mock(Statement.class);
TbResultSet tbResultSet = new TbResultSet(statement, asyncResultSet, s -> null);
ListenableFuture<List<Row>> future = tbResultSet.allRows(MoreExecutors.directExecutor(), 5000);
assertThatThrownBy(future::get)
.isInstanceOf(ExecutionException.class)
.hasCauseInstanceOf(ResultSetSizeLimitExceededException.class);
}
@Test
void allRows_exceedsLimitOnSecondPage_failsAfterSecondPage() {
Row row1 = mock(Row.class);
Row row2 = mock(Row.class);
Statement<?> statement = mock(Statement.class);
doReturn(statement).when(statement).setPagingState((ByteBuffer) null);
AsyncResultSet page2 = createMockResultSet(List.of(row2), false, 3000);
TbResultSet tbResultSetPage2 = new TbResultSet(statement, page2, s -> null);
SettableFuture<TbResultSet> page2Future = SettableFuture.create();
page2Future.set(tbResultSetPage2);
TbResultSetFuture tbPage2Future = new TbResultSetFuture(page2Future);
ExecutionInfo page1ExecInfo = mock(ExecutionInfo.class);
when(page1ExecInfo.getResponseSizeInBytes()).thenReturn(3000);
when(page1ExecInfo.getPagingState()).thenReturn(null);
AsyncResultSet page1 = createMockResultSet(List.of(row1), true, 3000);
when(page1.getExecutionInfo()).thenReturn(page1ExecInfo);
Function<Statement, TbResultSetFuture> executeAsync = s -> tbPage2Future;
TbResultSet tbResultSet = new TbResultSet(statement, page1, executeAsync);
ListenableFuture<List<Row>> future = tbResultSet.allRows(MoreExecutors.directExecutor(), 5000);
assertThatThrownBy(future::get)
.isInstanceOf(ExecutionException.class)
.hasCauseInstanceOf(ResultSetSizeLimitExceededException.class);
}
@Test
void allRows_unlimitedWithZero_returnsAllRowsRegardlessOfSize() throws Exception {
Row row = mock(Row.class);
AsyncResultSet asyncResultSet = createMockResultSet(List.of(row), false, 999999);
Statement<?> statement = mock(Statement.class);
TbResultSet tbResultSet = new TbResultSet(statement, asyncResultSet, s -> null);
ListenableFuture<List<Row>> future = tbResultSet.allRows(MoreExecutors.directExecutor(), 0);
List<Row> result = future.get();
assertThat(result).hasSize(1);
}
@Test
void allRows_noLimitOverload_returnsAllRows() throws Exception {
Row row = mock(Row.class);
AsyncResultSet asyncResultSet = createMockResultSet(List.of(row), false, 999999);
Statement<?> statement = mock(Statement.class);
TbResultSet tbResultSet = new TbResultSet(statement, asyncResultSet, s -> null);
ListenableFuture<List<Row>> future = tbResultSet.allRows(MoreExecutors.directExecutor());
List<Row> result = future.get();
assertThat(result).hasSize(1);
}
private AsyncResultSet createMockResultSet(List<Row> rows, boolean hasMorePages, int responseSizeInBytes) {
AsyncResultSet resultSet = mock(AsyncResultSet.class);
ExecutionInfo executionInfo = mock(ExecutionInfo.class);
ColumnDefinitions columnDefs = mock(ColumnDefinitions.class);
when(executionInfo.getResponseSizeInBytes()).thenReturn(responseSizeInBytes);
when(executionInfo.getPagingState()).thenReturn(null);
when(resultSet.getExecutionInfo()).thenReturn(executionInfo);
when(resultSet.getColumnDefinitions()).thenReturn(columnDefs);
when(resultSet.currentPage()).thenReturn(rows);
when(resultSet.hasMorePages()).thenReturn(hasMorePages);
when(resultSet.remaining()).thenReturn(rows.size());
return resultSet;
}
}

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java

@ -38,7 +38,7 @@ public enum EdgeEventType {
TENANT_PROFILE(true, EntityType.TENANT_PROFILE),
WIDGETS_BUNDLE(true, EntityType.WIDGETS_BUNDLE),
WIDGET_TYPE(true, EntityType.WIDGET_TYPE),
ADMIN_SETTINGS(true, null),
ADMIN_SETTINGS(true, EntityType.ADMIN_SETTINGS),
OTA_PACKAGE(true, EntityType.OTA_PACKAGE),
QUEUE(true, EntityType.QUEUE),
NOTIFICATION_RULE(true, EntityType.NOTIFICATION_RULE),

1
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -114,6 +114,7 @@ public class EntityIdFactory {
case DOMAIN -> new DomainId(uuid);
case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case AI_MODEL -> new AiModelId(uuid);
case ADMIN_SETTINGS -> new AdminSettingsId(uuid);
default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
};
}

2
common/discovery-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/edge-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

4
common/edge-api/src/main/proto/edge.proto

@ -46,9 +46,11 @@ enum EdgeVersion {
V_4_2_0 = 12;
V_4_3_0 = 13;
V_4_2_1_2 = 14;
V_4_2_2 = 4220;
V_4_3_0_1 = 15;
V_4_3_1 = 4310;
V_LATEST = 999;
V_LATEST = 99999;
}
/**

2
common/edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

9
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,
@ -263,6 +262,10 @@ public final class TbMsg implements Serializable {
ctx.push(ruleChainId, ruleNodeId);
}
public boolean isAlreadyInStack(RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return ctx.isAlreadyInStack(ruleChainId, ruleNodeId);
}
public TbMsgProcessingStackItem popFormStack() {
return ctx.pop();
}

12
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java

@ -63,6 +63,18 @@ public final class TbMsgProcessingCtx implements Serializable {
stack.add(new TbMsgProcessingStackItem(ruleChainId, ruleNodeId));
}
public boolean isAlreadyInStack(RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
if (stack == null || stack.isEmpty()) {
return false;
}
for (TbMsgProcessingStackItem item : stack) {
if (ruleChainId.equals(item.getRuleChainId()) && ruleNodeId.equals(item.getRuleNodeId())) {
return true;
}
}
return false;
}
public TbMsgProcessingStackItem pop() {
if (stack == null || stack.isEmpty()) {
return null;

89
common/message/src/test/java/org/thingsboard/server/common/msg/TbMsgProcessingCtxTest.java

@ -0,0 +1,89 @@
/**
* 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.common.msg;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
class TbMsgProcessingCtxTest {
private final RuleChainId RULE_CHAIN_ID = new RuleChainId(UUID.fromString("b87c4123-f9f2-41a6-9a09-e3a5b6580b11"));
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("1ca5e2ef-1309-41d9-bafa-709e9df0e2a6"));
@Test
void givenEmptyStack_whenIsAlreadyInStack_thenReturnFalse() {
TbMsgProcessingCtx ctx = new TbMsgProcessingCtx();
assertThat(ctx.isAlreadyInStack(RULE_CHAIN_ID, RULE_NODE_ID)).isFalse();
}
@Test
void givenStackWithDifferentEntry_whenIsAlreadyInStack_thenReturnFalse() {
TbMsgProcessingCtx ctx = new TbMsgProcessingCtx();
ctx.push(new RuleChainId(UUID.randomUUID()), new RuleNodeId(UUID.randomUUID()));
assertThat(ctx.isAlreadyInStack(RULE_CHAIN_ID, RULE_NODE_ID)).isFalse();
}
@Test
void givenStackWithMatchingEntry_whenIsAlreadyInStack_thenReturnTrue() {
TbMsgProcessingCtx ctx = new TbMsgProcessingCtx();
ctx.push(RULE_CHAIN_ID, RULE_NODE_ID);
assertThat(ctx.isAlreadyInStack(RULE_CHAIN_ID, RULE_NODE_ID)).isTrue();
}
@Test
void givenStackWithMatchingEntryAmongOthers_whenIsAlreadyInStack_thenReturnTrue() {
TbMsgProcessingCtx ctx = new TbMsgProcessingCtx();
ctx.push(new RuleChainId(UUID.randomUUID()), new RuleNodeId(UUID.randomUUID()));
ctx.push(RULE_CHAIN_ID, RULE_NODE_ID);
ctx.push(new RuleChainId(UUID.randomUUID()), new RuleNodeId(UUID.randomUUID()));
assertThat(ctx.isAlreadyInStack(RULE_CHAIN_ID, RULE_NODE_ID)).isTrue();
}
@Test
void givenStackWithSameChainButDifferentNode_whenIsAlreadyInStack_thenReturnFalse() {
TbMsgProcessingCtx ctx = new TbMsgProcessingCtx();
ctx.push(RULE_CHAIN_ID, new RuleNodeId(UUID.randomUUID()));
assertThat(ctx.isAlreadyInStack(RULE_CHAIN_ID, RULE_NODE_ID)).isFalse();
}
@Test
void givenStackWithSameNodeButDifferentChain_whenIsAlreadyInStack_thenReturnFalse() {
TbMsgProcessingCtx ctx = new TbMsgProcessingCtx();
ctx.push(new RuleChainId(UUID.randomUUID()), RULE_NODE_ID);
assertThat(ctx.isAlreadyInStack(RULE_CHAIN_ID, RULE_NODE_ID)).isFalse();
}
@Test
void givenStackWithEntryThenPopped_whenIsAlreadyInStack_thenReturnFalse() {
TbMsgProcessingCtx ctx = new TbMsgProcessingCtx();
ctx.push(RULE_CHAIN_ID, RULE_NODE_ID);
ctx.pop();
assertThat(ctx.isAlreadyInStack(RULE_CHAIN_ID, RULE_NODE_ID)).isFalse();
}
}

4
common/message/src/test/java/org/thingsboard/server/common/msg/tools/TbRateLimitsTest.java

@ -42,7 +42,7 @@ public class TbRateLimitsTest {
assertThat(rateLimits.tryConsume()).as("new token is available").isFalse();
int expectedRefillTime = (int) (((double) period / capacity) * 1000);
int gap = 500;
int gap = 1000;
for (int i = 0; i < capacity; i++) {
await("token refill for rate limit " + rateLimitConfig)
@ -71,7 +71,7 @@ public class TbRateLimitsTest {
assertThat(rateLimits.tryConsume()).as("new token is available").isFalse();
int expectedRefillTime = period * 1000;
int gap = 500;
int gap = 1000;
await("tokens refill for rate limit " + rateLimitConfig)
.pollInterval(new FixedPollInterval(10, TimeUnit.MILLISECONDS))

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>common</artifactId>

2
common/proto/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

4
common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java

@ -156,11 +156,7 @@ public class ProtoConverter {
case BOOLEAN_V:
case LONG_V:
case DOUBLE_V:
break;
case STRING_V:
if (StringUtils.isEmpty(keyValueProto.getStringV())) {
throw new IllegalArgumentException("Value is empty for key: " + key + "!");
}
break;
case JSON_V:
try {

2
common/queue/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/remote-js-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/script/script-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

8
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -898,11 +898,11 @@ public class TbUtils {
public static int parseBytesToInt(byte[] data, int offset, int length, boolean bigEndian) {
validationNumberByLength(data, offset, length, BYTES_LEN_INT_MAX);
var bb = ByteBuffer.allocate(4);
var bb = ByteBuffer.allocate(BYTES_LEN_INT_MAX);
if (!bigEndian) {
bb.order(ByteOrder.LITTLE_ENDIAN);
}
bb.position(bigEndian ? 4 - length : 0);
bb.position(bigEndian ? BYTES_LEN_INT_MAX - length : 0);
bb.put(data, offset, length);
bb.position(0);
return bb.getInt();
@ -923,11 +923,11 @@ public class TbUtils {
public static long parseBytesToUnsignedInt(byte[] data, int offset, int length, boolean bigEndian) {
validationNumberByLength(data, offset, length, BYTES_LEN_INT_MAX);
ByteBuffer bb = ByteBuffer.allocate(8);
ByteBuffer bb = ByteBuffer.allocate(BYTES_LEN_LONG_MAX);
if (!bigEndian) {
bb.order(ByteOrder.LITTLE_ENDIAN);
}
bb.position(bigEndian ? 8 - length : 0);
bb.position(bigEndian ? BYTES_LEN_LONG_MAX - length : 0);
bb.put(data, offset, length);
bb.position(0);

95
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -155,6 +155,101 @@ public class TbUtilsTest {
Assertions.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, false));
}
@Test
public void parseBytesToInt_doesNotChangeInputData() {
byte[] data = new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD};
byte[] copy = data.clone();
TbUtils.parseBytesToInt(data, 0, 4, true);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToInt(data, 0, 4, false);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToUnsignedInt(data, 0, 4, true);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToUnsignedInt(data, 0, 4, false);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToLong(data, 0, 4, true);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToLong(data, 0, 4, false);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToFloat(data, 0, 4, true);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToFloat(data, 0, 4, false);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesIntToFloat(data, 0, 4, true);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesIntToFloat(data, 0, 4, false);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToDouble(data, 0, 4, true);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesToDouble(data, 0, 4, false);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesLongToDouble(data, 0, 4, true);
Assertions.assertArrayEquals(copy, data);
TbUtils.parseBytesLongToDouble(data, 0, 4, false);
Assertions.assertArrayEquals(copy, data);
List<Byte> listData = toList(new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD});
List<Byte> listCopy = new ArrayList<>(listData);
TbUtils.parseBytesToInt(listData, 0, 4, true);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToInt(listData, 0, 4, false);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToUnsignedInt(listData, 0, 4, true);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToUnsignedInt(listData, 0, 4, false);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToLong(listData, 0, 4, true);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToLong(listData, 0, 4, false);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToFloat(listData, 0, 4, true);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToFloat(listData, 0, 4, false);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesIntToFloat(listData, 0, 4, true);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesIntToFloat(listData, 0, 4, false);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToDouble(listData, 0, 4, true);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesToDouble(listData, 0, 4, false);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesLongToDouble(listData, 0, 4, true);
Assertions.assertEquals(listCopy, listData);
TbUtils.parseBytesLongToDouble(listData, 0, 4, false);
Assertions.assertEquals(listCopy, listData);
}
@Test
public void compare_parseBytesToInt_and_parseBytesToUnsignedInt() {
byte[] data = new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF};
// 4 bytes: parseBytesToInt returns -1, parseBytesToUnsignedInt returns 4294967295L
Assertions.assertEquals(-1, TbUtils.parseBytesToInt(data, 0, 4, true));
Assertions.assertEquals(4294967295L, TbUtils.parseBytesToUnsignedInt(data, 0, 4, true));
// 2 bytes (0xFFFF): both return 65535 (no sign extension for parseBytesToInt when length < 4)
Assertions.assertEquals(65535, TbUtils.parseBytesToInt(data, 0, 2, true));
Assertions.assertEquals(65535L, TbUtils.parseBytesToUnsignedInt(data, 0, 2, true));
// 2 bytes with high bit set (0x8000)
byte[] data2 = new byte[]{(byte) 0x80, (byte) 0x00};
Assertions.assertEquals(32768, TbUtils.parseBytesToInt(data2, 0, 2, true));
Assertions.assertEquals(32768L, TbUtils.parseBytesToUnsignedInt(data2, 0, 2, true));
}
@Test
public void toFlatMap() {
ExecutionHashMap<String, Object> inputMap = new ExecutionHashMap<>(16, ctx);

2
common/stats/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
common/transport/transport-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/util/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

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;
}
}
}
}

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

@ -0,0 +1,338 @@
/**
* 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;
@ResourceLock("SsrfProtectionValidatorTest") // some tests mutate static additional-blocked-hosts
public class 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
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
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
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
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
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
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
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
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());
}
}
}

2
common/version-control/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

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

@ -18,6 +18,7 @@ package org.thingsboard.server.service.sync;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.revwalk.RevCommit;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors;
@ -47,6 +48,8 @@ public class DefaultGitSyncService implements GitSyncService {
private final Map<String, GitRepository> repositories = new ConcurrentHashMap<>();
private final Map<String, Runnable> updateListeners = new ConcurrentHashMap<>();
private final Map<String, RevCommit> lastCommits = new ConcurrentHashMap<>();
@Override
public void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate) {
RepositorySettings settings = new RepositorySettings();
@ -84,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(getBranchRef(repository), path, depth).stream()
return repository.listFilesAtCommit(getLastCommit(key), path, depth).stream()
.filter(file -> type == null || file.type() == type)
.toList();
}
@ -93,7 +96,7 @@ public class DefaultGitSyncService implements GitSyncService {
@Override
public byte[] getFileContent(String key, String path) {
GitRepository repository = getRepository(key);
return repository.getFileContentAtCommit(path, getBranchRef(repository));
return repository.getFileContentAtCommit(path, getLastCommit(key));
}
@Override
@ -137,6 +140,15 @@ public class DefaultGitSyncService implements GitSyncService {
}
private void onUpdate(String key) {
GitRepository repository = getRepository(key);
String branchRef = getBranchRef(repository);
try {
lastCommits.put(key, repository.resolveCommit(branchRef));
} catch (Throwable e) {
log.error("[{}] Failed to resolve commit for ref {}", key, branchRef, e);
return;
}
Runnable listener = updateListeners.get(key);
if (listener != null) {
log.debug("[{}] Handling repository update", key);
@ -154,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();
}

35
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -277,13 +277,17 @@ public class GitRepository {
return listFilesAtCommit(commitId, path, -1).stream().map(RepoFile::path).toList();
}
@SneakyThrows
public List<RepoFile> listFilesAtCommit(String commitId, String path, int depth) {
log.debug("Executing listFilesAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commitId, path);
RevCommit commit = resolveCommit(commitId);
return listFilesAtCommit(commit, path, depth);
}
@SneakyThrows
public List<RepoFile> listFilesAtCommit(RevCommit commit, String path, int depth) {
log.debug("Executing listFilesAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commit, path);
List<RepoFile> files = new ArrayList<>();
RevCommit revCommit = resolveCommit(commitId);
try (TreeWalk treeWalk = new TreeWalk(git.getRepository())) {
treeWalk.reset(revCommit.getTree().getId());
treeWalk.reset(commit.getTree().getId());
if (StringUtils.isNotEmpty(path)) {
treeWalk.setFilter(PathFilter.create(path));
}
@ -301,11 +305,15 @@ public class GitRepository {
return files;
}
@SneakyThrows
public byte[] getFileContentAtCommit(String file, String commitId) {
log.debug("Executing getFileContentAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commitId, file);
RevCommit revCommit = resolveCommit(commitId);
try (TreeWalk treeWalk = TreeWalk.forPath(git.getRepository(), file, revCommit.getTree())) {
RevCommit commit = resolveCommit(commitId);
return getFileContentAtCommit(file, commit);
}
@SneakyThrows
public byte[] getFileContentAtCommit(String file, RevCommit commit) {
log.debug("Executing getFileContentAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commit, file);
try (TreeWalk treeWalk = TreeWalk.forPath(git.getRepository(), file, commit.getTree())) {
if (treeWalk == null) {
throw new IllegalArgumentException("File not found");
}
@ -373,9 +381,9 @@ public class GitRepository {
for (RemoteRefUpdate update : pushResult.getRemoteUpdates()) {
RemoteRefUpdate.Status status = update.getStatus();
if (status == REJECTED_NONFASTFORWARD || status == REJECTED_NODELETE ||
status == REJECTED_REMOTE_CHANGED || status == REJECTED_OTHER_REASON) {
status == REJECTED_REMOTE_CHANGED || status == REJECTED_OTHER_REASON) {
throw new RuntimeException("Remote repository answered with error: " +
Optional.ofNullable(update.getMessage()).orElseGet(status::name));
Optional.ofNullable(update.getMessage()).orElseGet(status::name));
}
}
});
@ -450,7 +458,8 @@ public class GitRepository {
revCommit.getFullMessage(), revCommit.getAuthorIdent().getName(), revCommit.getAuthorIdent().getEmailAddress());
}
private RevCommit resolveCommit(String id) throws IOException {
@SneakyThrows
public RevCommit resolveCommit(String id) {
return git.getRepository().parseCommit(resolve(id));
}
@ -481,8 +490,8 @@ public class GitRepository {
private static final Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> {
SortOrder sortOrder = pageLink.getSortOrder();
if (sortOrder != null
&& sortOrder.getProperty().equals("timestamp")
&& SortOrder.Direction.ASC.equals(sortOrder.getDirection())) {
&& sortOrder.getProperty().equals("timestamp")
&& SortOrder.Direction.ASC.equals(sortOrder.getDirection())) {
return Comparator.comparingInt(RevCommit::getCommitTime);
}
return null;

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>dao</artifactId>

3
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java

@ -37,6 +37,9 @@ public abstract class CassandraAbstractAsyncDao extends CassandraAbstractDao {
@Value("${cassandra.query.result_processing_threads:50}")
private int threadPoolSize;
@Value("${cassandra.query.max_result_set_size_in_bytes:52428800}")
protected long maxResultSetSizeBytes;
@PostConstruct
public void startExecutor() {
readResultsProcessingExecutor = ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "cassandra-callback");

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!");

4
dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java

@ -17,6 +17,8 @@ package org.thingsboard.server.dao.settings;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import java.util.UUID;
@ -27,6 +29,8 @@ public interface AdminSettingsDao extends Dao<AdminSettings> {
AdminSettings findByTenantIdAndKey(UUID tenantId, String key);
PageData<AdminSettings> findAllByTenantId(TenantId tenantId, PageLink pageLink);
boolean removeByTenantIdAndKey(UUID tenantId, String key);
void removeByTenantId(UUID tenantId);

19
dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.EntityType;
@ -27,6 +28,9 @@ import org.thingsboard.server.common.data.id.AdminSettingsId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.Validator;
@ -44,6 +48,9 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
@Autowired
private DataValidator<AdminSettings> adminSettingsValidator;
@Autowired
protected ApplicationEventPublisher eventPublisher;
@Override
public AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId) {
log.trace("Executing findAdminSettingsById [{}]", adminSettingsId);
@ -63,10 +70,15 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
return adminSettingsDao.findByTenantIdAndKey(tenantId.getId(), key);
}
@Override
public PageData<AdminSettings> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
return adminSettingsDao.findAllByTenantId(tenantId, pageLink);
}
@Override
public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) {
log.trace("Executing saveAdminSettings [{}]", adminSettings);
adminSettingsValidator.validate(adminSettings, data -> tenantId);
AdminSettings oldAdminSettings = adminSettingsValidator.validate(adminSettings, data -> tenantId);
if (adminSettings.getKey().equals("mail")) {
AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail");
if (mailSettings != null) {
@ -84,7 +96,10 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
if (adminSettings.getTenantId() == null) {
adminSettings.setTenantId(TenantId.SYS_TENANT_ID);
}
return adminSettingsDao.save(tenantId, adminSettings);
AdminSettings savedAdminSettings = adminSettingsDao.save(tenantId, adminSettings);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedAdminSettings.getTenantId()).entityId(savedAdminSettings.getId())
.entity(savedAdminSettings).oldEntity(oldAdminSettings).created(adminSettings.getId() == null).build());
return savedAdminSettings;
}
@Override

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(

92
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -52,6 +52,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.nosql.ResultSetSizeLimitExceededException;
import org.thingsboard.server.dao.nosql.TbResultSet;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao;
@ -256,7 +257,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t);
log.error("[{}][{}][{}] Failed to fetch partitions for interval {}-{}", tenantId, entityId.getEntityType(), entityId.getId(), minPartition, maxPartition, t);
resultFuture.setException(t);
}
}, readResultsProcessingExecutor);
return resultFuture;
@ -330,7 +332,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), toPartitionTs(query.getStartTs()), toPartitionTs(query.getEndTs()), t);
log.error("[{}][{}][{}] Failed to fetch partitions for interval {}-{}", tenantId, entityId.getEntityType(), entityId.getId(), toPartitionTs(query.getStartTs()), toPartitionTs(query.getEndTs()), t);
resultFuture.setException(t);
}
}, readResultsProcessingExecutor);
@ -372,8 +375,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
cursor.addData(convertResultToTsKvEntryList(Collections.emptyList()));
findAllAsyncSequentiallyWithLimit(tenantId, cursor, resultFuture);
} else {
Futures.addCallback(result.allRows(readResultsProcessingExecutor), new FutureCallback<List<Row>>() {
Futures.addCallback(result.allRows(readResultsProcessingExecutor, maxResultSetSizeBytes), new FutureCallback<List<Row>>() {
@Override
public void onSuccess(@Nullable List<Row> result) {
cursor.addData(convertResultToTsKvEntryList(result == null ? Collections.emptyList() : result));
@ -382,7 +384,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to fetch data for query {}-{}", stmt, t);
if (t instanceof ResultSetSizeLimitExceededException e) {
log.warn("[{}][{}][{}] Result set size limit exceeded for key [{}], query [{}]: {} bytes, limit {} bytes",
tenantId, cursor.getEntityType(), cursor.getEntityId(), cursor.getKey(), stmt.getPreparedStatement().getQuery(), e.getActualBytes(), e.getLimitBytes());
} else {
log.error("[{}][{}][{}] Failed to fetch data for key [{}], query [{}]", tenantId, cursor.getEntityType(), cursor.getEntityId(), cursor.getKey(), stmt.getPreparedStatement().getQuery(), t);
}
resultFuture.setException(t);
}
}, readResultsProcessingExecutor);
@ -392,7 +400,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to fetch data for query {}-{}", stmt, t);
log.error("[{}][{}][{}] Failed to fetch data for key [{}], query [{}]", tenantId, cursor.getEntityType(), cursor.getEntityId(), cursor.getKey(), stmt.getPreparedStatement().getQuery(), t);
resultFuture.setException(t);
}
}, readResultsProcessingExecutor);
}
@ -425,7 +434,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
}
if (!isUseTsKeyValuePartitioningOnRead()) {
final long estimatedPartitionCount = estimatePartitionCount(minPartition, maxPartition);
if (estimatedPartitionCount <= useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount) {
if (estimatedPartitionCount <= useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount) {
return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition, (int) estimatedPartitionCount));
}
}
@ -446,7 +455,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
}
List<Long> calculatePartitions(long minPartition, long maxPartition) {
return calculatePartitions(minPartition, maxPartition, 0);
return calculatePartitions(minPartition, maxPartition, 0);
}
List<Long> calculatePartitions(long minPartition, long maxPartition, int estimatedPartitionCount) {
@ -533,6 +542,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
public void onFailure(Throwable t) {
}
}
private long computeTtl(long ttl) {
@ -569,7 +579,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to delete data for query {}-{}", stmt, t);
log.error("[{}][{}][{}] Failed to delete data for key [{}], query [{}]", tenantId, cursor.getEntityType(), cursor.getEntityId(), cursor.getKey(), stmt.getPreparedStatement().getQuery(), t);
resultFuture.setException(t);
}
}, readResultsProcessingExecutor);
}
@ -581,12 +592,12 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
try {
if (deleteStmt == null) {
deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF +
" WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?");
" WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?");
}
} finally {
stmtCreationLock.unlock();
@ -661,13 +672,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private String getPreparedStatementQuery(DataType type) {
return INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?, ?)";
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?, ?)";
}
private String getPreparedStatementQueryWithTtl(DataType type) {
@ -680,11 +691,11 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
try {
if (partitionInsertStmt == null) {
partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?)");
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?)");
}
} finally {
stmtCreationLock.unlock();
@ -699,11 +710,11 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
try {
if (partitionInsertTtlStmt == null) {
partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?) USING TTL ?");
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?) USING TTL ?");
}
} finally {
stmtCreationLock.unlock();
@ -809,16 +820,17 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()];
} else {
fetchStmts[type.ordinal()] = prepare(SELECT_PREFIX +
String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF
+ " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?"
+ (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " " + orderBy + " LIMIT ?" : ""));
String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF
+ " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?"
+ (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " " + orderBy + " LIMIT ?" : ""));
}
}
return fetchStmts;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -184,7 +184,7 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
}
private ListenableFuture<List<TsKvEntry>> convertAsyncResultSetToTsKvEntryList(TbResultSet rs) {
return Futures.transform(rs.allRows(readResultsProcessingExecutor),
return Futures.transform(rs.allRows(readResultsProcessingExecutor, maxResultSetSizeBytes),
rows -> this.convertResultToTsKvEntryList(rows), readResultsProcessingExecutor);
}

4
dao/src/main/java/org/thingsboard/server/dao/timeseries/SimpleListenableFuture.java

@ -26,4 +26,8 @@ public class SimpleListenableFuture<V> extends AbstractFuture<V> {
return super.set(value);
}
public boolean setException(Throwable throwable) {
return super.setException(throwable);
}
}

25
dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java

@ -25,9 +25,12 @@ import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.exception.DataValidationException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@ -113,4 +116,24 @@ public class AdminSettingsServiceTest extends AbstractServiceTest {
}).hasMessageContaining("already exists");
}
@Test
public void testFindAllByTenantId() {
int pageSize = 10;
int totalElements = 100;
for (int i = 0; i < totalElements; i++) {
AdminSettings settings = new AdminSettings();
settings.setTenantId(tenantId);
String key = RandomStringUtils.randomAlphanumeric(15);
settings.setKey(key);
settings.setJsonValue(JacksonUtil.newObjectNode().put("value", i));
adminSettingsService.saveAdminSettings(tenantId, settings);
}
PageData<AdminSettings> pageData = adminSettingsService.findAllByTenantId(tenantId, new PageLink(pageSize));
assertThat(pageData.getData().size()).isEqualTo(pageSize);
assertThat(pageData.getTotalElements()).isEqualTo(totalElements);
}
}

42
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java

@ -16,7 +16,10 @@
package org.thingsboard.server.dao.service.timeseries.nosql;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
@ -27,9 +30,12 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.nosql.ResultSetSizeLimitExceededException;
import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest;
import org.thingsboard.server.dao.timeseries.CassandraBaseTimeseriesDao;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
@ -37,6 +43,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@ -44,6 +51,9 @@ import static org.junit.Assert.assertTrue;
@DaoNoSqlTest
public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest {
@Autowired
private CassandraBaseTimeseriesDao cassandraBaseTimeseriesDao;
@Test
public void shouldSaveEntryOfEachTypeWithTtl() throws ExecutionException, InterruptedException, TimeoutException {
long ttlInSec = TimeUnit.SECONDS.toSeconds(3);
@ -94,4 +104,36 @@ public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest {
double expectedValue = (doubleValue + longValue)/ 2;
assertThat(listWithAgg.get(0).getDoubleValue().get()).isEqualTo(expectedValue);
}
@Test
public void testResultSetSizeLimitExceeded() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
String value = "x".repeat(500);
List<TsKvEntry> entries = new ArrayList<>();
for (int i = 0; i < 5; i++) {
entries.add(new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(i + 1), new StringDataEntry("bigKey", value)));
}
tsService.save(tenantId, deviceId, entries, 0).get(MAX_TIMEOUT, TimeUnit.SECONDS);
long originalLimit = (long) ReflectionTestUtils.getField(cassandraBaseTimeseriesDao, "maxResultSetSizeBytes");
try {
// Set a very low limit to trigger the exception
ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "maxResultSetSizeBytes", 1024L);
assertThatThrownBy(() -> tsService.findAll(tenantId, deviceId, Collections.singletonList(
new BaseReadTsKvQuery("bigKey", 0L, TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE)
)).get(MAX_TIMEOUT, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.satisfies(e -> assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(ResultSetSizeLimitExceededException.class));
} finally {
ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "maxResultSetSizeBytes", originalLimit);
}
// Verify query succeeds with original limit restored
List<TsKvEntry> result = tsService.findAll(tenantId, deviceId, Collections.singletonList(
new BaseReadTsKvQuery("bigKey", 0L, TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE)
)).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(result).hasSize(5);
}
}

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());
}

2
edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>edqs</artifactId>

2
edqs/src/main/resources/edqs.yml

@ -210,7 +210,7 @@ management:
web:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'
include: "${METRICS_ENDPOINTS_EXPOSE:info}"
health:
elasticsearch:
# Enable the org.springframework.boot.actuate.elasticsearch.ElasticsearchRestClientHealthIndicator.doHealthCheck

2
monitoring/pom.xml

@ -21,7 +21,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>

2
msa/black-box-tests/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

7
msa/js-executor/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-js-executor",
"private": true,
"version": "4.3.0.1",
"version": "4.3.1",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.ts",
"bin": "server.js",
@ -39,7 +39,10 @@
"fs-extra": "^11.3.1",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"typescript": "5.9.2"
"typescript": "~5.9.3"
},
"resolutions": {
"@yao-pkg/pkg/tar": ">=7.5.8"
},
"pkg": {
"assets": [

15
msa/js-executor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>
@ -81,7 +81,7 @@
<goal>yarn</goal>
</goals>
<configuration>
<arguments>install --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network</arguments>
<arguments>install --non-interactive --check-files --network-concurrency 4 --network-timeout 100000 --mutex network</arguments>
</configuration>
</execution>
<execution>
@ -96,6 +96,17 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>${basedir}/node_modules</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>

44
msa/js-executor/yarn.lock

@ -1036,9 +1036,9 @@ mimic-response@^3.1.0:
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
version "3.1.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
dependencies:
brace-expansion "^1.1.7"
@ -1052,10 +1052,10 @@ minipass@^7.0.4, minipass@^7.1.2:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
minizlib@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.2.tgz#f33d638eb279f664439aa38dc5f91607468cb574"
integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==
minizlib@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c"
integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==
dependencies:
minipass "^7.1.2"
@ -1064,11 +1064,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
moment@^2.29.1:
version "2.30.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
@ -1253,9 +1248,9 @@ pump@^3.0.0:
once "^1.3.1"
qs@^6.14.0:
version "6.14.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
version "6.14.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.2.tgz#b5634cf9d9ad9898e31fba3504e866e8efb6798c"
integrity sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==
dependencies:
side-channel "^1.1.0"
@ -1553,16 +1548,15 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^7.4.3:
version "7.4.3"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571"
integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==
tar@>=7.5.8, tar@^7.4.3:
version "7.5.9"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8"
integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"
minipass "^7.1.2"
minizlib "^3.0.1"
mkdirp "^3.0.1"
minizlib "^3.1.0"
yallist "^5.0.0"
text-hex@1.0.x:
@ -1640,10 +1634,10 @@ type-is@^2.0.0, type-is@^2.0.1:
media-typer "^1.1.0"
mime-types "^3.0.0"
typescript@5.9.2:
version "5.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
typescript@~5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
undefsafe@^2.0.5:
version "2.0.5"

2
msa/monitoring/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>

4
msa/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>msa</artifactId>
@ -133,7 +133,7 @@
</property>
</activation>
<properties>
<docker.lts.tag>4.3.0-latest</docker.lts.tag>
<docker.lts.tag>4.3.1-latest</docker.lts.tag>
<docker.skip.latest.tag>false</docker.skip.latest.tag>
</properties>
<build>

2
msa/tb-node/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/tb/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0.1</version>
<version>4.3.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save