Browse Source

Merge branch 'master' into feature/monitoring-service

pull/7725/head
ViacheslavKlimov 4 years ago
parent
commit
366afca70d
  1. 8
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  2. 13
      application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json
  3. 8
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  4. 1
      application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java
  5. 4
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  6. 16
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  7. 49
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  8. 6
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  9. 28
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  10. 6
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  11. 6
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  12. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  14. 15
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  15. 4
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  16. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  17. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  18. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  19. 23
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  20. 6
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java
  21. 162
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java
  22. 69
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
  23. 39
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java
  24. 35
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java
  25. 23
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java
  26. 17
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  27. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  28. 32
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  29. 38
      application/src/main/resources/thingsboard.yml
  30. 122
      application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java
  31. 4
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  32. 114
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java
  33. 10
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  34. 68
      application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandlerTest.java
  35. 2
      application/src/test/resources/logback-test.xml
  36. 19
      common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java
  37. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java
  38. 2
      common/data/src/main/java/org/thingsboard/server/common/data/script/ScriptLanguage.java
  39. 9
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java
  40. 30
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java
  41. 6
      common/script/script-api/pom.xml
  42. 112
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  43. 4
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
  44. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbJson.java
  45. 104
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  46. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelInvokeService.java
  47. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelScript.java
  48. 8
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelScriptExecutionTask.java
  49. 11
      dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java
  50. 19
      dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java
  51. 17
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java
  52. 5
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  53. 8
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  54. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  55. 79
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  56. 3
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
  57. 39
      dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java
  58. 133
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningDaysAlwaysExistsTest.java
  59. 134
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningHoursAlwaysExistsTest.java
  60. 78
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningIndefiniteAlwaysExistsTest.java
  61. 121
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java
  62. 134
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java
  63. 116
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java
  64. 3
      docker/tb-js-executor.env
  65. 1
      lombok.config
  66. 4
      msa/black-box-tests/README.md
  67. 38
      msa/black-box-tests/pom.xml
  68. 130
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java
  69. 221
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  70. 53
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java
  71. 61
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java
  72. 262
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  73. 9
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java
  74. 112
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java
  75. 226
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  76. 220
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java
  77. 40
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java
  78. 2
      msa/black-box-tests/src/test/resources/config.properties
  79. 27
      msa/black-box-tests/src/test/resources/testNG.xml
  80. 2
      msa/js-executor/api/jsExecutor.models.ts
  81. 11
      msa/js-executor/api/jsInvokeMessageProcessor.ts
  82. 1
      msa/js-executor/config/custom-environment-variables.yml
  83. 1
      msa/js-executor/config/default.yml
  84. 2
      msa/js-executor/docker/start-js-executor.sh
  85. 4
      packaging/java/scripts/install/logback.xml
  86. 34
      pom.xml
  87. 26
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  88. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
  89. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java
  90. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
  91. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
  92. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
  93. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java
  94. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
  95. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
  96. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  97. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
  98. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  99. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
  100. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java

8
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -57,9 +57,9 @@
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"scriptLang": "MVEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {
@ -71,9 +71,9 @@
"name": "Log Other", "name": "Log Other",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"scriptLang": "MVEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {

13
application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json

@ -70,9 +70,9 @@
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"scriptLang": "MVEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {
@ -84,9 +84,9 @@
"name": "Log Other", "name": "Log Other",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"scriptLang": "MVEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {
@ -174,11 +174,6 @@
"fromIndex": 3, "fromIndex": 3,
"toIndex": 7, "toIndex": 7,
"type": "Timeseries Updated" "type": "Timeseries Updated"
},
{
"fromIndex": 4,
"toIndex": 7,
"type": "Success"
} }
], ],
"ruleChainConnections": null "ruleChainConnections": null

8
application/src/main/data/json/tenant/rule_chains/root_rule_chain.json

@ -57,9 +57,9 @@
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"scriptLang": "MVEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {
@ -71,9 +71,9 @@
"name": "Log Other", "name": "Log Other",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"scriptLang": "MVEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {

1
application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java

@ -29,6 +29,7 @@ import java.util.Arrays;
@ComponentScan({"org.thingsboard.server.install", @ComponentScan({"org.thingsboard.server.install",
"org.thingsboard.server.service.component", "org.thingsboard.server.service.component",
"org.thingsboard.server.service.install", "org.thingsboard.server.service.install",
"org.thingsboard.server.service.security.auth.jwt.settings",
"org.thingsboard.server.dao", "org.thingsboard.server.dao",
"org.thingsboard.server.common.stats", "org.thingsboard.server.common.stats",
"org.thingsboard.server.common.transport.config.ssl", "org.thingsboard.server.common.transport.config.ssl",

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

@ -33,7 +33,7 @@ import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
@ -274,7 +274,7 @@ public class ActorSystemContext {
@Autowired(required = false) @Autowired(required = false)
@Getter @Getter
private MvelInvokeService mvelInvokeService; private TbelInvokeService tbelInvokeService;
@Autowired @Autowired
@Getter @Getter

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

@ -97,7 +97,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine; import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -474,11 +474,11 @@ class DefaultTbContext implements TbContext {
return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsInvokeService(), script, argNames); return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsInvokeService(), script, argNames);
} }
private ScriptEngine createMvelScriptEngine(String script, String... argNames) { private ScriptEngine createTbelScriptEngine(String script, String... argNames) {
if (mainCtx.getMvelInvokeService() == null) { if (mainCtx.getTbelInvokeService() == null) {
throw new RuntimeException("MVEL execution is disabled!"); throw new RuntimeException("TBEL execution is disabled!");
} }
return new RuleNodeMvelScriptEngine(getTenantId(), mainCtx.getMvelInvokeService(), script, argNames); return new RuleNodeTbelScriptEngine(getTenantId(), mainCtx.getTbelInvokeService(), script, argNames);
} }
@Override @Override
@ -492,11 +492,11 @@ class DefaultTbContext implements TbContext {
switch (scriptLang) { switch (scriptLang) {
case JS: case JS:
return createJsScriptEngine(script, argNames); return createJsScriptEngine(script, argNames);
case MVEL: case TBEL:
if (Arrays.isNullOrEmpty(argNames)) { if (Arrays.isNullOrEmpty(argNames)) {
return createMvelScriptEngine(script, "msg", "metadata", "msgType"); return createTbelScriptEngine(script, "msg", "metadata", "msgType");
} else { } else {
return createMvelScriptEngine(script, argNames); return createTbelScriptEngine(script, argNames);
} }
default: default:
throw new RuntimeException("Unsupported script language: " + scriptLang.name()); throw new RuntimeException("Unsupported script language: " + scriptLang.name());

49
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -22,7 +22,9 @@ import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
@ -37,8 +39,13 @@ import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo; import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -65,6 +72,14 @@ public class AdminController extends BaseController {
@Autowired @Autowired
private SystemSecurityService systemSecurityService; private SystemSecurityService systemSecurityService;
@Lazy
@Autowired
private JwtSettingsService jwtSettingsService;
@Lazy
@Autowired
private JwtTokenFactory tokenFactory;
@Autowired @Autowired
private EntitiesVersionControlService versionControlService; private EntitiesVersionControlService versionControlService;
@ -152,6 +167,40 @@ public class AdminController extends BaseController {
} }
} }
@ApiOperation(value = "Get the JWT Settings object (getJwtSettings)",
notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.GET)
@ResponseBody
public JwtSettings getJwtSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(jwtSettingsService.getJwtSettings());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Update JWT Settings (saveJwtSettings)",
notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.POST)
@ResponseBody
public JwtPair saveJwtSettings(
@ApiParam(value = "A JSON value representing the JWT Settings.")
@RequestBody JwtSettings jwtSettings) throws ThingsboardException {
try {
SecurityUser securityUser = getCurrentUser();
accessControlService.checkPermission(securityUser, Resource.ADMIN_SETTINGS, Operation.WRITE);
checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings));
return tokenFactory.createTokenPair(securityUser);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Send test email (sendTestMail)", @ApiOperation(value = "Send test email (sendTestMail)",
notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " +
"You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH) "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH)

6
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -51,7 +51,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ActivateUserRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
import org.thingsboard.server.service.security.model.ResetPasswordRequest; import org.thingsboard.server.service.security.model.ResetPasswordRequest;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -236,7 +236,7 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/activate", method = RequestMethod.POST) @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
@ResponseBody @ResponseBody
public JwtTokenPair activateUser( public JwtPair activateUser(
@ApiParam(value = "Activate user request.") @ApiParam(value = "Activate user request.")
@RequestBody ActivateUserRequest activateRequest, @RequestBody ActivateUserRequest activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
@ -278,7 +278,7 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST) @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
@ResponseBody @ResponseBody
public JwtTokenPair resetPassword( public JwtPair resetPassword(
@ApiParam(value = "Reset password request.") @ApiParam(value = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest, @RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {

28
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -38,7 +38,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
@ -69,7 +69,7 @@ import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine; import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
@ -146,7 +146,7 @@ public class RuleChainController extends BaseController {
private JsInvokeService jsInvokeService; private JsInvokeService jsInvokeService;
@Autowired(required = false) @Autowired(required = false)
private MvelInvokeService mvelInvokeService; private TbelInvokeService tbelInvokeService;
@Autowired(required = false) @Autowired(required = false)
private ActorSystemContext actorContext; private ActorSystemContext actorContext;
@ -154,8 +154,8 @@ public class RuleChainController extends BaseController {
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}") @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
private boolean debugPerTenantEnabled; private boolean debugPerTenantEnabled;
@Value("${mvel.enabled:true}") @Value("${tbel.enabled:true}")
private boolean mvelEnabled; private boolean tbelEnabled;
@ApiOperation(value = "Get Rule Chain (getRuleChainById)", @ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@ -378,13 +378,13 @@ public class RuleChainController extends BaseController {
} }
} }
@ApiOperation(value = "Is MVEL script executor enabled", @ApiOperation(value = "Is TBEL script executor enabled",
notes = "Returns 'True' if the MVEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/mvelEnabled", method = RequestMethod.GET) @RequestMapping(value = "/ruleChain/tbelEnabled", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public Boolean isMvelEnabled() { public Boolean isTbelEnabled() {
return mvelEnabled; return tbelEnabled;
} }
@ApiOperation(value = "Test Script function", @ApiOperation(value = "Test Script function",
@ -393,7 +393,7 @@ public class RuleChainController extends BaseController {
@RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public JsonNode testScript( public JsonNode testScript(
@ApiParam(value = "Script language: JS or MVEL") @ApiParam(value = "Script language: JS or TBEL")
@RequestParam(required = false) ScriptLanguage scriptLang, @RequestParam(required = false) ScriptLanguage scriptLang,
@ApiParam(value = "Test JS request. See API call description above.") @ApiParam(value = "Test JS request. See API call description above.")
@RequestBody JsonNode inputParams) throws ThingsboardException { @RequestBody JsonNode inputParams) throws ThingsboardException {
@ -418,10 +418,10 @@ public class RuleChainController extends BaseController {
if (ScriptLanguage.JS.equals(scriptLang)) { if (ScriptLanguage.JS.equals(scriptLang)) {
engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, script, argNames); engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, script, argNames);
} else { } else {
if (mvelInvokeService == null) { if (tbelInvokeService == null) {
throw new IllegalArgumentException("MVEL script engine is disabled!"); throw new IllegalArgumentException("TBEL script engine is disabled!");
} }
engine = new RuleNodeMvelScriptEngine(getTenantId(), mvelInvokeService, script, argNames); engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames);
} }
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) { switch (scriptType) {

6
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java

@ -39,7 +39,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -87,8 +87,8 @@ public class TwoFactorAuthController extends BaseController {
"and Too Many Requests error if rate limits are exceeded.") "and Too Many Requests error if rate limits are exceeded.")
@PostMapping("/verification/check") @PostMapping("/verification/check")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType, public JwtPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
if (verificationSuccess) { if (verificationSuccess) {

6
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.user.TbUserService; import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -145,7 +145,7 @@ public class UserController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET) @RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public JwtTokenPair getUserToken( public JwtPair getUserToken(
@ApiParam(value = USER_ID_PARAM_DESCRIPTION) @ApiParam(value = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId) throws ThingsboardException { @PathVariable(USER_ID) String strUserId) throws ThingsboardException {
checkParameter(USER_ID, strUserId); checkParameter(USER_ID, strUserId);
@ -182,7 +182,7 @@ public class UserController extends BaseController {
@RequestBody User user, @RequestBody User user,
@ApiParam(value = "Send activation email (or use activation link)", defaultValue = "true") @ApiParam(value = "Send activation email (or use activation link)", defaultValue = "true")
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException { @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException {
if (Authority.TENANT_ADMIN.equals(getCurrentUser().getAuthority())) { if (!Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
user.setTenantId(getCurrentUser().getTenantId()); user.setTenantId(getCurrentUser().getTenantId());
} }
checkEntity(user.getId(), user, Resource.USER); checkEntity(user.getId(), user, Resource.USER);

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -273,6 +273,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.createSysAdmin(); systemDataLoaderService.createSysAdmin();
systemDataLoaderService.createDefaultTenantProfiles(); systemDataLoaderService.createDefaultTenantProfiles();
systemDataLoaderService.createAdminSettings(); systemDataLoaderService.createAdminSettings();
systemDataLoaderService.createRandomJwtSettings();
systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createOAuth2Templates();
systemDataLoaderService.createQueues(); systemDataLoaderService.createQueues();

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

@ -244,7 +244,7 @@ public final class EdgeGrpcSession implements Closeable {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
String errorMsg = t.getMessage() != null ? t.getMessage() : ""; String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t);
UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder() UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder()
.setUplinkMsgId(uplinkMsg.getUplinkMsgId()) .setUplinkMsgId(uplinkMsg.getUplinkMsgId())
.setSuccess(false).setErrorMsg(errorMsg).build(); .setSuccess(false).setErrorMsg(errorMsg).build();

15
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -82,6 +82,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
@ -167,6 +168,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired @Autowired
private QueueService queueService; private QueueService queueService;
@Autowired
private JwtSettingsService jwtSettingsService;
@Bean @Bean
protected BCryptPasswordEncoder passwordEncoder() { protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
@ -263,6 +267,16 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings);
} }
@Override
public void createRandomJwtSettings() throws Exception {
jwtSettingsService.createRandomJwtSettings();
}
@Override
public void saveLegacyYmlSettings() throws Exception {
jwtSettingsService.saveLegacyYmlSettings();
}
@Override @Override
public void createOAuth2Templates() throws Exception { public void createOAuth2Templates() throws Exception {
installScripts.createOAuth2Templates(); installScripts.createOAuth2Templates();
@ -656,4 +670,5 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
queueService.saveQueue(sequentialByOriginatorQueue); queueService.saveQueue(sequentialByOriginatorQueue);
} }
} }
} }

4
application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java

@ -23,6 +23,10 @@ public interface SystemDataLoaderService {
void createAdminSettings() throws Exception; void createAdminSettings() throws Exception;
void createRandomJwtSettings() throws Exception;
void saveLegacyYmlSettings() throws Exception;
void createOAuth2Templates() throws Exception; void createOAuth2Templates() throws Exception;
void loadSystemWidgets() throws Exception; void loadSystemWidgets() throws Exception;

1
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -186,6 +186,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
break; break;
case "3.4.1": case "3.4.1":
log.info("Updating data from version 3.4.1 to 3.4.2 ..."); log.info("Updating data from version 3.4.1 to 3.4.2 ...");
systemDataLoaderService.saveLegacyYmlSettings();
boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false); boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false);
if (!skipAuditLogsMigration) { if (!skipAuditLogsMigration) {
log.info("Starting audit logs migration. Can be skipped with TB_SKIP_AUDIT_LOGS_MIGRATION env variable set to true"); log.info("Starting audit logs migration. Can be skipped with TB_SKIP_AUDIT_LOGS_MIGRATION env variable set to true");

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
@ -143,8 +144,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
EdgeNotificationService edgeNotificationService, EdgeNotificationService edgeNotificationService,
OtaPackageStateService firmwareStateService, OtaPackageStateService firmwareStateService,
GitVersionControlQueueService vcQueueService, GitVersionControlQueueService vcQueueService,
PartitionService partitionService) { PartitionService partitionService,
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer()); Optional<JwtSettingsService> jwtSettingsService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer(); this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer(); this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer(); this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();

3
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -70,6 +70,7 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -126,7 +127,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbTenantProfileCache tenantProfileCache, TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, QueueService queueService) { PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, QueueService queueService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer()); super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty());
this.statisticsService = statisticsService; this.statisticsService = statisticsService;
this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory; this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;
this.submitStrategyFactory = submitStrategyFactory; this.submitStrategyFactory = submitStrategyFactory;

23
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -76,11 +77,13 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final PartitionService partitionService; protected final PartitionService partitionService;
protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer; protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
protected final Optional<JwtSettingsService> jwtSettingsService;
public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache, TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService, TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer) { PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, Optional<JwtSettingsService> jwtSettingsService) {
this.actorContext = actorContext; this.actorContext = actorContext;
this.encodingService = encodingService; this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache; this.tenantProfileCache = tenantProfileCache;
@ -89,6 +92,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
this.apiUsageStateService = apiUsageStateService; this.apiUsageStateService = apiUsageStateService;
this.partitionService = partitionService; this.partitionService = partitionService;
this.nfConsumer = nfConsumer; this.nfConsumer = nfConsumer;
this.jwtSettingsService = jwtSettingsService;
} }
public void init(String mainConsumerThreadName, String nfConsumerThreadName) { public void init(String mainConsumerThreadName, String nfConsumerThreadName) {
@ -172,12 +176,17 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
apiUsageStateService.onTenantProfileUpdate(tenantProfileId); apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
} }
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) { } else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tenantProfileCache.evict(componentLifecycleMsg.getTenantId()); if (TenantId.SYS_TENANT_ID.equals(componentLifecycleMsg.getTenantId())) {
partitionService.removeTenant(componentLifecycleMsg.getTenantId()); jwtSettingsService.ifPresent(JwtSettingsService::reloadJwtSettings);
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) { return;
apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId()); } else {
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) { tenantProfileCache.evict(componentLifecycleMsg.getTenantId());
apiUsageStateService.onTenantDelete((TenantId) componentLifecycleMsg.getEntityId()); partitionService.removeTenant(componentLifecycleMsg.getTenantId());
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete((TenantId) componentLifecycleMsg.getEntityId());
}
} }
} else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) { } else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId())); deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));

6
application/src/main/java/org/thingsboard/server/service/script/RuleNodeMvelScriptEngine.java → application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java

@ -23,7 +23,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.RuleNodeScriptFactory; import org.thingsboard.script.api.RuleNodeScriptFactory;
import org.thingsboard.script.api.mvel.MvelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -42,9 +42,9 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
public class RuleNodeMvelScriptEngine extends RuleNodeScriptEngine<MvelInvokeService, Object> { public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeService, Object> {
public RuleNodeMvelScriptEngine(TenantId tenantId, MvelInvokeService scriptInvokeService, String script, String... argNames) { public RuleNodeTbelScriptEngine(TenantId tenantId, TbelInvokeService scriptInvokeService, String script, String... argNames) {
super(tenantId, scriptInvokeService, script, argNames); super(tenantId, scriptInvokeService, script, argNames);
} }

162
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java

@ -0,0 +1,162 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.jwt.settings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultJwtSettingsService implements JwtSettingsService {
@Lazy
private final AdminSettingsService adminSettingsService;
@Lazy
private final Optional<TbClusterService> tbClusterService;
private final JwtSettingsValidator jwtSettingsValidator;
@Value("${security.jwt.tokenExpirationTime:9000}")
private Integer tokenExpirationTime;
@Value("${security.jwt.refreshTokenExpTime:604800}")
private Integer refreshTokenExpTime;
@Value("${security.jwt.tokenIssuer:thingsboard.io}")
private String tokenIssuer;
@Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}")
private String tokenSigningKey;
private volatile JwtSettings jwtSettings = null; //lazy init
/**
* Create JWT admin settings is intended to be called from Install scripts only
*/
@Override
public void createRandomJwtSettings() {
if (getJwtSettingsFromDb() == null) {
log.info("Creating JWT admin settings...");
this.jwtSettings = getJwtSettingsFromYml();
if (isSigningKeyDefault(jwtSettings)) {
this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
}
saveJwtSettings(jwtSettings);
} else {
log.info("Skip creating JWT admin settings because they already exist.");
}
}
/**
* Create JWT admin settings is intended to be called from Upgrade scripts only
*/
@Override
public void saveLegacyYmlSettings() {
log.info("Saving legacy JWT admin settings from YML...");
if (getJwtSettingsFromDb() == null) {
saveJwtSettings(getJwtSettingsFromYml());
}
}
@Override
public JwtSettings saveJwtSettings(JwtSettings jwtSettings) {
jwtSettingsValidator.validate(jwtSettings);
final AdminSettings adminJwtSettings = mapJwtToAdminSettings(jwtSettings);
final AdminSettings existedSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
if (existedSettings != null) {
adminJwtSettings.setId(existedSettings.getId());
}
log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored");
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings);
tbClusterService.ifPresent(cs -> cs.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED));
return reloadJwtSettings();
}
@Override
public JwtSettings reloadJwtSettings() {
return getJwtSettings(true);
}
@Override
public JwtSettings getJwtSettings() {
return getJwtSettings(false);
}
public JwtSettings getJwtSettings(boolean forceReload) {
if (this.jwtSettings == null || forceReload) {
synchronized (this) {
if (this.jwtSettings == null || forceReload) {
JwtSettings result = getJwtSettingsFromDb();
if (result == null) {
result = getJwtSettingsFromYml();
log.warn("Loading the JWT settings from YML since there are no settings in DB. Looks like the upgrade script was not applied.");
}
if (isSigningKeyDefault(result)) {
log.warn("WARNING: The platform is configured to use default JWT Signing Key. " +
"This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " +
"Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.");
}
this.jwtSettings = result;
}
}
}
return this.jwtSettings;
}
private JwtSettings getJwtSettingsFromYml() {
return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey);
}
private JwtSettings getJwtSettingsFromDb() {
AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
return adminJwtSettings != null ? mapAdminToJwtSettings(adminJwtSettings) : null;
}
private JwtSettings mapAdminToJwtSettings(AdminSettings adminSettings) {
Objects.requireNonNull(adminSettings, "adminSettings for JWT is null");
return JacksonUtil.treeToValue(adminSettings.getJsonValue(), JwtSettings.class);
}
private AdminSettings mapJwtToAdminSettings(JwtSettings jwtSettings) {
Objects.requireNonNull(jwtSettings, "jwtSettings is null");
AdminSettings adminJwtSettings = new AdminSettings();
adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID);
adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY);
adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings));
return adminJwtSettings;
}
private boolean isSigningKeyDefault(JwtSettings settings) {
return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey());
}
}

69
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.jwt.settings;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.util.Arrays;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.Base64;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
@Override
public void validate(JwtSettings jwtSettings) {
if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) {
throw new DataValidationException("JWT token issuer should be specified!");
}
if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) {
throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!");
}
if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) {
throw new DataValidationException("JWT token expiration time should be at least 1 minute!");
}
if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) {
throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!");
}
if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) {
throw new DataValidationException("JWT token signing key should be specified!");
}
byte[] decodedKey;
try {
decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey());
} catch (Exception e) {
throw new DataValidationException("JWT token signing key should be a valid Base64 encoded string! " + e.getMessage());
}
if (Arrays.isNullOrEmpty(decodedKey)) {
throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!");
}
if (decodedKey.length * Byte.SIZE < 256 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) {
throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!");
}
System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory
}
}

39
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.jwt.settings;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtSettings;
/**
* During Install or upgrade the validation is suppressed to keep existing data
* */
@Primary
@Profile("install")
@Component
@RequiredArgsConstructor
public class InstallJwtSettingsValidator implements JwtSettingsValidator {
@Override
public void validate(JwtSettings jwtSettings) {
}
}

35
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.jwt.settings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
public interface JwtSettingsService {
String ADMIN_SETTINGS_JWT_KEY = "jwt";
String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey";
JwtSettings getJwtSettings();
JwtSettings reloadJwtSettings();
void createRandomJwtSettings();
void saveLegacyYmlSettings();
JwtSettings saveJwtSettings(JwtSettings jwtSettings);
}

23
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.jwt.settings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
public interface JwtSettingsValidator {
void validate(JwtSettings jwtSettings);
}

17
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -104,10 +104,10 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
registration); registration);
JwtTokenPair tokenPair = tokenFactory.createTokenPair(securityUser);
clearAuthenticationAttributes(request, response); clearAuthenticationAttributes(request, response);
getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken());
JwtPair tokenPair = tokenFactory.createTokenPair(securityUser);
getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(baseUrl, tokenPair));
systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, registration.getName(), null); systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, registration.getName(), null);
} catch (Exception e) { } catch (Exception e) {
log.debug("Error occurred during processing authentication success result. " + log.debug("Error occurred during processing authentication success result. " +
@ -128,4 +128,13 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
super.clearAuthenticationAttributes(request); super.clearAuthenticationAttributes(request);
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
} }
String getRedirectUrl(String baseUrl, JwtPair tokenPair) {
if (baseUrl.indexOf("?") > 0) {
baseUrl += "&";
} else {
baseUrl += "/?";
}
return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken();
}
} }

4
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java

@ -26,7 +26,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -49,7 +49,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException { Authentication authentication) throws IOException, ServletException {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
JwtTokenPair tokenPair = new JwtTokenPair(); JwtPair tokenPair = new JwtPair();
if (authentication instanceof MfaAuthenticationToken) { if (authentication instanceof MfaAuthenticationToken) {
int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true) int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)

32
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java

@ -24,8 +24,8 @@ import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.UnsupportedJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -35,9 +35,9 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
@ -49,6 +49,7 @@ import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Component @Component
@RequiredArgsConstructor
@Slf4j @Slf4j
public class JwtTokenFactory { public class JwtTokenFactory {
@ -62,12 +63,7 @@ public class JwtTokenFactory {
private static final String CUSTOMER_ID = "customerId"; private static final String CUSTOMER_ID = "customerId";
private static final String SESSION_ID = "sessionId"; private static final String SESSION_ID = "sessionId";
private final JwtSettings settings; private final JwtSettingsService jwtSettingsService;
@Autowired
public JwtTokenFactory(JwtSettings settings) {
this.settings = settings;
}
/** /**
* Factory method for issuing new JWT Tokens. * Factory method for issuing new JWT Tokens.
@ -80,7 +76,7 @@ public class JwtTokenFactory {
UserPrincipal principal = securityUser.getUserPrincipal(); UserPrincipal principal = securityUser.getUserPrincipal();
JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream() JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()), settings.getTokenExpirationTime()); .map(GrantedAuthority::getAuthority).collect(Collectors.toList()), jwtSettingsService.getJwtSettings().getTokenExpirationTime());
jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName()) jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName())
.claim(LAST_NAME, securityUser.getLastName()) .claim(LAST_NAME, securityUser.getLastName())
.claim(ENABLED, securityUser.isEnabled()) .claim(ENABLED, securityUser.isEnabled())
@ -142,7 +138,7 @@ public class JwtTokenFactory {
public JwtToken createRefreshToken(SecurityUser securityUser) { public JwtToken createRefreshToken(SecurityUser securityUser) {
UserPrincipal principal = securityUser.getUserPrincipal(); UserPrincipal principal = securityUser.getUserPrincipal();
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), settings.getRefreshTokenExpTime()) String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), jwtSettingsService.getJwtSettings().getRefreshTokenExpTime())
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID) .claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID)
.setId(UUID.randomUUID().toString()).compact(); .setId(UUID.randomUUID().toString()).compact();
@ -198,30 +194,30 @@ public class JwtTokenFactory {
return Jwts.builder() return Jwts.builder()
.setClaims(claims) .setClaims(claims)
.setIssuer(settings.getTokenIssuer()) .setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
.setIssuedAt(Date.from(currentTime.toInstant())) .setIssuedAt(Date.from(currentTime.toInstant()))
.setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) .setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()); .signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey());
} }
public Jws<Claims> parseTokenClaims(JwtToken token) { public Jws<Claims> parseTokenClaims(JwtToken token) {
try { try {
return Jwts.parser() return Jwts.parser()
.setSigningKey(settings.getTokenSigningKey()) .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey())
.parseClaimsJws(token.getToken()); .parseClaimsJws(token.getToken());
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) {
log.debug("Invalid JWT Token", ex); log.debug("Invalid JWT Token", ex);
throw new BadCredentialsException("Invalid JWT token: ", ex); throw new BadCredentialsException("Invalid JWT token: ", ex);
} catch (ExpiredJwtException expiredEx) { } catch (SignatureException | ExpiredJwtException expiredEx) {
log.debug("JWT Token is expired", expiredEx); log.debug("JWT Token is expired", expiredEx);
throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx); throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx);
} }
} }
public JwtTokenPair createTokenPair(SecurityUser securityUser) { public JwtPair createTokenPair(SecurityUser securityUser) {
JwtToken accessToken = createAccessJwtToken(securityUser); JwtToken accessToken = createAccessJwtToken(securityUser);
JwtToken refreshToken = createRefreshToken(securityUser); JwtToken refreshToken = createRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); return new JwtPair(accessToken.getToken(), refreshToken.getToken());
} }
} }

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

@ -107,11 +107,11 @@ plugins:
# Security parameters # Security parameters
security: security:
# JWT Token parameters # JWT Token parameters
jwt: jwt: # Since 3.4.2 values are persisted to the database during install or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN
tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours) tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours)
refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week). refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week).
tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Base64 encoded
# Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator
user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}" user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}"
# Enable/disable case-sensitive username login # Enable/disable case-sensitive username login
@ -148,7 +148,7 @@ ui:
# Help parameters # Help parameters
help: help:
# Base url for UI help assets # Base url for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.4.1}" base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.4.2}"
database: database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records
@ -227,6 +227,7 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}" default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE # Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}" ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}"
ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}" ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}"
ts_key_value_ttl: "${TS_KV_TTL:0}" ts_key_value_ttl: "${TS_KV_TTL:0}"
buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}" buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
@ -610,24 +611,25 @@ state:
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}" defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}"
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}" persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
mvel: tbel:
enabled: "${MVEL_ENABLED:true}" enabled: "${TBEL_ENABLED:true}"
max_total_args_size: "${MVEL_MAX_TOTAL_ARGS_SIZE:100000}" max_total_args_size: "${TBEL_MAX_TOTAL_ARGS_SIZE:100000}"
max_result_size: "${MVEL_MAX_RESULT_SIZE:300000}" max_result_size: "${TBEL_MAX_RESULT_SIZE:300000}"
max_script_body_size: "${MVEL_MAX_SCRIPT_BODY_SIZE:50000}" max_script_body_size: "${TBEL_MAX_SCRIPT_BODY_SIZE:50000}"
# Maximum allowed MVEL script execution memory # Maximum allowed TBEL script execution memory
max_memory_limit_mb: "${MVEL_MAX_MEMORY_LIMIT_MB: 8}" max_memory_limit_mb: "${TBEL_MAX_MEMORY_LIMIT_MB: 8}"
# Maximum allowed MVEL script execution errors before it will be blacklisted # Maximum allowed TBEL script execution errors before it will be blacklisted
max_errors: "${MVEL_MAX_ERRORS:3}" max_errors: "${TBEL_MAX_ERRORS:3}"
# MVEL Eval max request timeout in milliseconds. 0 - no timeout # TBEL Eval max request timeout in milliseconds. 0 - no timeout
max_requests_timeout: "${MVEL_MAX_REQUEST_TIMEOUT:500}" max_requests_timeout: "${TBEL_MAX_REQUEST_TIMEOUT:500}"
# Maximum time in seconds for black listed function to stay in the list. # Maximum time in seconds for black listed function to stay in the list.
max_black_list_duration_sec: "${MVEL_MAX_BLACKLIST_DURATION_SEC:60}" max_black_list_duration_sec: "${TBEL_MAX_BLACKLIST_DURATION_SEC:60}"
# Specify thread pool size for javascript executor service # Specify thread pool size for javascript executor service
thread_pool_size: "${MVEL_THREAD_POOL_SIZE:50}" thread_pool_size: "${TBEL_THREAD_POOL_SIZE:50}"
compiled_scripts_cache_size: "${TBEL_COMPILED_SCRIPTS_CACHE_SIZE:1000}"
stats: stats:
enabled: "${TB_MVEL_STATS_ENABLED:false}" enabled: "${TB_TBEL_STATS_ENABLED:false}"
print_interval_ms: "${TB_MVEL_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}"
js: js:
evaluator: "${JS_EVALUATOR:local}" # local/remote evaluator: "${JS_EVALUATOR:local}" # local/remote

122
application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java

@ -17,14 +17,21 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.mail.DefaultMailService; import org.thingsboard.server.service.mail.DefaultMailService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
@ -32,8 +39,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
public abstract class BaseAdminControllerTest extends AbstractControllerTest { public abstract class BaseAdminControllerTest extends AbstractControllerTest {
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey");
@Autowired @Autowired
MailService mailService; MailService mailService;
@ -45,67 +53,67 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
public void testFindAdminSettingsByKey() throws Exception { public void testFindAdminSettingsByKey() throws Exception {
loginSysAdmin(); loginSysAdmin();
doGet("/api/admin/settings/general") doGet("/api/admin/settings/general")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().contentType(contentType)) .andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue())) .andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("general"))) .andExpect(jsonPath("$.key", is("general")))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080"))); .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080")));
doGet("/api/admin/settings/mail") doGet("/api/admin/settings/mail")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().contentType(contentType)) .andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue())) .andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("mail"))) .andExpect(jsonPath("$.key", is("mail")))
.andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp"))) .andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp")))
.andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost"))) .andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost")))
.andExpect(jsonPath("$.jsonValue.smtpPort", is("25"))); .andExpect(jsonPath("$.jsonValue.smtpPort", is("25")));
doGet("/api/admin/settings/unknown") doGet("/api/admin/settings/unknown")
.andExpect(status().isNotFound()); .andExpect(status().isNotFound());
} }
@Test @Test
public void testSaveAdminSettings() throws Exception { public void testSaveAdminSettings() throws Exception {
loginSysAdmin(); loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class); AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class);
JsonNode jsonValue = adminSettings.getJsonValue(); JsonNode jsonValue = adminSettings.getJsonValue();
((ObjectNode) jsonValue).put("baseUrl", "http://myhost.org"); ((ObjectNode) jsonValue).put("baseUrl", "http://myhost.org");
adminSettings.setJsonValue(jsonValue); adminSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk()); doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
doGet("/api/admin/settings/general") doGet("/api/admin/settings/general")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().contentType(contentType)) .andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org"))); .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org")));
((ObjectNode) jsonValue).put("baseUrl", "http://localhost:8080"); ((ObjectNode) jsonValue).put("baseUrl", "http://localhost:8080");
adminSettings.setJsonValue(jsonValue); adminSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", adminSettings) doPost("/api/admin/settings", adminSettings)
.andExpect(status().isOk()); .andExpect(status().isOk());
} }
@Test @Test
public void testSaveAdminSettingsWithEmptyKey() throws Exception { public void testSaveAdminSettingsWithEmptyKey() throws Exception {
loginSysAdmin(); loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
adminSettings.setKey(null); adminSettings.setKey(null);
doPost("/api/admin/settings", adminSettings) doPost("/api/admin/settings", adminSettings)
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Key should be specified"))); .andExpect(statusReason(containsString("Key should be specified")));
} }
@Test @Test
public void testChangeAdminSettingsKey() throws Exception { public void testChangeAdminSettingsKey() throws Exception {
loginSysAdmin(); loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
adminSettings.setKey("newKey"); adminSettings.setKey("newKey");
doPost("/api/admin/settings", adminSettings) doPost("/api/admin/settings", adminSettings)
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("is prohibited"))); .andExpect(statusReason(containsString("is prohibited")));
} }
@Test @Test
@ -113,7 +121,7 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
loginSysAdmin(); loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
doPost("/api/admin/settings/testMail", adminSettings) doPost("/api/admin/settings/testMail", adminSettings)
.andExpect(status().isOk()); .andExpect(status().isOk());
} }
@Test @Test
@ -139,4 +147,48 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError()); doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError());
Mockito.doNothing().when(mailService).sendTestMail(Mockito.any(), Mockito.any()); Mockito.doNothing().when(mailService).sendTestMail(Mockito.any(), Mockito.any());
} }
void resetJwtSettingsToDefault() throws Exception {
loginSysAdmin();
doPost("/api/admin/jwtSettings", defaultJwtSettings).andExpect(status().isOk()); // jwt test scenarios are always started from
loginTenantAdmin();
}
@Test
public void testGetAndSaveDefaultJwtSettings() throws Exception {
JwtSettings jwtSettings;
loginSysAdmin();
jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());
jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
resetJwtSettingsToDefault();
}
@Test
public void testCreateJwtSettings() throws Exception {
loginSysAdmin();
JwtSettings jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8)));
doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());
doGet("/api/admin/jwtSettings").andExpect(status().isUnauthorized()); //the old JWT token does not work after signing key was changed!
loginSysAdmin();
JwtSettings newJwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(newJwtSettings);
resetJwtSettingsToDefault();
}
} }

4
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java

@ -51,7 +51,7 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
@ -396,7 +396,7 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
private void logInWithPreVerificationToken(String username, String password) throws Exception { private void logInWithPreVerificationToken(String username, String password) throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password); LoginRequest loginRequest = new LoginRequest(username, password);
JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class); JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class);
assertThat(response.getToken()).isNotNull(); assertThat(response.getToken()).isNotNull();
assertThat(response.getRefreshToken()).isNull(); assertThat(response.getRefreshToken()).isNull();
assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);

114
application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java → application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Assert; import org.junit.Assert;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -23,31 +24,39 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.mvel.MvelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.script.api.tbel.TbelScript;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"mvel.max_script_body_size=100", "tbel.max_script_body_size=100",
"mvel.max_total_args_size=50", "tbel.max_total_args_size=50",
"mvel.max_result_size=50", "tbel.max_result_size=50",
"mvel.max_errors=2", "tbel.max_errors=2",
"tbel.compiled_scripts_cache_size=100"
}) })
class MvelInvokeServiceTest extends AbstractControllerTest { class TbelInvokeServiceTest extends AbstractControllerTest {
@Autowired @Autowired
private MvelInvokeService invokeService; private TbelInvokeService invokeService;
@Value("${mvel.max_errors}") @Value("${tbel.max_errors}")
private int maxJsErrors; private int maxJsErrors;
@Test @Test
@ -110,6 +119,89 @@ class MvelInvokeServiceTest extends AbstractControllerTest {
assertThatScriptIsBlocked(scriptId); assertThatScriptIsBlocked(scriptId);
} }
@Test
void givenScriptsWithSameBody_thenCompileAndCacheOnlyOnce() throws Exception {
String script = "return msg.temperature > 20;";
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 100; i++) {
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
}
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Map<String, TbelScript> scriptMap = getFieldValue(invokeService, "scriptMap");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
String scriptHash = scriptIdToHash.get(scriptsIds.get(0));
assertThat(scriptsIds.stream().map(scriptIdToHash::get)).containsOnly(scriptHash);
assertThat(scriptMap).containsKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNotNull();
}
@Test
public void whenReleasingScript_thenCheckForScriptHashUsages() throws Exception {
String script = "return msg.temperature > 20;";
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 10; i++) {
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
}
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Map<String, TbelScript> scriptMap = getFieldValue(invokeService, "scriptMap");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
String scriptHash = scriptIdToHash.get(scriptsIds.get(0));
for (int i = 0; i < 9; i++) {
UUID scriptId = scriptsIds.get(i);
assertThat(scriptIdToHash).containsKey(scriptId);
invokeService.release(scriptId);
assertThat(scriptIdToHash).doesNotContainKey(scriptId);
}
assertThat(scriptMap).containsKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNotNull();
invokeService.release(scriptsIds.get(9));
assertThat(scriptMap).doesNotContainKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNull();
}
@Test
public void whenCompiledScriptsCacheIsTooBig_thenRemoveRarelyUsedScripts() throws Exception {
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 110; i++) { // tbel.compiled_scripts_cache_size = 100
String script = "return msg.temperature > " + i;
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
for (int j = 0; j < i; j++) {
invokeScript(scriptId, "{ \"temperature\": 12 }"); // so that scriptsIds is ordered by number of invocations
}
}
ConcurrentMap<String, Serializable> cache = compiledScriptsCache.asMap();
for (int i = 0; i < 10; i++) { // iterating rarely used scripts
UUID scriptId = scriptsIds.get(i);
String scriptHash = scriptIdToHash.get(scriptId);
assertThat(cache).doesNotContainKey(scriptHash);
}
for (int i = 10; i < 110; i++) {
UUID scriptId = scriptsIds.get(i);
String scriptHash = scriptIdToHash.get(scriptId);
assertThat(cache).containsKey(scriptHash);
}
UUID scriptRemovedFromCache = scriptsIds.get(0);
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNull();
invokeScript(scriptRemovedFromCache, "{ \"temperature\": 12 }");
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNotNull();
}
private void assertThatScriptIsBlocked(UUID scriptId) { private void assertThatScriptIsBlocked(UUID scriptId) {
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
invokeScript(scriptId, "{}"); invokeScript(scriptId, "{}");
@ -125,4 +217,10 @@ class MvelInvokeServiceTest extends AbstractControllerTest {
return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString(); return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString();
} }
private <T> T getFieldValue(Object target, String fieldName) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (T) field.get(target);
}
} }

10
application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java

@ -23,7 +23,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.AccessJwtToken; import org.thingsboard.server.service.security.model.token.AccessJwtToken;
@ -36,6 +37,8 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
public class JwtTokenFactoryTest { public class JwtTokenFactoryTest {
@ -50,7 +53,10 @@ public class JwtTokenFactoryTest {
jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2));
jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7));
tokenFactory = new JwtTokenFactory(jwtSettings); JwtSettingsService jwtSettingsService = mock(JwtSettingsService.class);
willReturn(jwtSettings).given(jwtSettingsService).getJwtSettings();
tokenFactory = new JwtTokenFactory(jwtSettingsService);
} }
@Test @Test

68
application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandlerTest.java

@ -0,0 +1,68 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@DaoSqlTest
public class Oauth2AuthenticationSuccessHandlerTest extends AbstractControllerTest {
@Autowired
private Oauth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler;
@Mock
private JwtTokenFactory jwtTokenFactory;
private SecurityUser securityUser;
@Before
public void before() {
UserId userId = new UserId(UUID.randomUUID());
securityUser = new SecurityUser(userId);
when(jwtTokenFactory.createTokenPair(eq(securityUser))).thenReturn(new JwtPair("testAccessToken", "testRefreshToken"));
}
@Test
public void testGetRedirectUrl() {
JwtPair jwtPair = jwtTokenFactory.createTokenPair(securityUser);
String urlWithoutParams = "http://localhost:8080/dashboardGroups/3fa13530-6597-11ed-bd76-8bd591f0ec3e";
String urlWithParams = "http://localhost:8080/dashboardGroups/3fa13530-6597-11ed-bd76-8bd591f0ec3e?state=someState&page=1";
String redirectUrl = oauth2AuthenticationSuccessHandler.getRedirectUrl(urlWithoutParams, jwtPair);
String expectedUrl = urlWithoutParams + "/?accessToken=" + jwtPair.getToken() + "&refreshToken=" + jwtPair.getRefreshToken();
assertEquals(expectedUrl, redirectUrl);
redirectUrl = oauth2AuthenticationSuccessHandler.getRedirectUrl(urlWithParams, jwtPair);
expectedUrl = urlWithParams + "&accessToken=" + jwtPair.getToken() + "&refreshToken=" + jwtPair.getRefreshToken();
assertEquals(expectedUrl, redirectUrl);
}
}

2
application/src/test/resources/logback-test.xml

@ -16,6 +16,8 @@
<logger name="org.cassandraunit" level="INFO"/> <logger name="org.cassandraunit" level="INFO"/>
<logger name="org.eclipse.leshan" level="INFO"/> <logger name="org.eclipse.leshan" level="INFO"/>
<!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors -->
<logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/>
<root level="WARN"> <root level="WARN">
<appender-ref ref="console"/> <appender-ref ref="console"/>

19
common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Throwables;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -29,6 +30,8 @@ import java.util.concurrent.ThreadLocalRandom;
@Slf4j @Slf4j
public final class EdgeUtils { public final class EdgeUtils {
private static final int STACK_TRACE_LIMIT = 10;
private EdgeUtils() { private EdgeUtils() {
} }
@ -93,4 +96,20 @@ public final class EdgeUtils {
edgeEvent.setBody(body); edgeEvent.setBody(body);
return edgeEvent; return edgeEvent;
} }
public static String createErrorMsgFromRootCauseAndStackTrace(Throwable t) {
Throwable rootCause = Throwables.getRootCause(t);
StringBuilder errorMsg = new StringBuilder(rootCause.getMessage() != null ? rootCause.getMessage() : "");
if (rootCause.getStackTrace().length > 0) {
int idx = 0;
for (StackTraceElement stackTraceElement : rootCause.getStackTrace()) {
errorMsg.append("\n").append(stackTraceElement.toString());
idx++;
if (idx > STACK_TRACE_LIMIT) {
break;
}
}
}
return errorMsg.toString();
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -36,6 +37,7 @@ import java.util.function.Consumer;
public abstract class SearchTextBasedWithAdditionalInfo<I extends UUIDBased> extends SearchTextBased<I> implements HasAdditionalInfo { public abstract class SearchTextBasedWithAdditionalInfo<I extends UUIDBased> extends SearchTextBased<I> implements HasAdditionalInfo {
public static final ObjectMapper mapper = new ObjectMapper(); public static final ObjectMapper mapper = new ObjectMapper();
@NoXss
private transient JsonNode additionalInfo; private transient JsonNode additionalInfo;
@JsonIgnore @JsonIgnore
private byte[] additionalInfoBytes; private byte[] additionalInfoBytes;

2
common/data/src/main/java/org/thingsboard/server/common/data/script/ScriptLanguage.java

@ -16,5 +16,5 @@
package org.thingsboard.server.common.data.script; package org.thingsboard.server.common.data.script;
public enum ScriptLanguage { public enum ScriptLanguage {
JS, MVEL JS, TBEL
} }

9
application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java

@ -13,19 +13,18 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.security.model; package org.thingsboard.server.common.data.security.model;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ApiModel(value = "JWT Token Pair") @ApiModel(value = "JWT Pair")
@Data @Data
@NoArgsConstructor @NoArgsConstructor
public class JwtTokenPair { public class JwtPair {
@ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..") @ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
private String token; private String token;
@ -34,7 +33,7 @@ public class JwtTokenPair {
private Authority scope; private Authority scope;
public JwtTokenPair(String token, String refreshToken) { public JwtPair(String token, String refreshToken) {
this.token = token; this.token = token;
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
} }

30
application/src/main/java/org/thingsboard/server/config/JwtSettings.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java

@ -13,35 +13,43 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.config; package org.thingsboard.server.common.data.security.model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtToken;
@Component @ApiModel(value = "JWT Settings")
@ConfigurationProperties(prefix = "security.jwt") @AllArgsConstructor
@NoArgsConstructor
@Data @Data
public class JwtSettings { public class JwtSettings {
/** /**
* {@link JwtToken} will expire after this time. * {@link JwtToken} will expire after this time.
*/ */
@ApiModelProperty(position = 1, value = "The JWT will expire after seconds.", example = "9000")
private Integer tokenExpirationTime; private Integer tokenExpirationTime;
/**
* {@link JwtToken} can be refreshed during this timeframe.
*/
@ApiModelProperty(position = 2, value = "The JWT can be refreshed during seconds.", example = "604800")
private Integer refreshTokenExpTime;
/** /**
* Token issuer. * Token issuer.
*/ */
@ApiModelProperty(position = 3, value = "The JWT issuer.", example = "thingsboard.io")
private String tokenIssuer; private String tokenIssuer;
/** /**
* Key is used to sign {@link JwtToken}. * Key is used to sign {@link JwtToken}.
* Base64 encoded
*/ */
@ApiModelProperty(position = 4, value = "The JWT key is used to sing token. Base64 encoded.", example = "cTU4WnNqemI2aU5wbWVjdm1vYXRzanhjNHRUcXliMjE=")
private String tokenSigningKey; private String tokenSigningKey;
/**
* {@link JwtToken} can be refreshed during this timeframe.
*/
private Integer refreshTokenExpTime;
} }

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

@ -56,6 +56,10 @@
<groupId>com.google.code.gson</groupId> <groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
@ -86,7 +90,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>mvel2</artifactId> <artifactId>tbel</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

112
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java

@ -13,8 +13,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.script.api.mvel; package org.thingsboard.script.api.tbel;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -42,63 +46,74 @@ import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.io.Serializable; import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@Slf4j @Slf4j
@ConditionalOnProperty(prefix = "mvel", value = "enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnProperty(prefix = "tbel", value = "enabled", havingValue = "true", matchIfMissing = true)
@Service @Service
public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService { public class DefaultTbelInvokeService extends AbstractScriptInvokeService implements TbelInvokeService {
protected final Map<UUID, String> scriptIdToHash = new ConcurrentHashMap<>();
protected final Map<String, TbelScript> scriptMap = new ConcurrentHashMap<>();
protected Cache<String, Serializable> compiledScriptsCache;
protected Map<UUID, MvelScript> scriptMap = new ConcurrentHashMap<>();
private SandboxedParserConfiguration parserConfig; private SandboxedParserConfiguration parserConfig;
private static final Pattern NEW_KEYWORD_PATTERN = Pattern.compile("new\\s"); private static final Pattern NEW_KEYWORD_PATTERN = Pattern.compile("new\\s");
@Getter @Getter
@Value("${mvel.max_total_args_size:100000}") @Value("${tbel.max_total_args_size:100000}")
private long maxTotalArgsSize; private long maxTotalArgsSize;
@Getter @Getter
@Value("${mvel.max_result_size:300000}") @Value("${tbel.max_result_size:300000}")
private long maxResultSize; private long maxResultSize;
@Getter @Getter
@Value("${mvel.max_script_body_size:50000}") @Value("${tbel.max_script_body_size:50000}")
private long maxScriptBodySize; private long maxScriptBodySize;
@Getter @Getter
@Value("${mvel.max_errors:3}") @Value("${tbel.max_errors:3}")
private int maxErrors; private int maxErrors;
@Getter @Getter
@Value("${mvel.max_black_list_duration_sec:60}") @Value("${tbel.max_black_list_duration_sec:60}")
private int maxBlackListDurationSec; private int maxBlackListDurationSec;
@Getter @Getter
@Value("${mvel.max_requests_timeout:0}") @Value("${tbel.max_requests_timeout:0}")
private long maxInvokeRequestsTimeout; private long maxInvokeRequestsTimeout;
@Getter @Getter
@Value("${mvel.stats.enabled:false}") @Value("${tbel.stats.enabled:false}")
private boolean statsEnabled; private boolean statsEnabled;
@Value("${mvel.thread_pool_size:50}") @Value("${tbel.thread_pool_size:50}")
private int threadPoolSize; private int threadPoolSize;
@Value("${mvel.max_memory_limit_mb:8}") @Value("${tbel.max_memory_limit_mb:8}")
private long maxMemoryLimitMb; private long maxMemoryLimitMb;
@Value("${tbel.compiled_scripts_cache_size:1000}")
private int compiledScriptsCacheSize;
private ListeningExecutorService executor; private ListeningExecutorService executor;
protected DefaultMvelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) { private final Lock lock = new ReentrantLock();
protected DefaultTbelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
super(apiUsageStateClient, apiUsageReportClient); super(apiUsageStateClient, apiUsageReportClient);
} }
@Scheduled(fixedDelayString = "${mvel.stats.print_interval_ms:10000}") @Scheduled(fixedDelayString = "${tbel.stats.print_interval_ms:10000}")
public void printStats() { public void printStats() {
super.printStats(); super.printStats();
} }
@ -112,14 +127,17 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
parserConfig.addImport("JSON", TbJson.class); parserConfig.addImport("JSON", TbJson.class);
parserConfig.registerDataType("Date", TbDate.class, date -> 8L); parserConfig.registerDataType("Date", TbDate.class, date -> 8L);
TbUtils.register(parserConfig); TbUtils.register(parserConfig);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "mvel-executor")); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
try { try {
// Special command to warm up MVEL engine // Special command to warm up TBEL engine
Serializable script = MVEL.compileExpression("var warmUp = {}; warmUp", new SandboxedParserContext(parserConfig)); Serializable script = compileScript("var warmUp = {}; warmUp");
MVEL.executeTbExpression(script, new ExecutionContext(parserConfig), Collections.emptyMap()); MVEL.executeTbExpression(script, new ExecutionContext(parserConfig), Collections.emptyMap());
} catch (Exception e) { } catch (Exception e) {
// do nothing // do nothing
} }
compiledScriptsCache = Caffeine.newBuilder()
.maximumSize(compiledScriptsCacheSize)
.build();
} }
@PreDestroy @PreDestroy
@ -131,7 +149,7 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Override @Override
protected String getStatsName() { protected String getStatsName() {
return "MVEL Scripts Stats"; return "TBEL Scripts Stats";
} }
@Override @Override
@ -141,16 +159,22 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Override @Override
protected boolean isScriptPresent(UUID scriptId) { protected boolean isScriptPresent(UUID scriptId) {
return scriptMap.containsKey(scriptId); return scriptIdToHash.containsKey(scriptId);
} }
@Override @Override
protected ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) { protected ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) {
return executor.submit(() -> { return executor.submit(() -> {
try { try {
Serializable compiledScript = MVEL.compileExpression(scriptBody, new SandboxedParserContext(parserConfig)); String scriptHash = hash(scriptBody, argNames);
MvelScript script = new MvelScript(compiledScript, scriptBody, argNames); compiledScriptsCache.get(scriptHash, k -> compileScript(scriptBody));
scriptMap.put(scriptId, script); lock.lock();
try {
scriptIdToHash.put(scriptId, scriptHash);
scriptMap.computeIfAbsent(scriptHash, k -> new TbelScript(scriptBody, argNames));
} finally {
lock.unlock();
}
return scriptId; return scriptId;
} catch (Exception e) { } catch (Exception e) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e);
@ -159,15 +183,17 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
} }
@Override @Override
protected MvelScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) { protected TbelScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) {
ExecutionContext executionContext = new ExecutionContext(this.parserConfig, maxMemoryLimitMb * 1024 * 1024); ExecutionContext executionContext = new ExecutionContext(this.parserConfig, maxMemoryLimitMb * 1024 * 1024);
return new MvelScriptExecutionTask(executionContext, executor.submit(() -> { return new TbelScriptExecutionTask(executionContext, executor.submit(() -> {
MvelScript script = scriptMap.get(scriptId); String scriptHash = scriptIdToHash.get(scriptId);
if (script == null) { if (scriptHash == null) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException("Script not found!")); throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException("Script not found!"));
} }
TbelScript script = scriptMap.get(scriptHash);
Serializable compiledScript = compiledScriptsCache.get(scriptHash, k -> compileScript(script.getScriptBody()));
try { try {
return MVEL.executeTbExpression(script.getCompiledScript(), executionContext, script.createVars(args)); return MVEL.executeTbExpression(compiledScript, executionContext, script.createVars(args));
} catch (ScriptMemoryOverflowException e) { } catch (ScriptMemoryOverflowException e) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, script.getScriptBody(), new RuntimeException("Script memory overflow!")); throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, script.getScriptBody(), new RuntimeException("Script memory overflow!"));
} catch (Exception e) { } catch (Exception e) {
@ -177,7 +203,33 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
} }
@Override @Override
protected void doRelease(UUID scriptId) throws Exception { protected void doRelease(UUID scriptId) {
scriptMap.remove(scriptId); String scriptHash = scriptIdToHash.remove(scriptId);
if (scriptHash != null) {
lock.lock();
try {
if (!scriptIdToHash.containsValue(scriptHash)) {
scriptMap.remove(scriptHash);
compiledScriptsCache.invalidate(scriptHash);
}
} finally {
lock.unlock();
}
}
} }
private Serializable compileScript(String scriptBody) {
return MVEL.compileExpression(scriptBody, new SandboxedParserContext(parserConfig));
}
@SuppressWarnings("UnstableApiUsage")
protected String hash(String scriptBody, String[] argNames) {
Hasher hasher = Hashing.murmur3_128().newHasher();
hasher.putUnencodedChars(scriptBody);
for (String argName : argNames) {
hasher.putString(argName, StandardCharsets.UTF_8);
}
return hasher.hash().toString();
}
} }

4
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbDate.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.script.api.mvel; package org.thingsboard.script.api.tbel;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -60,7 +60,7 @@ public class TbDate extends Date {
} }
public String toISOString() { public String toISOString() {
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sssZ"); DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
return formatter.format(this); return formatter.format(this);
} }

2
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbJson.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbJson.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.script.api.mvel; package org.thingsboard.script.api.tbel;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.mvel2.ExecutionContext; import org.mvel2.ExecutionContext;

104
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbUtils.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -13,19 +13,25 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.script.api.mvel; package org.thingsboard.script.api.tbel;
import org.mvel2.ExecutionContext; import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration; import org.mvel2.ParserConfiguration;
import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.util.MethodStub; import org.mvel2.util.MethodStub;
import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
public class TbUtils { public class TbUtils {
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
public static void register(ParserConfiguration parserConfig) throws Exception { public static void register(ParserConfiguration parserConfig) throws Exception {
parserConfig.addImport("btoa", new MethodStub(TbUtils.class.getMethod("btoa", parserConfig.addImport("btoa", new MethodStub(TbUtils.class.getMethod("btoa",
String.class))); String.class)));
@ -35,6 +41,10 @@ public class TbUtils {
List.class))); List.class)));
parserConfig.addImport("bytesToString", new MethodStub(TbUtils.class.getMethod("bytesToString", parserConfig.addImport("bytesToString", new MethodStub(TbUtils.class.getMethod("bytesToString",
List.class, String.class))); List.class, String.class)));
parserConfig.addImport("decodeToString", new MethodStub(TbUtils.class.getMethod("bytesToString",
List.class)));
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
ExecutionContext.class, List.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
ExecutionContext.class, String.class))); ExecutionContext.class, String.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
@ -47,10 +57,24 @@ public class TbUtils {
String.class))); String.class)));
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble", parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
String.class))); String.class)));
} parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt",
String.class)));
public static void main(String[] args) { parserConfig.addImport("parseBigEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToInt",
System.out.println(Integer.class == int.class); String.class)));
parserConfig.addImport("parseHexToInt", new MethodStub(TbUtils.class.getMethod("parseHexToInt",
String.class)));
parserConfig.addImport("parseHexToInt", new MethodStub(TbUtils.class.getMethod("parseHexToInt",
String.class, boolean.class)));
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
double.class, int.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class)));
parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex",
String.class)));
parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex",
byte[].class)));
parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex",
ExecutionArrayList.class)));
} }
public static String btoa(String input) { public static String btoa(String input) {
@ -61,6 +85,10 @@ public class TbUtils {
return new String(Base64.getDecoder().decode(encoded)); return new String(Base64.getDecoder().decode(encoded));
} }
public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException {
return TbJson.parse(ctx, bytesToString(bytesList));
}
public static String bytesToString(List<Byte> bytesList) { public static String bytesToString(List<Byte> bytesList) {
byte[] bytes = bytesFromList(bytesList); byte[] bytes = bytesFromList(bytesList);
return new String(bytes); return new String(bytes);
@ -149,6 +177,72 @@ public class TbUtils {
return null; return null;
} }
public static int parseLittleEndianHexToInt(String hex) {
return parseHexToInt(hex, false);
}
public static int parseBigEndianHexToInt(String hex) {
return parseHexToInt(hex, true);
}
public static int parseHexToInt(String hex) {
return parseHexToInt(hex, true);
}
public static int parseHexToInt(String hex, boolean bigEndian) {
int length = hex.length();
if (length > 8) {
throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed.");
}
if (bigEndian) {
return Integer.parseInt(hex, 16);
} else {
if (length < 8) {
hex = hex + "0".repeat(8 - length);
}
return Integer.reverseBytes(Integer.parseInt(hex, 16));
}
}
public static ExecutionArrayList<Integer> hexToBytes(ExecutionContext ctx, String hex) {
int len = hex.length();
if (len % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length.");
}
ExecutionArrayList<Integer> data = new ExecutionArrayList<>(ctx);
for (int i = 0; i < len; i += 2) {
data.add((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
public static String base64ToHex(String base64) {
return bytesToHex(Base64.getDecoder().decode(base64));
}
public static String bytesToHex(ExecutionArrayList<?> bytesList) {
byte[] bytes = new byte[bytesList.size()];
for (int i = 0; i < bytesList.size(); i++) {
bytes[i] = Byte.parseByte(bytesList.get(i).toString());
}
return bytesToHex(bytes);
}
public static String bytesToHex(byte[] bytes) {
byte[] hexChars = new byte[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars, StandardCharsets.UTF_8);
}
public static double toFixed(double value, int precision) {
return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue();
}
private static boolean isHexadecimal(String value) { private static boolean isHexadecimal(String value) {
return value != null && (value.contains("0x") || value.contains("0X")); return value != null && (value.contains("0x") || value.contains("0X"));
} }

6
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelInvokeService.java

@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.script.api.mvel; package org.thingsboard.script.api.tbel;
import org.thingsboard.script.api.ScriptInvokeService; import org.thingsboard.script.api.ScriptInvokeService;
import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.script.ScriptLanguage;
public interface MvelInvokeService extends ScriptInvokeService { public interface TbelInvokeService extends ScriptInvokeService {
@Override @Override
default ScriptLanguage getLanguage() { default ScriptLanguage getLanguage() {
return ScriptLanguage.MVEL; return ScriptLanguage.TBEL;
} }
} }

6
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelScript.java

@ -13,18 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.script.api.mvel; package org.thingsboard.script.api.tbel;
import lombok.Data; import lombok.Data;
import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Data @Data
public class MvelScript { public class TbelScript {
private final Serializable compiledScript;
private final String scriptBody; private final String scriptBody;
private final String[] argNames; private final String[] argNames;

8
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScriptExecutionTask.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelScriptExecutionTask.java

@ -13,20 +13,18 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.script.api.mvel; package org.thingsboard.script.api.tbel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.Getter;
import org.mvel2.ExecutionContext; import org.mvel2.ExecutionContext;
import org.thingsboard.script.api.TbScriptExecutionTask; import org.thingsboard.script.api.TbScriptExecutionTask;
public class MvelScriptExecutionTask extends TbScriptExecutionTask { public class TbelScriptExecutionTask extends TbScriptExecutionTask {
private final ExecutionContext context; private final ExecutionContext context;
public MvelScriptExecutionTask(ExecutionContext context, ListenableFuture<Object> resultFuture) { public TbelScriptExecutionTask(ExecutionContext context, ListenableFuture<Object> resultFuture) {
super(resultFuture); super(resultFuture);
this.context = context; this.context = context;
} }

11
dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.service; package org.thingsboard.server.dao.service;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.owasp.validator.html.AntiSamy; import org.owasp.validator.html.AntiSamy;
import org.owasp.validator.html.Policy; import org.owasp.validator.html.Policy;
@ -48,12 +49,18 @@ public class NoXssValidator implements ConstraintValidator<NoXss, Object> {
@Override @Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (!(value instanceof String) || ((String) value).isEmpty()) { String stringValue;
if (value instanceof CharSequence || value instanceof JsonNode) {
stringValue = value.toString();
} else {
return true;
}
if (stringValue.isEmpty()) {
return true; return true;
} }
try { try {
return xssChecker.scan((String) value, xssPolicy).getNumberOfErrors() == 0; return xssChecker.scan(stringValue, xssPolicy).getNumberOfErrors() == 0;
} catch (ScanException | PolicyException e) { } catch (ScanException | PolicyException e) {
return false; return false;
} }

19
dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java

@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
@ -66,6 +67,24 @@ public class UserDataValidator extends DataValidator<User> {
} }
} }
@Override
protected User validateUpdate(TenantId tenantId, User user) {
User old = userDao.findById(user.getTenantId(), user.getId().getId());
if (old == null) {
throw new DataValidationException("Can't update non existing user!");
}
if (!old.getTenantId().equals(user.getTenantId())) {
throw new DataValidationException("Can't update user tenant id!");
}
if (!old.getAuthority().equals(user.getAuthority())) {
throw new DataValidationException("Can't update user authority!");
}
if (!old.getCustomerId().equals(user.getCustomerId())) {
throw new DataValidationException("Can't update user customer id!");
}
return old;
}
@Override @Override
protected void validateDataImpl(TenantId requestTenantId, User user) { protected void validateDataImpl(TenantId requestTenantId, User user) {
if (StringUtils.isEmpty(user.getEmail())) { if (StringUtils.isEmpty(user.getEmail())) {

17
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java

@ -140,6 +140,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository {
selectPart.append(" a.originator_id as entity_id "); selectPart.append(" a.originator_id as entity_id ");
} }
EntityDataSortOrder sortOrder = pageLink.getSortOrder(); EntityDataSortOrder sortOrder = pageLink.getSortOrder();
String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch());
if (sortOrder != null && sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { if (sortOrder != null && sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) {
String sortOrderKey = sortOrder.getKey().getKey(); String sortOrderKey = sortOrder.getKey().getKey();
sortPart.append(alarmFieldColumnMap.getOrDefault(sortOrderKey, sortOrderKey)) sortPart.append(alarmFieldColumnMap.getOrDefault(sortOrderKey, sortOrderKey))
@ -166,7 +167,11 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository {
} }
joinPart.append(" as e(id, priority)) e "); joinPart.append(" as e(id, priority)) e ");
if (pageLink.isSearchPropagatedAlarms()) { if (pageLink.isSearchPropagatedAlarms()) {
joinPart.append("on ea.entity_id = e.id"); if (textSearchQuery.isEmpty()) {
joinPart.append("on ea.entity_id = e.id");
} else {
joinPart.append("on a.entity_id = e.id");
}
} else { } else {
joinPart.append("on a.originator_id = e.id"); joinPart.append("on a.originator_id = e.id");
} }
@ -230,13 +235,11 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository {
} }
} }
String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch()); String mainQuery = String.format("%s%s", selectPart, fromPart);
String mainQuery; if (textSearchQuery.isEmpty()) {
if (!textSearchQuery.isEmpty()) { mainQuery = String.format("%s%s%s", mainQuery, joinPart, wherePart);
mainQuery = selectPart.toString() + fromPart.toString() + wherePart.toString();
mainQuery = String.format("select * from (%s) a %s WHERE %s", mainQuery, joinPart, textSearchQuery);
} else { } else {
mainQuery = selectPart.toString() + fromPart.toString() + joinPart.toString() + wherePart.toString(); mainQuery = String.format("select * from (%s%s) a %s WHERE %s", mainQuery, wherePart, joinPart, textSearchQuery);
} }
String countQuery = String.format("select count(*) from (%s) result", mainQuery); String countQuery = String.format("select count(*) from (%s) result", mainQuery);
long queryTs = System.currentTimeMillis(); long queryTs = System.currentTimeMillis();

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

@ -105,11 +105,6 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
@Override
public ListenableFuture<Void> removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
return Futures.immediateFuture(null);
}
@Override @Override
public ListenableFuture<List<ReadTsKvQueryResult>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) { public ListenableFuture<List<ReadTsKvQueryResult>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
return processFindAllAsync(tenantId, entityId, queries); return processFindAllAsync(tenantId, entityId, queries);

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

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.sqlts.timescale;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
@ -36,7 +35,6 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao; import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao;
@ -52,7 +50,6 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function; import java.util.function.Function;
@Component @Component
@ -144,11 +141,6 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
}); });
} }
@Override
public ListenableFuture<Void> removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
return service.submit(() -> null);
}
@Override @Override
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) { if (query.getAggregation() == Aggregation.NONE) {

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

@ -46,7 +46,6 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -277,7 +276,6 @@ public class BaseTimeseriesService implements TimeseriesService {
private void deleteAndRegisterFutures(TenantId tenantId, List<ListenableFuture<TsKvLatestRemovingResult>> futures, EntityId entityId, DeleteTsKvQuery query) { private void deleteAndRegisterFutures(TenantId tenantId, List<ListenableFuture<TsKvLatestRemovingResult>> futures, EntityId entityId, DeleteTsKvQuery query) {
futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor())); futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor()));
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query));
futures.add(Futures.transform(timeseriesDao.removePartition(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor()));
} }
private static void validate(EntityId entityId) { private static void validate(EntityId entityId) {

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

@ -28,6 +28,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -59,7 +60,6 @@ import java.time.LocalDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -82,17 +82,21 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
protected static final int MIN_AGGREGATION_STEP_MS = 1000; protected static final int MIN_AGGREGATION_STEP_MS = 1000;
public static final String ASC_ORDER = "ASC"; public static final String ASC_ORDER = "ASC";
public static final long SECONDS_IN_DAY = TimeUnit.DAYS.toSeconds(1); public static final long SECONDS_IN_DAY = TimeUnit.DAYS.toSeconds(1);
protected static final List<Long> FIXED_PARTITION = List.of(0L);
protected static List<Long> FIXED_PARTITION = Arrays.asList(new Long[]{0L});
private CassandraTsPartitionsCache cassandraTsPartitionsCache; private CassandraTsPartitionsCache cassandraTsPartitionsCache;
@Autowired @Autowired
private Environment environment; private Environment environment;
@Getter
@Value("${cassandra.query.ts_key_value_partitioning}") @Value("${cassandra.query.ts_key_value_partitioning}")
private String partitioning; private String partitioning;
@Getter
@Value("${cassandra.query.use_ts_key_value_partitioning_on_read:true}")
private boolean useTsKeyValuePartitioningOnRead;
@Value("${cassandra.query.ts_key_value_partitions_max_cache_size:100000}") @Value("${cassandra.query.ts_key_value_partitions_max_cache_size:100000}")
private long partitionsCacheSize; private long partitionsCacheSize;
@ -222,46 +226,6 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
return resultFuture; return resultFuture;
} }
@Override
public ListenableFuture<Void> removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
long minPartition = toPartitionTs(query.getStartTs());
long maxPartition = toPartitionTs(query.getEndTs());
if (minPartition == maxPartition) {
return Futures.immediateFuture(null);
} else {
TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition);
final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>();
final ListenableFuture<List<Long>> partitionsListFuture = Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() {
@Override
public void onSuccess(@Nullable List<Long> partitions) {
int index = 0;
if (minPartition != query.getStartTs()) {
index = 1;
}
List<Long> partitionsToDelete = new ArrayList<>();
for (int i = index; i < partitions.size() - 1; i++) {
partitionsToDelete.add(partitions.get(i));
}
QueryCursor cursor = new QueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitionsToDelete);
deletePartitionAsync(tenantId, cursor, resultFuture);
for (Long partition : partitionsToDelete) {
cassandraTsPartitionsCache.invalidate(new CassandraPartitionCacheKey(entityId, query.getKey(), partition));
}
}
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t);
}
}, readResultsProcessingExecutor);
return resultFuture;
}
}
@Override @Override
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) { if (query.getAggregation() == Aggregation.NONE) {
@ -337,7 +301,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
private long toPartitionTs(long ts) { long toPartitionTs(long ts) {
LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC); LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC);
return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli(); return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli();
} }
@ -417,10 +381,37 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
if (isFixedPartitioning()) { //no need to fetch partitions from DB if (isFixedPartitioning()) { //no need to fetch partitions from DB
return Futures.immediateFuture(FIXED_PARTITION); return Futures.immediateFuture(FIXED_PARTITION);
} }
if (!isUseTsKeyValuePartitioningOnRead()) {
return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition));
}
TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition);
return Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); return Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
} }
List<Long> calculatePartitions(long minPartition, long maxPartition) {
if (minPartition == maxPartition) {
return Collections.singletonList(minPartition);
}
List<Long> partitions = new ArrayList<>();
long currentPartition = minPartition;
LocalDateTime currentPartitionTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(currentPartition), ZoneOffset.UTC);
while (maxPartition > currentPartition) {
partitions.add(currentPartition);
currentPartitionTime = calculateNextPartition(currentPartitionTime);
currentPartition = currentPartitionTime.toInstant(ZoneOffset.UTC).toEpochMilli();
}
partitions.add(maxPartition);
return partitions;
}
private LocalDateTime calculateNextPartition(LocalDateTime time) {
return time.plus(1, tsFormat.getTruncateUnit());
}
private AsyncFunction<List<Long>, List<TbResultSet>> getFetchChunksAsyncFunction(TenantId tenantId, EntityId entityId, String key, Aggregation aggregation, long startTs, long endTs) { private AsyncFunction<List<Long>, List<TbResultSet>> getFetchChunksAsyncFunction(TenantId tenantId, EntityId entityId, String key, Aggregation aggregation, long startTs, long endTs) {
return partitions -> { return partitions -> {
try { try {

3
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java

@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -39,7 +38,5 @@ public interface TimeseriesDao {
ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
ListenableFuture<Void> removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
void cleanup(long systemTtl); void cleanup(long systemTtl);
} }

39
dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java

@ -15,23 +15,16 @@
*/ */
package org.thingsboard.server.dao.service; package org.thingsboard.server.dao.service;
import org.junit.jupiter.api.BeforeAll; import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.asset.Asset;
import javax.validation.ConstraintValidatorContext; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.mock;
public class NoXssValidatorTest { public class NoXssValidatorTest {
private static NoXssValidator validator;
@BeforeAll
public static void beforeAll() {
validator = new NoXssValidator();
validator.initialize(null);
}
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = { @ValueSource(strings = {
@ -44,9 +37,25 @@ public class NoXssValidatorTest {
" <img src= \"http://site.com/\" > ", " <img src= \"http://site.com/\" > ",
"123 <input type=text value=a onfocus=alert(1337) AUTOFOCUS>bebe" "123 <input type=text value=a onfocus=alert(1337) AUTOFOCUS>bebe"
}) })
public void testIsNotValid(String stringWithXss) { public void givenEntityWithMaliciousPropertyValue_thenReturnValidationError(String maliciousString) {
boolean isValid = validator.isValid(stringWithXss, mock(ConstraintValidatorContext.class)); Asset invalidAsset = new Asset();
assertFalse(isValid); invalidAsset.setName(maliciousString);
assertThatThrownBy(() -> {
ConstraintValidator.validateFields(invalidAsset);
}).hasMessageContaining("field value is malformed");
}
@Test
public void givenEntityWithMaliciousValueInAdditionalInfo_thenReturnValidationError() {
Asset invalidAsset = new Asset();
String maliciousValue = "qwerty<script>alert(document.cookie)</script>qwerty";
invalidAsset.setAdditionalInfo(JacksonUtil.newObjectNode()
.set("description", new TextNode(maliciousValue)));
assertThatThrownBy(() -> {
ConstraintValidator.validateFields(invalidAsset);
}).hasMessageContaining("field value is malformed");
} }
} }

133
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningDaysAlwaysExistsTest.java

@ -0,0 +1,133 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=DAYS",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningDaysAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsDays() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("DAYS");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:00:01Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T00:00:00Z").getTime());
}
@Test
public void testCalculatePartitionsDays() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T23:59:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-15T00:00:00Z").getTime());
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(6).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-13T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-14T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-15T00:00:00Z").getTime()));
long leapStartTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-27T00:00:00Z").getTime());
long leapEndTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime());
assertThat(tsDao.calculatePartitions(leapStartTs, leapEndTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-27T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-28T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-29T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime()));
long newYearStartTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-30T00:00:00Z").getTime());
long newYearEndTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime());
assertThat(tsDao.calculatePartitions(newYearStartTs, newYearEndTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-30T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-31T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime()));
}
}

134
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningHoursAlwaysExistsTest.java

@ -0,0 +1,134 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=HOURS",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningHoursAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsHours() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("HOURS");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T01:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T01:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T02:00:01Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T02:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:00:00Z").getTime());
}
@Test
public void testCalculatePartitionsHours() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:59:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:59:00Z").getTime());
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T01:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T02:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(25).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T01:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T02:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T04:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T05:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T06:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T07:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T08:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T09:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T10:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T11:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T12:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T13:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T14:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T15:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T16:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T17:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T18:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T19:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T20:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T21:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T22:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T23:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime()));
}
}

78
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningIndefiniteAlwaysExistsTest.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=INDEFINITE",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningIndefiniteAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsIndefinite() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("INDEFINITE");
assertThat(tsDao.toPartitionTs(ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(0L);
}
@Test
public void testCalculatePartitionsIndefinite() throws ParseException {
//Indefinite partitioning should never call tsDao.calculatePartitions()
}
}

121
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java

@ -0,0 +1,121 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=MINUTES",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsMinutes() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("MINUTES");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:01:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:01:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:02:01Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:02:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:00Z").getTime());
}
@Test
public void testCalculatePartitionsMinutes() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime());
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:01:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(11).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:01:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:03:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:04:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:05:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:06:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:07:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:08:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:09:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime()));
}
}

134
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java

@ -0,0 +1,134 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=MONTHS",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsMonths() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("MONTHS");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(1640995200000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime())).isEqualTo(1651363200000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:01Z").getTime())).isEqualTo(1651363200000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(1651363200000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(1701388800000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-01T00:00:00Z").getTime());
}
@Test
public void testCalculatePartitionsMonths() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-31T23:59:59Z").getTime());
long leapTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-29T23:59:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime());
log.info("startTs {}, nextTs {}, leapTs {}, endTs {}", startTs, nextTs, leapTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(1575158400000L)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(1575158400000L, 1577836800000L)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, leapTs)).isEqualTo(List.of(1575158400000L, 1577836800000L, 1580515200000L)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(14).isEqualTo(List.of(
1575158400000L,
1577836800000L, 1580515200000L, 1583020800000L,
1585699200000L, 1588291200000L, 1590969600000L,
1593561600000L, 1596240000000L, 1598918400000L,
1601510400000L, 1604188800000L, 1606780800000L,
1609459200000L)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-04-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-05-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-06-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-07-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-08-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-09-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-10-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-11-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime()));
}
}

116
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java

@ -0,0 +1,116 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=YEARS",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsYears() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("YEARS");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:01Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-01-01T00:00:00Z").getTime());
}
@Test
public void testCalculatePartitionsYears() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-10-12T23:59:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2025-07-15T00:00:00Z").getTime());
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(7).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2024-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2025-01-01T00:00:00Z").getTime()));
}
}

3
docker/tb-js-executor.env

@ -3,4 +3,5 @@ LOGGER_LEVEL=info
LOG_FOLDER=logs LOG_FOLDER=logs
LOGGER_FILENAME=tb-js-executor-%DATE%.log LOGGER_FILENAME=tb-js-executor-%DATE%.log
DOCKER_MODE=true DOCKER_MODE=true
SCRIPT_BODY_TRACE_FREQUENCY=1000 SCRIPT_BODY_TRACE_FREQUENCY=1000
NODE_OPTIONS="--max-old-space-size=200"

1
lombok.config

@ -1,2 +1,3 @@
config.stopbubbling = true config.stopbubbling = true
lombok.anyconstructor.addconstructorproperties = true lombok.anyconstructor.addconstructorproperties = true
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy

4
msa/black-box-tests/README.md

@ -30,5 +30,9 @@ As result, in REPOSITORY column, next images should be present:
mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.hybridMode=true mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.hybridMode=true
To run the black box tests with using local env run tests in the [msa/black-box-tests](../black-box-tests) directory with runLocal property:
mvn clean install -DblackBoxTests.skip=false -DrunLocal=true

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

@ -57,11 +57,6 @@
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.takari.junit</groupId>
<artifactId>takari-cpsuite</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
@ -72,6 +67,26 @@
<artifactId>junit-vintage-engine</artifactId> <artifactId>junit-vintage-engine</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.awaitility</groupId> <groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId> <artifactId>awaitility</artifactId>
@ -152,11 +167,18 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<configuration> <configuration>
<includes> <suiteXmlFiles>
<include>**/*TestSuite.java</include> <suiteXmlFile>src/test/resources/testNG.xml</suiteXmlFile>
</includes> </suiteXmlFiles>
<skipTests>${blackBoxTests.skip}</skipTests> <skipTests>${blackBoxTests.skip}</skipTests>
</configuration> </configuration>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-testng</artifactId>
<version>${surefire.version}</version>
</dependency>
</dependencies>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>

130
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java

@ -15,115 +15,59 @@
*/ */
package org.thingsboard.server.msa; package org.thingsboard.server.msa;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts; import org.apache.http.ssl.SSLContexts;
import org.junit.BeforeClass; import org.testng.annotations.AfterSuite;
import org.junit.Rule; import org.testng.annotations.BeforeSuite;
import org.junit.rules.TestRule; import org.testng.annotations.Listeners;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.thingsboard.rest.client.RestClient;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
import javax.net.ssl.SSLContext;
import java.net.URI; import java.net.URI;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
@Slf4j @Slf4j
@Listeners(TestListener.class)
public abstract class AbstractContainerTest { public abstract class AbstractContainerTest {
protected static final String HTTPS_URL = "https://localhost";
protected static final String WSS_URL = "wss://localhost";
protected static String TB_TOKEN;
protected static RestClient restClient;
protected static long timeoutMultiplier = 1; protected static long timeoutMultiplier = 1;
protected ObjectMapper mapper = new ObjectMapper(); protected ObjectMapper mapper = new ObjectMapper();
protected JsonParser jsonParser = new JsonParser(); private static final ContainerTestSuite containerTestSuite = ContainerTestSuite.getInstance();
protected static TestRestClient testRestClient;
@BeforeClass
public static void before() throws Exception {
restClient = new RestClient(HTTPS_URL);
restClient.getRestTemplate().setRequestFactory(getRequestFactoryForSelfSignedCert());
@BeforeSuite
public void beforeSuite() {
if ("false".equals(System.getProperty("runLocal", "false"))) {
containerTestSuite.start();
}
testRestClient = new TestRestClient(TestProperties.getBaseUrl());
if (!"kafka".equals(System.getProperty("blackBoxTests.queue", "kafka"))) { if (!"kafka".equals(System.getProperty("blackBoxTests.queue", "kafka"))) {
timeoutMultiplier = 10; timeoutMultiplier = 10;
} }
} }
@Rule @AfterSuite
public TestRule watcher = new TestWatcher() { public void afterSuite() {
protected void starting(Description description) { if (containerTestSuite.isActive()) {
log.info("================================================="); containerTestSuite.stop();
log.info("STARTING TEST: {}" , description.getMethodName());
log.info("=================================================");
} }
/**
* Invoked when a test succeeds
*/
protected void succeeded(Description description) {
log.info("=================================================");
log.info("SUCCEEDED TEST: {}" , description.getMethodName());
log.info("=================================================");
}
/**
* Invoked when a test fails
*/
protected void failed(Throwable e, Description description) {
log.info("=================================================");
log.info("FAILED TEST: {}" , description.getMethodName(), e);
log.info("=================================================");
}
};
protected Device createGatewayDevice() throws JsonProcessingException {
String isGateway = "{\"gateway\":true}";
ObjectMapper objectMapper = new ObjectMapper();
JsonNode additionalInfo = objectMapper.readTree(isGateway);
Device gatewayDeviceTemplate = new Device();
gatewayDeviceTemplate.setName("mqtt_gateway");
gatewayDeviceTemplate.setType("gateway");
gatewayDeviceTemplate.setAdditionalInfo(additionalInfo);
return restClient.saveDevice(gatewayDeviceTemplate);
}
protected Device createDevice(String name) {
Device device = new Device();
device.setName(name + StringUtils.randomAlphanumeric(7));
device.setType("DEFAULT");
return restClient.saveDevice(device);
} }
protected WsClient subscribeToWebSocket(DeviceId deviceId, String scope, CmdsType property) throws Exception { protected WsClient subscribeToWebSocket(DeviceId deviceId, String scope, CmdsType property) throws Exception {
WsClient wsClient = new WsClient(new URI(WSS_URL + "/api/ws/plugins/telemetry?token=" + restClient.getToken()), timeoutMultiplier); String webSocketUrl = TestProperties.getWebSocketUrl();
SSLContextBuilder builder = SSLContexts.custom(); WsClient wsClient = new WsClient(new URI(webSocketUrl + "/api/ws/plugins/telemetry?token=" + testRestClient.getToken()), timeoutMultiplier);
builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); if (webSocketUrl.matches("^(wss)://.*$")) {
wsClient.setSocketFactory(builder.build().getSocketFactory()); SSLContextBuilder builder = SSLContexts.custom();
builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
wsClient.setSocketFactory(builder.build().getSocketFactory());
}
wsClient.connectBlocking(); wsClient.connectBlocking();
JsonObject cmdsObject = new JsonObject(); JsonObject cmdsObject = new JsonObject();
@ -150,16 +94,6 @@ public abstract class AbstractContainerTest {
.build(); .build();
} }
protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, Long expectedTs, String expectedValue) {
List<Object> list = wsTelemetryResponse.getDataValuesByKey(key);
return expectedTs.equals(list.get(0)) && expectedValue.equals(list.get(1));
}
protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, String expectedValue) {
List<Object> list = wsTelemetryResponse.getDataValuesByKey(key);
return expectedValue.equals(list.get(1));
}
protected JsonObject createGatewayConnectPayload(String deviceName){ protected JsonObject createGatewayConnectPayload(String deviceName){
JsonObject payload = new JsonObject(); JsonObject payload = new JsonObject();
payload.addProperty("device", deviceName); payload.addProperty("device", deviceName);
@ -216,20 +150,4 @@ public abstract class AbstractContainerTest {
} }
} }
private static HttpComponentsClientHttpRequestFactory getRequestFactoryForSelfSignedCert() throws Exception {
SSLContextBuilder builder = SSLContexts.custom();
builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
SSLContext sslContext = builder.build();
SSLConnectionSocketFactory sslSelfSigned = new SSLConnectionSocketFactory(sslContext, (s, sslSession) -> true);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
.<ConnectionSocketFactory>create()
.register("https", sslSelfSigned)
.build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
return new HttpComponentsClientHttpRequestFactory(httpClient);
}
} }

221
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java

@ -17,9 +17,6 @@ package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.junit.ClassRule;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.Wait;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -41,10 +38,8 @@ import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail; import static org.testng.Assert.fail;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"})
@Slf4j @Slf4j
public class ContainerTestSuite { public class ContainerTestSuite {
final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster")); final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster"));
@ -57,107 +52,133 @@ public class ContainerTestSuite {
private static final String TB_JS_EXECUTOR_LOG_REGEXP = ".*template started.*"; private static final String TB_JS_EXECUTOR_LOG_REGEXP = ".*template started.*";
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(400); private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(400);
private static DockerComposeContainer<?> testContainer; private DockerComposeContainer<?> testContainer;
private ThingsBoardDbInstaller installTb;
@ClassRule private boolean isActive;
public static ThingsBoardDbInstaller installTb = new ThingsBoardDbInstaller();
private static ContainerTestSuite containerTestSuite;
@ClassRule
public static DockerComposeContainer getTestContainer() { public boolean isActive() {
if (testContainer == null) { return isActive;
log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER); }
log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE);
boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers")); public void setActive(boolean active) {
try { isActive = active;
final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/"; }
log.info("targetDir {}", targetDir);
FileUtils.copyDirectory(new File(SOURCE_DIR), new File(targetDir)); private ContainerTestSuite() {
replaceInFile(targetDir + "docker-compose.yml", " container_name: \"${LOAD_BALANCER_NAME}\"", "", "container_name"); }
FileUtils.copyDirectory(new File("src/test/resources"), new File(targetDir)); public static ContainerTestSuite getInstance() {
if (containerTestSuite == null) {
class DockerComposeContainerImpl<SELF extends DockerComposeContainer<SELF>> extends DockerComposeContainer<SELF> { containerTestSuite = new ContainerTestSuite();
public DockerComposeContainerImpl(List<File> composeFiles) { }
super(composeFiles); return containerTestSuite;
} }
@Override public void start() {
public void stop() { installTb = new ThingsBoardDbInstaller();
super.stop(); installTb.createVolumes();
tryDeleteDir(targetDir); log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER);
} log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE);
} boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers"));
try {
final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/";
log.info("targetDir {}", targetDir);
FileUtils.copyDirectory(new File(SOURCE_DIR), new File(targetDir));
replaceInFile(targetDir + "docker-compose.yml", " container_name: \"${LOAD_BALANCER_NAME}\"", "", "container_name");
List<File> composeFiles = new ArrayList<>(Arrays.asList( FileUtils.copyDirectory(new File("src/test/resources"), new File(targetDir));
new File(targetDir + "docker-compose.yml"),
new File(targetDir + "docker-compose.volumes.yml"), class DockerComposeContainerImpl<SELF extends DockerComposeContainer<SELF>> extends DockerComposeContainer<SELF> {
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")), public DockerComposeContainerImpl(List<File> composeFiles) {
new File(targetDir + "docker-compose.postgres.volumes.yml"), super(composeFiles);
new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"),
new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")),
new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.volumes.yml" : "docker-compose.redis.volumes.yml"))
));
Map<String, String> queueEnv = new HashMap<>();
queueEnv.put("TB_QUEUE_TYPE", QUEUE_TYPE);
switch (QUEUE_TYPE) {
case "kafka":
composeFiles.add(new File(targetDir + "docker-compose.kafka.yml"));
break;
case "aws-sqs":
replaceInFile(targetDir, "queue-aws-sqs.env",
Map.of("YOUR_KEY", getSysProp("blackBoxTests.awsKey"),
"YOUR_SECRET", getSysProp("blackBoxTests.awsSecret"),
"YOUR_REGION", getSysProp("blackBoxTests.awsRegion")));
break;
case "rabbitmq":
composeFiles.add(new File(targetDir + "docker-compose.rabbitmq-server.yml"));
replaceInFile(targetDir, "queue-rabbitmq.env",
Map.of("localhost", "rabbitmq"));
break;
case "service-bus":
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_NAMESPACE_NAME", getSysProp("blackBoxTests.serviceBusNamespace"),
"YOUR_SAS_KEY_NAME", getSysProp("blackBoxTests.serviceBusSASPolicy")));
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_SAS_KEY", getSysProp("blackBoxTests.serviceBusPrimaryKey")));
break;
case "pubsub":
replaceInFile(targetDir, "queue-pubsub.env",
Map.of("YOUR_PROJECT_ID", getSysProp("blackBoxTests.pubSubProjectId"),
"YOUR_SERVICE_ACCOUNT", getSysProp("blackBoxTests.pubSubServiceAccount")));
break;
default:
throw new RuntimeException("Unsupported queue type: " + QUEUE_TYPE);
} }
if (IS_HYBRID_MODE) { @Override
composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml")); public void stop() {
super.stop();
tryDeleteDir(targetDir);
} }
}
testContainer = new DockerComposeContainerImpl<>(composeFiles) List<File> composeFiles = new ArrayList<>(Arrays.asList(
.withPull(false) new File(targetDir + "docker-compose.yml"),
.withLocalCompose(true) new File(targetDir + "docker-compose.volumes.yml"),
.withTailChildContainers(!skipTailChildContainers) new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")),
.withEnv(installTb.getEnv()) new File(targetDir + "docker-compose.postgres.volumes.yml"),
.withEnv(queueEnv) new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"),
.withEnv("LOAD_BALANCER_NAME", "") new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")),
.withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.volumes.yml" : "docker-compose.redis.volumes.yml"))
.waitingFor("tb-core1", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) ));
.waitingFor("tb-core2", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-http-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) Map<String, String> queueEnv = new HashMap<>();
.waitingFor("tb-http-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) queueEnv.put("TB_QUEUE_TYPE", QUEUE_TYPE);
.waitingFor("tb-mqtt-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) switch (QUEUE_TYPE) {
.waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) case "kafka":
.waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) composeFiles.add(new File(targetDir + "docker-compose.kafka.yml"));
.waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) break;
.waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); case "aws-sqs":
} catch (Exception e) { replaceInFile(targetDir, "queue-aws-sqs.env",
log.error("Failed to create test container", e); Map.of("YOUR_KEY", getSysProp("blackBoxTests.awsKey"),
fail("Failed to create test container"); "YOUR_SECRET", getSysProp("blackBoxTests.awsSecret"),
"YOUR_REGION", getSysProp("blackBoxTests.awsRegion")));
break;
case "rabbitmq":
composeFiles.add(new File(targetDir + "docker-compose.rabbitmq-server.yml"));
replaceInFile(targetDir, "queue-rabbitmq.env",
Map.of("localhost", "rabbitmq"));
break;
case "service-bus":
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_NAMESPACE_NAME", getSysProp("blackBoxTests.serviceBusNamespace"),
"YOUR_SAS_KEY_NAME", getSysProp("blackBoxTests.serviceBusSASPolicy")));
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_SAS_KEY", getSysProp("blackBoxTests.serviceBusPrimaryKey")));
break;
case "pubsub":
replaceInFile(targetDir, "queue-pubsub.env",
Map.of("YOUR_PROJECT_ID", getSysProp("blackBoxTests.pubSubProjectId"),
"YOUR_SERVICE_ACCOUNT", getSysProp("blackBoxTests.pubSubServiceAccount")));
break;
default:
throw new RuntimeException("Unsupported queue type: " + QUEUE_TYPE);
} }
if (IS_HYBRID_MODE) {
composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml"));
}
testContainer = new DockerComposeContainerImpl<>(composeFiles)
.withPull(false)
.withLocalCompose(true)
.withTailChildContainers(!skipTailChildContainers)
.withEnv(installTb.getEnv())
.withEnv(queueEnv)
.withEnv("LOAD_BALANCER_NAME", "")
.withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-core1", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-core2", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-http-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-http-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-mqtt-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT));
testContainer.start();
setActive(true);
} catch (Exception e) {
log.error("Failed to create test container", e);
fail("Failed to create test container");
}
}
public void stop() {
if (isActive) {
testContainer.stop();
installTb.savaLogsAndRemoveVolumes();
setActive(false);
} }
return testContainer;
} }
private static void replaceInFile(String targetDir, String fileName, Map<String, String> replacements) throws IOException { private static void replaceInFile(String targetDir, String fileName, Map<String, String> replacements) throws IOException {

53
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2022 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.msa;
import lombok.extern.slf4j.Slf4j;
import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.TestListenerAdapter;
import static org.testng.internal.Utils.log;
@Slf4j
public class TestListener extends TestListenerAdapter {
@Override
public void onTestStart(ITestResult result) {
super.onTestStart(result);
log.info("===>>> Test started: " + result.getName());
}
/**
* Invoked when a test succeeds
*/
@Override
public void onTestSuccess(ITestResult result) {
super.onTestSuccess(result);
if (result != null) {
log.info("<<<=== Test completed successfully: " + result.getName());
}
}
/**
* Invoked when a test fails
*/
@Override
public void onTestFailure(ITestResult result) {
super.onTestFailure(result);
log.info("<<<=== Test failed: " + result.getName());
}
}

61
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2022 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.msa;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
@Slf4j
public class TestProperties {
private static final String HTTPS_URL = "https://localhost";
private static final String WSS_URL = "wss://localhost";
private static final ContainerTestSuite instance = ContainerTestSuite.getInstance();
private static Properties properties;
public static String getBaseUrl() {
if (instance.isActive()) {
return HTTPS_URL;
}
return getProperties().getProperty("tb.baseUrl");
}
public static String getWebSocketUrl() {
if (instance.isActive()) {
return WSS_URL;
}
return getProperties().getProperty("tb.wsUrl");
}
private static Properties getProperties() {
if (properties == null) {
try (InputStream input = TestProperties.class.getClassLoader().getResourceAsStream("config.properties")) {
properties = new Properties();
properties.load(input);
} catch (IOException ex) {
log.error("Exception while reading test properties " + ex.getMessage());
}
}
return properties;
}
}

262
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java

@ -0,0 +1,262 @@
/**
* Copyright © 2016-2022 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.msa;
import com.fasterxml.jackson.databind.JsonNode;
import io.restassured.RestAssured;
import io.restassured.common.mapper.TypeRef;
import io.restassured.config.HeaderConfig;
import io.restassured.config.RestAssuredConfig;
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;
import io.restassured.http.ContentType;
import io.restassured.path.json.JsonPath;
import io.restassured.response.ValidatableResponse;
import io.restassured.specification.RequestSpecification;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.core.AnyOf.anyOf;
import static org.thingsboard.server.common.data.StringUtils.isEmpty;
public class TestRestClient {
private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private final RequestSpecification requestSpec;
private String token;
private String refreshToken;
public TestRestClient(String url) {
RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter());
requestSpec = given().baseUri(url)
.contentType(ContentType.JSON)
.config(RestAssuredConfig.config()
.headerConfig(HeaderConfig.headerConfig()
.overwriteHeadersWithName(JWT_TOKEN_HEADER_PARAM, CONTENT_TYPE_HEADER)));
if (url.matches("^(https)://.*$")) {
requestSpec.relaxedHTTPSValidation();
}
}
public void login(String username, String password) {
Map<String, String> loginRequest = new HashMap<>();
loginRequest.put("username", username);
loginRequest.put("password", password);
JsonPath jsonPath = given().spec(requestSpec).body(loginRequest)
.post( "/api/auth/login")
.getBody().jsonPath();
token = jsonPath.get("token");
refreshToken = jsonPath.get("refreshToken");
requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
}
public Device postDevice(String accessToken, Device device) {
return given().spec(requestSpec).body(device)
.pathParams("accessToken", accessToken)
.post("/api/device?accessToken={accessToken}")
.then()
.statusCode(HTTP_OK)
.extract()
.as(Device.class);
}
public ValidatableResponse getDeviceById(DeviceId deviceId, int statusCode) {
return given().spec(requestSpec)
.pathParams("deviceId", deviceId.getId())
.get("/api/device/{deviceId}")
.then()
.statusCode(statusCode);
}
public Device getDeviceById(DeviceId deviceId) {
return getDeviceById(deviceId, HTTP_OK)
.extract()
.as(Device.class);
}
public DeviceCredentials getDeviceCredentialsByDeviceId(DeviceId deviceId) {
return given().spec(requestSpec).get("/api/device/{deviceId}/credentials", deviceId.getId())
.then()
.assertThat()
.statusCode(HTTP_OK)
.extract()
.as(DeviceCredentials.class);
}
public ValidatableResponse postTelemetry(String credentialsId, JsonNode telemetry) {
return given().spec(requestSpec).body(telemetry)
.post("/api/v1/{credentialsId}/telemetry", credentialsId)
.then()
.statusCode(HTTP_OK);
}
public ValidatableResponse deleteDevice(DeviceId deviceId) {
return given().spec(requestSpec)
.delete("/api/device/{deviceId}", deviceId.getId())
.then()
.statusCode(HTTP_OK);
}
public ValidatableResponse deleteDeviceIfExists(DeviceId deviceId) {
return given().spec(requestSpec)
.delete("/api/device/{deviceId}", deviceId.getId())
.then()
.statusCode(anyOf(is(HTTP_OK),is(HTTP_NOT_FOUND)));
}
public ValidatableResponse postTelemetryAttribute(String entityType, DeviceId deviceId, String scope, JsonNode attribute) {
return given().spec(requestSpec).body(attribute)
.post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityType, deviceId.getId(), scope)
.then()
.statusCode(HTTP_OK);
}
public ValidatableResponse postAttribute(String accessToken, JsonNode attribute) {
return given().spec(requestSpec).body(attribute)
.post("/api/v1/{accessToken}/attributes/", accessToken)
.then()
.statusCode(HTTP_OK);
}
public JsonNode getAttributes(String accessToken, String clientKeys, String sharedKeys) {
return given().spec(requestSpec)
.queryParam("clientKeys", clientKeys)
.queryParam("sharedKeys", sharedKeys)
.get("/api/v1/{accessToken}/attributes", accessToken)
.then()
.statusCode(HTTP_OK)
.extract()
.as(JsonNode.class);
}
public PageData<RuleChain> getRuleChains(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return given().spec(requestSpec).queryParams(params)
.get("/api/ruleChains")
.then()
.statusCode(HTTP_OK)
.extract()
.as(new TypeRef<PageData<RuleChain>>() {});
}
public RuleChain postRootRuleChain(RuleChain ruleChain) {
return given().spec(requestSpec)
.body(ruleChain)
.post("/api/ruleChain")
.then()
.statusCode(HTTP_OK)
.extract()
.as(RuleChain.class);
}
public RuleChainMetaData postRuleChainMetadata(RuleChainMetaData ruleChainMetaData) {
return given().spec(requestSpec)
.body(ruleChainMetaData)
.post("/api/ruleChain/metadata")
.then()
.statusCode(HTTP_OK)
.extract()
.as(RuleChainMetaData.class);
}
public void setRootRuleChain(RuleChainId ruleChainId) {
given().spec(requestSpec)
.post("/api/ruleChain/{ruleChainId}/root", ruleChainId.getId())
.then()
.statusCode(HTTP_OK);
}
public void deleteRuleChain(RuleChainId ruleChainId) {
given().spec(requestSpec)
.delete("/api/ruleChain/{ruleChainId}", ruleChainId.getId())
.then()
.statusCode(HTTP_OK);
}
private String getUrlParams(PageLink pageLink) {
String urlParams = "pageSize={pageSize}&page={page}";
if (!isEmpty(pageLink.getTextSearch())) {
urlParams += "&textSearch={textSearch}";
}
if (pageLink.getSortOrder() != null) {
urlParams += "&sortProperty={sortProperty}&sortOrder={sortOrder}";
}
return urlParams;
}
private void addPageLinkToParam(Map<String, String> params, PageLink pageLink) {
params.put("pageSize", String.valueOf(pageLink.getPageSize()));
params.put("page", String.valueOf(pageLink.getPage()));
if (!isEmpty(pageLink.getTextSearch())) {
params.put("textSearch", pageLink.getTextSearch());
}
if (pageLink.getSortOrder() != null) {
params.put("sortProperty", pageLink.getSortOrder().getProperty());
params.put("sortOrder", pageLink.getSortOrder().getDirection().name());
}
}
public List<EntityRelation> findRelationByFrom(EntityId fromId, RelationTypeGroup relationTypeGroup) {
Map<String, String> params = new HashMap<>();
params.put("fromId", fromId.getId().toString());
params.put("fromType", fromId.getEntityType().name());
params.put("relationTypeGroup", relationTypeGroup.name());
return given().spec(requestSpec)
.pathParams(params)
.get("/api/relations?fromId={fromId}&fromType={fromType}&relationTypeGroup={relationTypeGroup}")
.then()
.statusCode(HTTP_OK)
.extract()
.as(new TypeRef<List<EntityRelation>>() {});
}
public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) {
return given().spec(requestSpec)
.body(serverRpcPayload)
.post("/api/rpc/twoway/{deviceId}", deviceId.getId())
.then()
.statusCode(HTTP_OK)
.extract()
.as(JsonNode.class);
}
public String getToken() {
return token;
}
public String getRefreshToken() {
return refreshToken;
}
}

9
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java

@ -16,7 +16,6 @@
package org.thingsboard.server.msa; package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.rules.ExternalResource;
import org.testcontainers.utility.Base58; import org.testcontainers.utility.Base58;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -30,7 +29,7 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@Slf4j @Slf4j
public class ThingsBoardDbInstaller extends ExternalResource { public class ThingsBoardDbInstaller {
final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster")); final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster"));
final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode")); final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode"));
@ -129,8 +128,7 @@ public class ThingsBoardDbInstaller extends ExternalResource {
return env; return env;
} }
@Override public void createVolumes() {
protected void before() throws Throwable {
try { try {
dockerCompose.withCommand("volume create " + postgresDataVolume); dockerCompose.withCommand("volume create " + postgresDataVolume);
@ -192,8 +190,7 @@ public class ThingsBoardDbInstaller extends ExternalResource {
} }
} }
@Override public void savaLogsAndRemoveVolumes() {
protected void after() {
copyLogs(tbLogVolume, "./target/tb-logs/"); copyLogs(tbLogVolume, "./target/tb-logs/");
copyLogs(tbCoapTransportLogVolume, "./target/tb-coap-transport-logs/"); copyLogs(tbCoapTransportLogVolume, "./target/tb-coap-transport-logs/");
copyLogs(tbLwm2mTransportLogVolume, "./target/tb-lwm2m-transport-logs/"); copyLogs(tbLwm2mTransportLogVolume, "./target/tb-lwm2m-transport-logs/");

112
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java

@ -16,109 +16,79 @@
package org.thingsboard.server.msa.connectivity; package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Sets; import org.testng.annotations.AfterMethod;
import org.junit.Assert; import org.testng.annotations.BeforeMethod;
import org.junit.Test; import org.testng.annotations.Test;
import org.springframework.http.ResponseEntity;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.AbstractContainerTest;
import org.thingsboard.server.msa.WsClient; import org.thingsboard.server.msa.WsClient;
import org.thingsboard.server.msa.mapper.WsTelemetryResponse; import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.thingsboard.server.common.data.DataConstants.DEVICE; import static org.thingsboard.server.common.data.DataConstants.DEVICE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
public class HttpClientTest extends AbstractContainerTest { public class HttpClientTest extends AbstractContainerTest {
private Device device;
@BeforeMethod
public void setUp() throws Exception {
testRestClient.login("tenant@thingsboard.org", "tenant");
device = testRestClient.postDevice("", defaultDevicePrototype("http_"));
}
@AfterMethod
public void tearDown() {
testRestClient.deleteDeviceIfExists(device.getId());
}
@Test @Test
public void telemetryUpload() throws Exception { public void telemetryUpload() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
Device device = createDevice("http_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
ResponseEntity deviceTelemetryResponse = restClient.getRestTemplate() testRestClient.postTelemetry(deviceCredentials.getCredentialsId(), mapper.readTree(createPayload().toString()));
.postForEntity(HTTPS_URL + "/api/v1/{credentialsId}/telemetry",
mapper.readTree(createPayload().toString()),
ResponseEntity.class,
deviceCredentials.getCredentialsId());
Assert.assertTrue(deviceTelemetryResponse.getStatusCode().is2xxSuccessful());
WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage(); WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"), assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
restClient.deleteDevice(device.getId()); assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
} }
@Test @Test
public void getAttributes() throws Exception { public void getAttributes() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); String accessToken = testRestClient.getDeviceCredentialsByDeviceId(device.getId()).getCredentialsId();
TB_TOKEN = restClient.getToken(); assertThat(accessToken).isNotNull();
Device device = createDevice("test"); JsonNode sharedAattribute = mapper.readTree(createPayload().toString());
String accessToken = restClient.getDeviceCredentialsByDeviceId(device.getId()).get().getCredentialsId(); testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, sharedAattribute);
assertNotNull(accessToken);
ResponseEntity deviceSharedAttributes = restClient.getRestTemplate() JsonNode clientAttribute = mapper.readTree(createPayload().toString());
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/" + DEVICE + "/" + device.getId().toString() + "/attributes/" + SHARED_SCOPE, mapper.readTree(createPayload().toString()), testRestClient.postAttribute(accessToken, clientAttribute);
ResponseEntity.class,
accessToken);
Assert.assertTrue(deviceSharedAttributes.getStatusCode().is2xxSuccessful());
ResponseEntity deviceClientsAttributes = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/v1/" + accessToken + "/attributes/", mapper.readTree(createPayload().toString()),
ResponseEntity.class,
accessToken);
Assert.assertTrue(deviceClientsAttributes.getStatusCode().is2xxSuccessful());
TimeUnit.SECONDS.sleep(3 * timeoutMultiplier); TimeUnit.SECONDS.sleep(3 * timeoutMultiplier);
@SuppressWarnings("deprecation") JsonNode attributes = testRestClient.getAttributes(accessToken, null, null);
Optional<JsonNode> allOptional = restClient.getAttributes(accessToken, null, null); assertThat(attributes.get("shared")).isEqualTo(sharedAattribute);
assertTrue(allOptional.isPresent()); assertThat(attributes.get("client")).isEqualTo(clientAttribute);
JsonNode all = allOptional.get();
assertEquals(2, all.size());
assertEquals(mapper.readTree(createPayload().toString()), all.get("shared"));
assertEquals(mapper.readTree(createPayload().toString()), all.get("client"));
@SuppressWarnings("deprecation")
Optional<JsonNode> sharedOptional = restClient.getAttributes(accessToken, null, "stringKey");
assertTrue(sharedOptional.isPresent());
JsonNode shared = sharedOptional.get();
assertEquals(shared.get("shared").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
assertFalse(shared.has("client"));
@SuppressWarnings("deprecation") JsonNode attributes2 = testRestClient.getAttributes(accessToken, null, "stringKey");
Optional<JsonNode> clientOptional = restClient.getAttributes(accessToken, "longKey,stringKey", null); assertThat(attributes2.get("shared").get("stringKey")).isEqualTo(sharedAattribute.get("stringKey"));
assertTrue(clientOptional.isPresent()); assertThat(attributes2.has("client")).isFalse();
JsonNode client = clientOptional.get(); JsonNode attributes3 = testRestClient.getAttributes(accessToken, "longKey,stringKey", null);
assertFalse(client.has("shared"));
assertEquals(mapper.readTree(createPayload().get("longKey").toString()), client.get("client").get("longKey"));
assertEquals(client.get("client").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
restClient.deleteDevice(device.getId()); assertThat(attributes3.has("shared")).isFalse();
assertThat(attributes3.get("client").get("longKey")).isEqualTo(clientAttribute.get("longKey"));
assertThat(attributes3.get("client").get("stringKey")).isEqualTo(clientAttribute.get("stringKey"));
} }
} }

226
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java

@ -16,7 +16,6 @@
package org.thingsboard.server.msa.connectivity; package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -26,19 +25,19 @@ import io.netty.buffer.Unpooled;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.testng.annotations.AfterMethod;
import org.junit.Test; import org.testng.annotations.BeforeMethod;
import org.springframework.core.ParameterizedTypeReference; import org.testng.annotations.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.mqtt.MqttHandler; import org.thingsboard.mqtt.MqttHandler;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -61,14 +60,29 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.testng.Assert.fail;
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
@Slf4j @Slf4j
public class MqttClientTest extends AbstractContainerTest { public class MqttClientTest extends AbstractContainerTest {
private Device device;
@BeforeMethod
public void setUp() throws Exception {
testRestClient.login("tenant@thingsboard.org", "tenant");
device = testRestClient.postDevice("", defaultDevicePrototype("http_"));
}
@AfterMethod
public void tearDown() {
testRestClient.deleteDeviceIfExists(device.getId());
}
@Test @Test
public void telemetryUpload() throws Exception { public void telemetryUpload() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
MqttClient mqttClient = getMqttClient(deviceCredentials, null); MqttClient mqttClient = getMqttClient(deviceCredentials, null);
@ -77,25 +91,19 @@ public class MqttClientTest extends AbstractContainerTest {
log.info("Received telemetry: {}", actualLatestTelemetry); log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size()); assertThat(actualLatestTelemetry.getData()).hasSize(4);
Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"), assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString())); assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1")); assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0))); assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73))); assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
} }
@Test @Test
public void telemetryUploadWithTs() throws Exception { public void telemetryUploadWithTs() throws Exception {
long ts = 1451649600512L; long ts = 1451649600512L;
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
MqttClient mqttClient = getMqttClient(deviceCredentials, null); MqttClient mqttClient = getMqttClient(deviceCredentials, null);
@ -104,22 +112,18 @@ public class MqttClientTest extends AbstractContainerTest {
log.info("Received telemetry: {}", actualLatestTelemetry); log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size()); assertThat(actualLatestTelemetry.getData()).hasSize(4);
Assert.assertEquals(getExpectedLatestValues(ts), actualLatestTelemetry.getLatestValues()); assertThat(getExpectedLatestValues(ts)).isEqualTo(actualLatestTelemetry.getLatestValues());
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", ts, Boolean.TRUE.toString())); assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", ts, "value1")); assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", ts, Double.toString(42.0))); assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", ts, Long.toString(73))); assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
} }
@Test @Test
public void publishAttributeUpdateToServer() throws Exception { public void publishAttributeUpdateToServer() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS);
MqttMessageListener listener = new MqttMessageListener(); MqttMessageListener listener = new MqttMessageListener();
@ -134,23 +138,18 @@ public class MqttClientTest extends AbstractContainerTest {
log.info("Received telemetry: {}", actualLatestTelemetry); log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size()); assertThat(actualLatestTelemetry.getData()).hasSize(4);
Assert.assertEquals(Sets.newHashSet("attr1", "attr2", "attr3", "attr4"), assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("attr1", "attr2", "attr3", "attr4"));
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "attr1", "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "attr2", Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "attr3", Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "attr4", Long.toString(73)));
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); assertThat(actualLatestTelemetry.getDataValuesByKey("attr1").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("attr2").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("attr3").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("attr4").get(1)).isEqualTo(Long.toString(73));
} }
@Test @Test
public void requestAttributeValuesFromServer() throws Exception { public void requestAttributeValuesFromServer() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS);
MqttMessageListener listener = new MqttMessageListener(); MqttMessageListener listener = new MqttMessageListener();
@ -166,21 +165,16 @@ public class MqttClientTest extends AbstractContainerTest {
log.info("Received ws telemetry: {}", actualLatestTelemetry); log.info("Received ws telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(1, actualLatestTelemetry.getData().size()); assertThat(actualLatestTelemetry.getData()).hasSize(1);
Assert.assertEquals(Sets.newHashSet("clientAttr"), assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnly("clientAttr");
actualLatestTelemetry.getLatestValues().keySet()); assertThat(actualLatestTelemetry.getDataValuesByKey("clientAttr").get(1)).isEqualTo(clientAttributeValue);
Assert.assertTrue(verify(actualLatestTelemetry, "clientAttr", clientAttributeValue));
// Add a new shared attribute // Add a new shared attribute
JsonObject sharedAttributes = new JsonObject(); JsonObject sharedAttributes = new JsonObject();
String sharedAttributeValue = StringUtils.randomAlphanumeric(8); String sharedAttributeValue = StringUtils.randomAlphanumeric(8);
sharedAttributes.addProperty("sharedAttr", sharedAttributeValue); sharedAttributes.addProperty("sharedAttr", sharedAttributeValue);
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString());
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", testRestClient.postTelemetryAttribute(DataConstants.DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute);
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
device.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
// Subscribe to attributes response // Subscribe to attributes response
mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE).get(); mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE).get();
@ -197,20 +191,16 @@ public class MqttClientTest extends AbstractContainerTest {
AttributesResponse attributes = mapper.readValue(Objects.requireNonNull(event).getMessage(), AttributesResponse.class); AttributesResponse attributes = mapper.readValue(Objects.requireNonNull(event).getMessage(), AttributesResponse.class);
log.info("Received telemetry: {}", attributes); log.info("Received telemetry: {}", attributes);
Assert.assertEquals(1, attributes.getClient().size()); assertThat(attributes.getClient()).hasSize(1);
Assert.assertEquals(clientAttributeValue, attributes.getClient().get("clientAttr")); assertThat(attributes.getClient().get("clientAttr")).isEqualTo(clientAttributeValue);
Assert.assertEquals(1, attributes.getShared().size());
Assert.assertEquals(sharedAttributeValue, attributes.getShared().get("sharedAttr"));
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); assertThat(attributes.getShared()).hasSize(1);
assertThat(attributes.getShared().get("sharedAttr")).isEqualTo(sharedAttributeValue);
} }
@Test @Test
public void subscribeToAttributeUpdatesFromServer() throws Exception { public void subscribeToAttributeUpdatesFromServer() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
MqttMessageListener listener = new MqttMessageListener(); MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener); MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
@ -225,38 +215,28 @@ public class MqttClientTest extends AbstractContainerTest {
JsonObject sharedAttributes = new JsonObject(); JsonObject sharedAttributes = new JsonObject();
String sharedAttributeValue = StringUtils.randomAlphanumeric(8); String sharedAttributeValue = StringUtils.randomAlphanumeric(8);
sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue); sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue);
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString());
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, testRestClient.postTelemetryAttribute(DataConstants.DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute);
device.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals(sharedAttributeValue, assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText())
mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()); .isEqualTo(sharedAttributeValue);
// Update the shared attribute value // Update the shared attribute value
JsonObject updatedSharedAttributes = new JsonObject(); JsonObject updatedSharedAttributes = new JsonObject();
String updatedSharedAttributeValue = StringUtils.randomAlphanumeric(8); String updatedSharedAttributeValue = StringUtils.randomAlphanumeric(8);
updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue); updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue);
ResponseEntity updatedSharedAttributesResponse = restClient.getRestTemplate() testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString()));
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(updatedSharedAttributes.toString()), ResponseEntity.class,
device.getId());
Assert.assertTrue(updatedSharedAttributesResponse.getStatusCode().is2xxSuccessful());
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals(updatedSharedAttributeValue, assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText())
mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()); .isEqualTo(updatedSharedAttributeValue);
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
} }
@Test @Test
public void serverSideRpc() throws Exception { public void serverSideRpc() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
MqttMessageListener listener = new MqttMessageListener(); MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener); MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
@ -270,21 +250,18 @@ public class MqttClientTest extends AbstractContainerTest {
serverRpcPayload.addProperty("method", "getValue"); serverRpcPayload.addProperty("method", "getValue");
serverRpcPayload.addProperty("params", true); serverRpcPayload.addProperty("params", true);
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName()))); ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName())));
ListenableFuture<ResponseEntity> future = service.submit(() -> { ListenableFuture<JsonNode> future = service.submit(() -> {
try { try {
return restClient.getRestTemplate() return testRestClient.postServerSideRpc(device.getId(), mapper.readTree(serverRpcPayload.toString()));
.postForEntity(HTTPS_URL + "/api/rpc/twoway/{deviceId}",
mapper.readTree(serverRpcPayload.toString()), String.class,
device.getId());
} catch (IOException e) { } catch (IOException e) {
return ResponseEntity.badRequest().build(); return null;
} }
}); });
// Wait for RPC call from the server and send the response // Wait for RPC call from the server and send the response
MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals("{\"method\":\"getValue\",\"params\":true}", Objects.requireNonNull(requestFromServer).getMessage()); assertThat(Objects.requireNonNull(requestFromServer).getMessage()).isEqualTo("{\"method\":\"getValue\",\"params\":true}");
Integer requestId = Integer.valueOf(Objects.requireNonNull(requestFromServer).getTopic().substring("v1/devices/me/rpc/request/".length())); Integer requestId = Integer.valueOf(Objects.requireNonNull(requestFromServer).getTopic().substring("v1/devices/me/rpc/request/".length()));
JsonObject clientResponse = new JsonObject(); JsonObject clientResponse = new JsonObject();
@ -292,19 +269,14 @@ public class MqttClientTest extends AbstractContainerTest {
// Send a response to the server's RPC request // Send a response to the server's RPC request
mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes())).get(); mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes())).get();
ResponseEntity serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS); JsonNode serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS);
service.shutdownNow(); service.shutdownNow();
Assert.assertTrue(serverResponse.getStatusCode().is2xxSuccessful()); assertThat(serverResponse).isEqualTo(mapper.readTree(clientResponse.toString()));
Assert.assertEquals(clientResponse.toString(), serverResponse.getBody());
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
} }
@Test @Test
public void clientSideRpc() throws Exception { public void clientSideRpc() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
MqttMessageListener listener = new MqttMessageListener(); MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener); MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
@ -328,46 +300,33 @@ public class MqttClientTest extends AbstractContainerTest {
TimeUnit.SECONDS.sleep(1 * timeoutMultiplier); TimeUnit.SECONDS.sleep(1 * timeoutMultiplier);
MqttEvent responseFromServer = listener.getEvents().poll(1 * timeoutMultiplier, TimeUnit.SECONDS); MqttEvent responseFromServer = listener.getEvents().poll(1 * timeoutMultiplier, TimeUnit.SECONDS);
Integer responseId = Integer.valueOf(Objects.requireNonNull(responseFromServer).getTopic().substring("v1/devices/me/rpc/response/".length())); Integer responseId = Integer.valueOf(Objects.requireNonNull(responseFromServer).getTopic().substring("v1/devices/me/rpc/response/".length()));
Assert.assertEquals(requestId, responseId); assertThat(responseId).isEqualTo(requestId);
Assert.assertEquals("requestReceived", mapper.readTree(responseFromServer.getMessage()).get("response").asText()); assertThat(mapper.readTree(responseFromServer.getMessage()).get("response").asText()).isEqualTo("requestReceived");
// Make the default rule chain a root again // Make the default rule chain a root again
ResponseEntity<RuleChain> rootRuleChainResponse = restClient.getRestTemplate() testRestClient.setRootRuleChain(defaultRuleChainId);
.postForEntity(HTTPS_URL + "/api/ruleChain/{ruleChainId}/root",
null,
RuleChain.class,
defaultRuleChainId);
Assert.assertTrue(rootRuleChainResponse.getStatusCode().is2xxSuccessful());
// Delete the created rule chain // Delete the created rule chain
restClient.getRestTemplate().delete(HTTPS_URL + "/api/ruleChain/{ruleChainId}", ruleChainId); testRestClient.deleteRuleChain(ruleChainId);
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
} }
@Test @Test
public void deviceDeletedClosingSession() throws Exception { public void deviceDeletedClosingSession() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
String deviceForDeletingTestName = "Device for deleting notification test";
Device device = createDevice(deviceForDeletingTestName);
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
MqttMessageListener listener = new MqttMessageListener(); MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener); MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
restClient.deleteDevice(device.getId()); testRestClient.deleteDeviceIfExists(device.getId());
TimeUnit.SECONDS.sleep(3 * timeoutMultiplier); TimeUnit.SECONDS.sleep(3 * timeoutMultiplier);
Assert.assertFalse(mqttClient.isConnected()); assertThat(mqttClient.isConnected()).isFalse();
} }
private RuleChainId createRootRuleChainForRpcResponse() throws Exception { private RuleChainId createRootRuleChainForRpcResponse() throws Exception {
RuleChain newRuleChain = new RuleChain(); RuleChain newRuleChain = new RuleChain();
newRuleChain.setName("testRuleChain"); newRuleChain.setName("testRuleChain");
ResponseEntity<RuleChain> ruleChainResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/ruleChain", RuleChain ruleChain = testRestClient.postRootRuleChain(newRuleChain);
newRuleChain,
RuleChain.class);
Assert.assertTrue(ruleChainResponse.getStatusCode().is2xxSuccessful());
RuleChain ruleChain = ruleChainResponse.getBody();
JsonNode configuration = mapper.readTree(this.getClass().getClassLoader().getResourceAsStream("RpcResponseRuleChainMetadata.json")); JsonNode configuration = mapper.readTree(this.getClass().getClassLoader().getResourceAsStream("RpcResponseRuleChainMetadata.json"));
RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
@ -376,37 +335,22 @@ public class MqttClientTest extends AbstractContainerTest {
ruleChainMetaData.setNodes(Arrays.asList(mapper.treeToValue(configuration.get("nodes"), RuleNode[].class))); ruleChainMetaData.setNodes(Arrays.asList(mapper.treeToValue(configuration.get("nodes"), RuleNode[].class)));
ruleChainMetaData.setConnections(Arrays.asList(mapper.treeToValue(configuration.get("connections"), NodeConnectionInfo[].class))); ruleChainMetaData.setConnections(Arrays.asList(mapper.treeToValue(configuration.get("connections"), NodeConnectionInfo[].class)));
ResponseEntity<RuleChainMetaData> ruleChainMetadataResponse = restClient.getRestTemplate() testRestClient.postRuleChainMetadata(ruleChainMetaData);
.postForEntity(HTTPS_URL + "/api/ruleChain/metadata",
ruleChainMetaData,
RuleChainMetaData.class);
Assert.assertTrue(ruleChainMetadataResponse.getStatusCode().is2xxSuccessful());
// Set a new rule chain as root // Set a new rule chain as root
ResponseEntity<RuleChain> rootRuleChainResponse = restClient.getRestTemplate() testRestClient.setRootRuleChain(ruleChain.getId());
.postForEntity(HTTPS_URL + "/api/ruleChain/{ruleChainId}/root",
null,
RuleChain.class,
ruleChain.getId());
Assert.assertTrue(rootRuleChainResponse.getStatusCode().is2xxSuccessful());
return ruleChain.getId(); return ruleChain.getId();
} }
private RuleChainId getDefaultRuleChainId() { private RuleChainId getDefaultRuleChainId() {
ResponseEntity<PageData<RuleChain>> ruleChains = restClient.getRestTemplate().exchange( PageData<RuleChain> ruleChains = testRestClient.getRuleChains(new PageLink(40, 0));
HTTPS_URL + "/api/ruleChains?pageSize=40&page=0&textSearch=",
HttpMethod.GET, Optional<RuleChain> defaultRuleChain = ruleChains.getData()
null,
new ParameterizedTypeReference<PageData<RuleChain>>() {
});
Optional<RuleChain> defaultRuleChain = ruleChains.getBody().getData()
.stream() .stream()
.filter(RuleChain::isRoot) .filter(RuleChain::isRoot)
.findFirst(); .findFirst();
if (!defaultRuleChain.isPresent()) { if (!defaultRuleChain.isPresent()) {
Assert.fail("Root rule chain wasn't found"); fail("Root rule chain wasn't found");
} }
return defaultRuleChain.get().getId(); return defaultRuleChain.get().getId();
} }

220
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java

@ -16,7 +16,6 @@
package org.thingsboard.server.msa.connectivity; package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -28,15 +27,17 @@ import io.netty.buffer.Unpooled;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.After; import org.springframework.http.HttpStatus;
import org.junit.Assert; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.junit.Before; import org.testng.annotations.AfterMethod;
import org.junit.Test; import org.testng.annotations.BeforeMethod;
import org.springframework.http.ResponseEntity; import org.testng.annotations.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.mqtt.MqttHandler; import org.thingsboard.mqtt.MqttHandler;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -50,9 +51,9 @@ import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -60,28 +61,34 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultGatewayPrototype;
@Slf4j @Slf4j
public class MqttGatewayClientTest extends AbstractContainerTest { public class MqttGatewayClientTest extends AbstractContainerTest {
Device gatewayDevice; private Device gatewayDevice;
MqttClient mqttClient; private MqttClient mqttClient;
Device createdDevice; private Device createdDevice;
MqttMessageListener listener; private MqttMessageListener listener;
private JsonParser jsonParser = new JsonParser();
@Before @BeforeMethod
public void createGateway() throws Exception { public void createGateway() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant"); testRestClient.login("tenant@thingsboard.org", "tenant");
this.gatewayDevice = createGatewayDevice(); gatewayDevice = testRestClient.postDevice("", defaultGatewayPrototype());
Optional<DeviceCredentials> gatewayDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId()); DeviceCredentials gatewayDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId());
Assert.assertTrue(gatewayDeviceCredentials.isPresent());
this.listener = new MqttMessageListener(); this.listener = new MqttMessageListener();
this.mqttClient = getMqttClient(gatewayDeviceCredentials.get(), listener); this.mqttClient = getMqttClient(gatewayDeviceCredentials, listener);
this.createdDevice = createDeviceThroughGateway(mqttClient, gatewayDevice); this.createdDevice = createDeviceThroughGateway(mqttClient, gatewayDevice);
} }
@After @AfterMethod
public void removeGateway() throws Exception { public void removeGateway() {
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.gatewayDevice.getId()); testRestClient.deleteDeviceIfExists(this.gatewayDevice.getId());
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.createdDevice.getId()); testRestClient.deleteDeviceIfExists(this.createdDevice.getId());
this.listener = null; this.listener = null;
this.mqttClient = null; this.mqttClient = null;
this.createdDevice = null; this.createdDevice = null;
@ -95,40 +102,38 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
log.info("Received telemetry: {}", actualLatestTelemetry); log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size()); assertThat(actualLatestTelemetry.getData()).hasSize(4);
Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"), assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString())); assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1")); assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0))); assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73))); assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
} }
@Test @Test
public void telemetryUploadWithTs() throws Exception { public void telemetryUploadWithTs() throws Exception {
long ts = 1451649600512L; long ts = 1451649600512L;
restClient.login("tenant@thingsboard.org", "tenant");
WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
mqttClient.publish("v1/gateway/telemetry", Unpooled.wrappedBuffer(createGatewayPayload(createdDevice.getName(), ts).toString().getBytes())).get(); mqttClient.publish("v1/gateway/telemetry", Unpooled.wrappedBuffer(createGatewayPayload(createdDevice.getName(), ts).toString().getBytes())).get();
WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage(); WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
log.info("Received telemetry: {}", actualLatestTelemetry); log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size()); assertThat(actualLatestTelemetry.getData()).hasSize(4);
Assert.assertEquals(getExpectedLatestValues(ts), actualLatestTelemetry.getLatestValues()); assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", ts, Boolean.TRUE.toString())); assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", ts, "value1")); assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", ts, Double.toString(42.0))); assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", ts, Long.toString(73))); assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
} }
@Test @Test
public void publishAttributeUpdateToServer() throws Exception { public void publishAttributeUpdateToServer() throws Exception {
Optional<DeviceCredentials> createdDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(createdDevice.getId()); testRestClient.getDeviceCredentialsByDeviceId(createdDevice.getId());
Assert.assertTrue(createdDeviceCredentials.isPresent());
WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS);
JsonObject clientAttributes = new JsonObject(); JsonObject clientAttributes = new JsonObject();
clientAttributes.addProperty("attr1", "value1"); clientAttributes.addProperty("attr1", "value1");
@ -142,20 +147,18 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
log.info("Received attributes: {}", actualLatestTelemetry); log.info("Received attributes: {}", actualLatestTelemetry);
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size()); assertThat(actualLatestTelemetry.getData()).hasSize(4);
Assert.assertEquals(Sets.newHashSet("attr1", "attr2", "attr3", "attr4"), assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("attr1", "attr2", "attr3", "attr4"));
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "attr1", "value1")); assertThat(actualLatestTelemetry.getDataValuesByKey("attr1").get(1)).isEqualTo("value1");
Assert.assertTrue(verify(actualLatestTelemetry, "attr2", Boolean.TRUE.toString())); assertThat(actualLatestTelemetry.getDataValuesByKey("attr2").get(1)).isEqualTo(Boolean.TRUE.toString());
Assert.assertTrue(verify(actualLatestTelemetry, "attr3", Double.toString(42.0))); assertThat(actualLatestTelemetry.getDataValuesByKey("attr3").get(1)).isEqualTo(Double.toString(42.0));
Assert.assertTrue(verify(actualLatestTelemetry, "attr4", Long.toString(73))); assertThat(actualLatestTelemetry.getDataValuesByKey("attr4").get(1)).isEqualTo(Long.toString(73));
} }
@Test @Test
public void responseDataOnAttributesRequestCheck() throws Exception { public void responseDataOnAttributesRequestCheck() throws Exception {
Optional<DeviceCredentials> createdDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(createdDevice.getId()); testRestClient.getDeviceCredentialsByDeviceId(createdDevice.getId());
Assert.assertTrue(createdDeviceCredentials.isPresent());
JsonObject sharedAttributes = new JsonObject(); JsonObject sharedAttributes = new JsonObject();
sharedAttributes.addProperty("attr1", "value1"); sharedAttributes.addProperty("attr1", "value1");
sharedAttributes.addProperty("attr2", true); sharedAttributes.addProperty("attr2", true);
@ -163,11 +166,8 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
sharedAttributes.addProperty("attr4", 73); sharedAttributes.addProperty("attr4", 73);
mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get();
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", testRestClient.postTelemetryAttribute(DataConstants.DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString()));
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
createdDevice.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
var event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); var event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
JsonObject requestData = new JsonObject(); JsonObject requestData = new JsonObject();
@ -181,8 +181,8 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
JsonObject responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); JsonObject responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject();
Assert.assertTrue(responseData.has("value")); assertThat(responseData.has("value")).isTrue();
Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("value").getAsString()); assertThat(responseData.get("value").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString());
requestData = new JsonObject(); requestData = new JsonObject();
requestData.addProperty("id", 1); requestData.addProperty("id", 1);
@ -198,9 +198,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject();
Assert.assertTrue(responseData.has("values")); assertThat(responseData.has("values")).isTrue();
Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("values").getAsJsonObject().get("attr1").getAsString()); assertThat(responseData.get("values").getAsJsonObject().get("attr1").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString());
Assert.assertEquals(sharedAttributes.get("attr2").getAsString(), responseData.get("values").getAsJsonObject().get("attr2").getAsString()); assertThat(responseData.get("values").getAsJsonObject().get("attr2").getAsString()).isEqualTo(sharedAttributes.get("attr2").getAsString());
requestData = new JsonObject(); requestData = new JsonObject();
requestData.addProperty("id", 1); requestData.addProperty("id", 1);
@ -216,9 +216,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject();
Assert.assertTrue(responseData.has("values")); assertThat(responseData.has("values")).isTrue();
Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("values").getAsJsonObject().get("attr1").getAsString()); assertThat(responseData.get("values").getAsJsonObject().get("attr1").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString());
Assert.assertEquals(1, responseData.get("values").getAsJsonObject().entrySet().size()); assertThat(responseData.get("values").getAsJsonObject().entrySet()).hasSize(1);
} }
@Test @Test
@ -237,11 +237,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
log.info("Received ws telemetry: {}", actualLatestTelemetry); log.info("Received ws telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking(); wsClient.closeBlocking();
Assert.assertEquals(1, actualLatestTelemetry.getData().size()); assertThat(actualLatestTelemetry.getData()).hasSize(1);
Assert.assertEquals(Sets.newHashSet("clientAttr"), assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnly("clientAttr");
actualLatestTelemetry.getLatestValues().keySet()); assertThat(actualLatestTelemetry.getDataValuesByKey("clientAttr").get(1)).isEqualTo(clientAttributeValue);
Assert.assertTrue(verify(actualLatestTelemetry, "clientAttr", clientAttributeValue));
// Add a new shared attribute // Add a new shared attribute
JsonObject sharedAttributes = new JsonObject(); JsonObject sharedAttributes = new JsonObject();
@ -251,16 +249,12 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
// Subscribe for attribute update event // Subscribe for attribute update event
mqttClient.on("v1/gateway/attributes", listener, MqttQoS.AT_LEAST_ONCE).get(); mqttClient.on("v1/gateway/attributes", listener, MqttQoS.AT_LEAST_ONCE).get();
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString()));
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
createdDevice.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
MqttEvent sharedAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); MqttEvent sharedAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
// Catch attribute update event // Catch attribute update event
Assert.assertNotNull(sharedAttributeEvent); assertThat(sharedAttributeEvent).isNotNull();
Assert.assertEquals("v1/gateway/attributes", sharedAttributeEvent.getTopic()); assertThat(sharedAttributeEvent.getTopic()).isEqualTo("v1/gateway/attributes");
// Subscribe to attributes response // Subscribe to attributes response
mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get();
@ -288,15 +282,11 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
gatewaySharedAttributeValue.addProperty("device", createdDevice.getName()); gatewaySharedAttributeValue.addProperty("device", createdDevice.getName());
gatewaySharedAttributeValue.add("data", sharedAttributes); gatewaySharedAttributeValue.add("data", sharedAttributes);
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString()));
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
createdDevice.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals(sharedAttributeValue, assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText())
mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()); .isEqualTo(sharedAttributeValue);
// Update the shared attribute value // Update the shared attribute value
JsonObject updatedSharedAttributes = new JsonObject(); JsonObject updatedSharedAttributes = new JsonObject();
@ -307,15 +297,10 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
gatewayUpdatedSharedAttributeValue.addProperty("device", createdDevice.getName()); gatewayUpdatedSharedAttributeValue.addProperty("device", createdDevice.getName());
gatewayUpdatedSharedAttributeValue.add("data", updatedSharedAttributes); gatewayUpdatedSharedAttributeValue.add("data", updatedSharedAttributes);
ResponseEntity updatedSharedAttributesResponse = restClient.getRestTemplate() testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString()));
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(updatedSharedAttributes.toString()), ResponseEntity.class,
createdDevice.getId());
Assert.assertTrue(updatedSharedAttributesResponse.getStatusCode().is2xxSuccessful());
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals(updatedSharedAttributeValue, assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText())
mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()); .isEqualTo(updatedSharedAttributeValue);
} }
@Test @Test
@ -331,14 +316,11 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
serverRpcPayload.addProperty("method", "getValue"); serverRpcPayload.addProperty("method", "getValue");
serverRpcPayload.addProperty("params", true); serverRpcPayload.addProperty("params", true);
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName()))); ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName())));
ListenableFuture<ResponseEntity> future = service.submit(() -> { ListenableFuture<JsonNode> future = service.submit(() -> {
try { try {
return restClient.getRestTemplate() return testRestClient.postServerSideRpc(createdDevice.getId(), mapper.readTree(serverRpcPayload.toString()));
.postForEntity(HTTPS_URL + "/api/rpc/twoway/{deviceId}",
mapper.readTree(serverRpcPayload.toString()), String.class,
createdDevice.getId());
} catch (IOException e) { } catch (IOException e) {
return ResponseEntity.badRequest().build(); return null;
} }
}); });
@ -346,19 +328,13 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
service.shutdownNow(); service.shutdownNow();
Assert.assertNotNull(requestFromServer); assertThat(requestFromServer).isNotNull();
Assert.assertNotNull(requestFromServer.getMessage()); assertThat(requestFromServer.getMessage()).isNotNull();
JsonNode requestFromServerJson = JacksonUtil.toJsonNode(requestFromServer.getMessage());
JsonObject requestFromServerJson = new JsonParser().parse(requestFromServer.getMessage()).getAsJsonObject(); assertThat(requestFromServerJson.get("device").asText()).isEqualTo(createdDevice.getName());
assertThat(requestFromServerJson.get("data").get("method").asText()).isEqualTo("getValue");
Assert.assertEquals(createdDevice.getName(), requestFromServerJson.get("device").getAsString()); assertThat(requestFromServerJson.get("data").get("params").asText()).isEqualTo("true");
int requestId = requestFromServerJson.get("data").get("id").asInt();
JsonObject requestFromServerData = requestFromServerJson.get("data").getAsJsonObject();
Assert.assertEquals("getValue", requestFromServerData.get("method").getAsString());
Assert.assertTrue(requestFromServerData.get("params").getAsBoolean());
int requestId = requestFromServerData.get("id").getAsInt();
JsonObject clientResponse = new JsonObject(); JsonObject clientResponse = new JsonObject();
clientResponse.addProperty("response", "someResponse"); clientResponse.addProperty("response", "someResponse");
@ -369,16 +345,15 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
// Send a response to the server's RPC request // Send a response to the server's RPC request
mqttClient.publish(gatewayRpcTopic, Unpooled.wrappedBuffer(gatewayResponse.toString().getBytes())).get(); mqttClient.publish(gatewayRpcTopic, Unpooled.wrappedBuffer(gatewayResponse.toString().getBytes())).get();
ResponseEntity serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS); JsonNode serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertTrue(serverResponse.getStatusCode().is2xxSuccessful());
Assert.assertEquals(clientResponse.toString(), serverResponse.getBody()); assertThat(serverResponse).isEqualTo(mapper.readTree(clientResponse.toString()));
} }
@Test @Test
public void deviceCreationAfterDeleted() throws Exception { public void deviceCreationAfterDeleted() throws Exception {
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.createdDevice.getId()); testRestClient.deleteDevice(this.createdDevice.getId());
Optional<Device> deletedDevice = restClient.getDeviceById(this.createdDevice.getId()); testRestClient.getDeviceById(this.createdDevice.getId(), HttpStatus.NOT_FOUND.value());
Assert.assertTrue(deletedDevice.isEmpty());
this.createdDevice = createDeviceThroughGateway(mqttClient, gatewayDevice); this.createdDevice = createDeviceThroughGateway(mqttClient, gatewayDevice);
} }
@ -397,13 +372,13 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
log.info(gatewayAttributesRequest.toString()); log.info(gatewayAttributesRequest.toString());
mqttClient.publish("v1/gateway/attributes/request", Unpooled.wrappedBuffer(gatewayAttributesRequest.toString().getBytes())).get(); mqttClient.publish("v1/gateway/attributes/request", Unpooled.wrappedBuffer(gatewayAttributesRequest.toString().getBytes())).get();
MqttEvent clientAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); MqttEvent clientAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertNotNull(clientAttributeEvent); assertThat(clientAttributeEvent).isNotNull();
JsonObject responseMessage = new JsonParser().parse(Objects.requireNonNull(clientAttributeEvent).getMessage()).getAsJsonObject(); JsonObject responseMessage = new JsonParser().parse(Objects.requireNonNull(clientAttributeEvent).getMessage()).getAsJsonObject();
Assert.assertEquals(messageId, responseMessage.get("id").getAsInt()); assertThat(responseMessage.get("id").getAsInt()).isEqualTo(messageId);
Assert.assertEquals(createdDevice.getName(), responseMessage.get("device").getAsString()); assertThat(responseMessage.get("device").getAsString()).isEqualTo(createdDevice.getName());
Assert.assertEquals(3, responseMessage.entrySet().size()); assertThat(responseMessage.entrySet()).hasSize(3);
Assert.assertEquals(expectedValue, responseMessage.get("value").getAsString()); assertThat(responseMessage.get("value").getAsString()).isEqualTo(expectedValue);
} }
private Device createDeviceThroughGateway(MqttClient mqttClient, Device gatewayDevice) throws Exception { private Device createDeviceThroughGateway(MqttClient mqttClient, Device gatewayDevice) throws Exception {
@ -411,24 +386,19 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
TimeUnit.SECONDS.sleep(30); TimeUnit.SECONDS.sleep(30);
} }
String deviceName = "mqtt_device"; String deviceName = "mqtt_device" + RandomStringUtils.randomAlphabetic(5);
mqttClient.publish("v1/gateway/connect", Unpooled.wrappedBuffer(createGatewayConnectPayload(deviceName).toString().getBytes()), MqttQoS.AT_LEAST_ONCE).get(); mqttClient.publish("v1/gateway/connect", Unpooled.wrappedBuffer(createGatewayConnectPayload(deviceName).toString().getBytes()), MqttQoS.AT_LEAST_ONCE).get();
if (timeoutMultiplier > 1) { if (timeoutMultiplier > 1) {
TimeUnit.SECONDS.sleep(30); TimeUnit.SECONDS.sleep(30);
} }
List<EntityRelation> relations = restClient.findByFrom(gatewayDevice.getId(), RelationTypeGroup.COMMON); List<EntityRelation> relations = testRestClient.findRelationByFrom(gatewayDevice.getId(), RelationTypeGroup.COMMON);
assertThat(relations).hasSize(1);
Assert.assertEquals(1, relations.size());
EntityId createdEntityId = relations.get(0).getTo(); EntityId createdEntityId = relations.get(0).getTo();
DeviceId createdDeviceId = new DeviceId(createdEntityId.getId()); DeviceId createdDeviceId = new DeviceId(createdEntityId.getId());
Optional<Device> createdDevice = restClient.getDeviceById(createdDeviceId); return testRestClient.getDeviceById(createdDeviceId);
Assert.assertTrue(createdDevice.isPresent());
return createdDevice.get();
} }
private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException, ExecutionException { private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException, ExecutionException {

40
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2022 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.msa.prototypes;
import com.fasterxml.jackson.databind.JsonNode;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
public class DevicePrototypes {
public static Device defaultDevicePrototype(String name){
Device device = new Device();
device.setName(name + RandomStringUtils.randomAlphanumeric(7));
device.setType("DEFAULT");
return device;
}
public static Device defaultGatewayPrototype() {
String isGateway = "{\"gateway\":true}";
JsonNode additionalInfo = JacksonUtil.toJsonNode(isGateway);
Device gatewayDeviceTemplate = new Device();
gatewayDeviceTemplate.setName("mqtt_gateway_" + RandomStringUtils.randomAlphanumeric(5));
gatewayDeviceTemplate.setType("gateway");
gatewayDeviceTemplate.setAdditionalInfo(additionalInfo);
return gatewayDeviceTemplate;
}
}

2
msa/black-box-tests/src/test/resources/config.properties

@ -0,0 +1,2 @@
tb.baseUrl=http://localhost:8080
tb.wsUrl=ws://localhost:8080

27
msa/black-box-tests/src/test/resources/testNG.xml

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Copyright © 2016-2022 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.
-->
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Black-box tests">
<test verbose="2" name="Connectivity tests" preserve-order="false">
<packages>
<package name="org.thingsboard.server.msa.connectivity" />
</packages>
</test>
</suite>

2
msa/js-executor/api/jsExecutor.models.ts

@ -56,7 +56,7 @@ export interface JsCompileResponse extends TbMessage {
export interface JsInvokeResponse { export interface JsInvokeResponse {
success: boolean; success: boolean;
result: string; result?: string;
errorCode?: number; errorCode?: number;
errorDetails?: string; errorDetails?: string;
} }

11
msa/js-executor/api/jsInvokeMessageProcessor.ts

@ -39,6 +39,7 @@ const TIMEOUT_ERROR = 2;
const NOT_FOUND_ERROR = 3; const NOT_FOUND_ERROR = 3;
const statFrequency = Number(config.get('script.stat_print_frequency')); const statFrequency = Number(config.get('script.stat_print_frequency'));
const memoryUsageTraceFrequency = Number(config.get('script.memory_usage_trace_frequency'));
const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency')); const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency'));
const useSandbox = config.get('script.use_sandbox') === 'true'; const useSandbox = config.get('script.use_sandbox') === 'true';
const maxActiveScripts = Number(config.get('script.max_active_scripts')); const maxActiveScripts = Number(config.get('script.max_active_scripts'));
@ -167,11 +168,15 @@ export class JsInvokeMessageProcessor {
if (this.executedScriptsCounter % scriptBodyTraceFrequency == 0) { if (this.executedScriptsCounter % scriptBodyTraceFrequency == 0) {
this.logger.info('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody); this.logger.info('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody);
} }
if (this.executedScriptsCounter % memoryUsageTraceFrequency == 0) {
this.logger.info('Current memory usage: [%s]', process.memoryUsage());
}
this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then( this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then(
(script) => { (script) => {
this.executor.executeScript(script, invokeRequest.args, invokeRequest.timeout).then( this.executor.executeScript(script, invokeRequest.args, invokeRequest.timeout).then(
(result) => { (result: string | undefined) => {
if (result.length <= maxResultSize) { if (!result || result.length <= maxResultSize) {
const invokeResponse = JsInvokeMessageProcessor.createInvokeResponse(result, true); const invokeResponse = JsInvokeMessageProcessor.createInvokeResponse(result, true);
this.logger.debug('[%s] Sending success invoke response, scriptId: [%s]', requestId, scriptId); this.logger.debug('[%s] Sending success invoke response, scriptId: [%s]', requestId, scriptId);
this.sendResponse(requestId, responseTopic, headers, scriptId, undefined, invokeResponse); this.sendResponse(requestId, responseTopic, headers, scriptId, undefined, invokeResponse);
@ -323,7 +328,7 @@ export class JsInvokeMessageProcessor {
} }
} }
private static createInvokeResponse(result: string, success: boolean, errorCode?: number, err?: any): JsInvokeResponse { private static createInvokeResponse(result: string | undefined, success: boolean, errorCode?: number, err?: any): JsInvokeResponse {
return { return {
errorCode: errorCode, errorCode: errorCode,
success: success, success: success,

1
msa/js-executor/config/custom-environment-variables.yml

@ -75,6 +75,7 @@ logger:
script: script:
use_sandbox: "SCRIPT_USE_SANDBOX" use_sandbox: "SCRIPT_USE_SANDBOX"
memory_usage_trace_frequency: "MEMORY_USAGE_TRACE_FREQUENCY"
stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY" stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY"
script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY" script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY"
max_active_scripts: "MAX_ACTIVE_SCRIPTS" max_active_scripts: "MAX_ACTIVE_SCRIPTS"

1
msa/js-executor/config/default.yml

@ -64,6 +64,7 @@ logger:
script: script:
use_sandbox: "true" use_sandbox: "true"
memory_usage_trace_frequency: "1000"
script_body_trace_frequency: "10000" script_body_trace_frequency: "10000"
stat_print_frequency: "10000" stat_print_frequency: "10000"
max_active_scripts: "1000" max_active_scripts: "1000"

2
msa/js-executor/docker/start-js-executor.sh

@ -27,4 +27,4 @@ source "${CONF_FOLDER}/${configfile}"
cd ${pkg.installFolder} cd ${pkg.installFolder}
# This will forward this PID 1 to the node.js and forward SIGTERM for graceful shutdown as well # This will forward this PID 1 to the node.js and forward SIGTERM for graceful shutdown as well
exec node server.js exec node --no-compilation-cache server.js

4
packaging/java/scripts/install/logback.xml

@ -56,6 +56,10 @@
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />
</logger> </logger>
<logger name="org.thingsboard.server.config.jwt" level="INFO">
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.thingsboard.server" level="INFO" /> <logger name="org.thingsboard.server" level="INFO" />
<root level="INFO"> <root level="INFO">

34
pom.xml

@ -77,7 +77,7 @@
<zookeeper.version>3.5.5</zookeeper.version> <zookeeper.version>3.5.5</zookeeper.version>
<protobuf.version>3.21.9</protobuf.version> <protobuf.version>3.21.9</protobuf.version>
<grpc.version>1.42.1</grpc.version> <grpc.version>1.42.1</grpc.version>
<mvel.version>2.4.23TB</mvel.version> <tbel.version>1.0.0</tbel.version>
<lombok.version>1.18.18</lombok.version> <lombok.version>1.18.18</lombok.version>
<paho.client.version>1.2.4</paho.client.version> <paho.client.version>1.2.4</paho.client.version>
<netty.version>4.1.75.Final</netty.version> <netty.version>4.1.75.Final</netty.version>
@ -134,6 +134,10 @@
<spring-test-dbunit.version>1.3.0</spring-test-dbunit.version> <!-- 2016 --> <spring-test-dbunit.version>1.3.0</spring-test-dbunit.version> <!-- 2016 -->
<takari-cpsuite.version>1.2.7</takari-cpsuite.version> <!-- 2015 --> <takari-cpsuite.version>1.2.7</takari-cpsuite.version> <!-- 2015 -->
<!-- BLACKBOX TEST SCOPE --> <!-- BLACKBOX TEST SCOPE -->
<testng.version>7.6.1</testng.version>
<assertj.version>3.23.1</assertj.version>
<rest-assured.version>5.2.0</rest-assured.version>
<hamcrest.version>1.3</hamcrest.version>
<testcontainers.version>1.17.3</testcontainers.version> <testcontainers.version>1.17.3</testcontainers.version>
<zeroturnaround.version>1.12</zeroturnaround.version> <zeroturnaround.version>1.12</zeroturnaround.version>
<opensmpp.version>3.0.0</opensmpp.version> <opensmpp.version>3.0.0</opensmpp.version>
@ -1572,8 +1576,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>mvel2</artifactId> <artifactId>tbel</artifactId>
<version>${mvel.version}</version> <version>${tbel.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
@ -1635,6 +1639,30 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.awaitility</groupId> <groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId> <artifactId>awaitility</artifactId>

26
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -136,6 +136,8 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
@ -286,6 +288,23 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
return restTemplate.postForEntity(baseURL + "/api/admin/securitySettings", securitySettings, SecuritySettings.class).getBody(); return restTemplate.postForEntity(baseURL + "/api/admin/securitySettings", securitySettings, SecuritySettings.class).getBody();
} }
public Optional<JwtSettings> getJwtSettings() {
try {
ResponseEntity<JwtSettings> jwtSettings = restTemplate.getForEntity(baseURL + "/api/admin/jwtSettings", JwtSettings.class);
return Optional.ofNullable(jwtSettings.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public JwtPair saveJwtSettings(JwtSettings jwtSettings) {
return restTemplate.postForEntity(baseURL + "/api/admin/jwtSettings", jwtSettings, JwtPair.class).getBody();
}
public Optional<RepositorySettings> getRepositorySettings() { public Optional<RepositorySettings> getRepositorySettings() {
try { try {
ResponseEntity<RepositorySettings> repositorySettings = restTemplate.getForEntity(baseURL + "/api/admin/repositorySettings", RepositorySettings.class); ResponseEntity<RepositorySettings> repositorySettings = restTemplate.getForEntity(baseURL + "/api/admin/repositorySettings", RepositorySettings.class);
@ -2032,10 +2051,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
} }
public PageData<RuleChain> getRuleChains(PageLink pageLink) { public PageData<RuleChain> getRuleChains(PageLink pageLink) {
return getRuleChains(RuleChainType.CORE, pageLink);
}
public PageData<RuleChain> getRuleChains(RuleChainType ruleChainType, PageLink pageLink) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
params.put("type", ruleChainType.name());
addPageLinkToParam(params, pageLink); addPageLinkToParam(params, pageLink);
return restTemplate.exchange( return restTemplate.exchange(
baseURL + "/api/ruleChains?" + getUrlParams(pageLink), baseURL + "/api/ruleChains?type={type}&" + getUrlParams(pageLink),
HttpMethod.GET, HttpMethod.GET,
HttpEntity.EMPTY, HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<RuleChain>>() { new ParameterizedTypeReference<PageData<RuleChain>>() {

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java

@ -48,7 +48,7 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadAlarmNodeConfig(configuration); this.config = loadAlarmNodeConfig(configuration);
scriptEngine = ctx.createScriptEngine(config.getScriptLang(), scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getAlarmDetailsBuildMvel() : config.getAlarmDetailsBuildJs()); ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getAlarmDetailsBuildTbel() : config.getAlarmDetailsBuildJs());
} }
protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException; protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java

@ -33,7 +33,7 @@ public abstract class TbAbstractAlarmNodeConfiguration {
"\n" + "\n" +
"return details;"; "return details;";
static final String ALARM_DETAILS_BUILD_MVEL_TEMPLATE = "" + static final String ALARM_DETAILS_BUILD_TBEL_TEMPLATE = "" +
"var details = {};\n" + "var details = {};\n" +
"if (metadata.prevAlarmDetails != null) {\n" + "if (metadata.prevAlarmDetails != null) {\n" +
" details = JSON.parse(metadata.prevAlarmDetails);\n" + " details = JSON.parse(metadata.prevAlarmDetails);\n" +
@ -49,6 +49,6 @@ public abstract class TbAbstractAlarmNodeConfiguration {
private String alarmType; private String alarmType;
private ScriptLanguage scriptLang; private ScriptLanguage scriptLang;
private String alarmDetailsBuildJs; private String alarmDetailsBuildJs;
private String alarmDetailsBuildMvel; private String alarmDetailsBuildTbel;
} }

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data @Data
@ -26,9 +25,9 @@ public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigurat
@Override @Override
public TbClearAlarmNodeConfiguration defaultConfiguration() { public TbClearAlarmNodeConfiguration defaultConfiguration() {
TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration(); TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL); configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE); configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmDetailsBuildMvel(ALARM_DETAILS_BUILD_MVEL_TEMPLATE); configuration.setAlarmDetailsBuildTbel(ALARM_DETAILS_BUILD_TBEL_TEMPLATE);
configuration.setAlarmType("General Alarm"); configuration.setAlarmType("General Alarm");
return configuration; return configuration;
} }

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java

@ -39,9 +39,9 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
@Override @Override
public TbCreateAlarmNodeConfiguration defaultConfiguration() { public TbCreateAlarmNodeConfiguration defaultConfiguration() {
TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration(); TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL); configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE); configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmDetailsBuildMvel(ALARM_DETAILS_BUILD_MVEL_TEMPLATE); configuration.setAlarmDetailsBuildTbel(ALARM_DETAILS_BUILD_TBEL_TEMPLATE);
configuration.setAlarmType("General Alarm"); configuration.setAlarmType("General Alarm");
configuration.setSeverity(AlarmSeverity.CRITICAL.name()); configuration.setSeverity(AlarmSeverity.CRITICAL.name());
configuration.setPropagate(false); configuration.setPropagate(false);

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java

@ -56,7 +56,7 @@ public class TbLogNode implements TbNode {
this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
this.standard = new TbLogNodeConfiguration().defaultConfiguration().getJsScript().equals(config.getJsScript()); this.standard = new TbLogNodeConfiguration().defaultConfiguration().getJsScript().equals(config.getJsScript());
this.scriptEngine = this.standard ? null : ctx.createScriptEngine(config.getScriptLang(), this.scriptEngine = this.standard ? null : ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript()); ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
} }
@Override @Override

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java

@ -24,14 +24,14 @@ public class TbLogNodeConfiguration implements NodeConfiguration {
private ScriptLanguage scriptLang; private ScriptLanguage scriptLang;
private String jsScript; private String jsScript;
private String mvelScript; private String tbelScript;
@Override @Override
public TbLogNodeConfiguration defaultConfiguration() { public TbLogNodeConfiguration defaultConfiguration() {
TbLogNodeConfiguration configuration = new TbLogNodeConfiguration(); TbLogNodeConfiguration configuration = new TbLogNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL); configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setJsScript("return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"); configuration.setJsScript("return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);");
configuration.setMvelScript("return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"); configuration.setTbelScript("return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);");
return configuration; return configuration;
} }
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java

@ -96,7 +96,7 @@ public class TbMsgGeneratorNode implements TbNode {
if (ctx.isLocalEntity(originatorId)) { if (ctx.isLocalEntity(originatorId)) {
if (initialized.compareAndSet(false, true)) { if (initialized.compareAndSet(false, true)) {
this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(), this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType"); ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
scheduleTickMsg(ctx); scheduleTickMsg(ctx);
} }
} else if (initialized.compareAndSet(true, false)) { } else if (initialized.compareAndSet(true, false)) {

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java

@ -35,16 +35,16 @@ public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgG
private EntityType originatorType; private EntityType originatorType;
private ScriptLanguage scriptLang; private ScriptLanguage scriptLang;
private String jsScript; private String jsScript;
private String mvelScript; private String tbelScript;
@Override @Override
public TbMsgGeneratorNodeConfiguration defaultConfiguration() { public TbMsgGeneratorNodeConfiguration defaultConfiguration() {
TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration(); TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration();
configuration.setMsgCount(UNLIMITED_MSG_COUNT); configuration.setMsgCount(UNLIMITED_MSG_COUNT);
configuration.setPeriodInSeconds(1); configuration.setPeriodInSeconds(1);
configuration.setScriptLang(ScriptLanguage.MVEL); configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setJsScript(DEFAULT_SCRIPT); configuration.setJsScript(DEFAULT_SCRIPT);
configuration.setMvelScript(DEFAULT_SCRIPT); configuration.setTbelScript(DEFAULT_SCRIPT);
return configuration; return configuration;
} }
} }

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -23,7 +23,6 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -53,7 +52,7 @@ public class TbJsFilterNode implements TbNode {
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
scriptEngine = ctx.createScriptEngine(config.getScriptLang(), scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript()); ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
} }
@Override @Override

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java

@ -24,14 +24,14 @@ public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilter
private ScriptLanguage scriptLang; private ScriptLanguage scriptLang;
private String jsScript; private String jsScript;
private String mvelScript; private String tbelScript;
@Override @Override
public TbJsFilterNodeConfiguration defaultConfiguration() { public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration(); TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL); configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setJsScript("return msg.temperature > 20;"); configuration.setJsScript("return msg.temperature > 20;");
configuration.setMvelScript("return msg.temperature > 20;"); configuration.setTbelScript("return msg.temperature > 20;");
return configuration; return configuration;
} }
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java

@ -56,7 +56,7 @@ public class TbJsSwitchNode implements TbNode {
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(), this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript()); ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
} }
@Override @Override

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java

@ -15,13 +15,10 @@
*/ */
package org.thingsboard.rule.engine.filter; package org.thingsboard.rule.engine.filter;
import com.google.common.collect.Sets;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.script.ScriptLanguage;
import java.util.Set;
@Data @Data
public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> { public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
@ -33,7 +30,7 @@ public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitch
"}\n" + "}\n" +
"return nextRelation(metadata, msg);"; "return nextRelation(metadata, msg);";
private static final String DEFAULT_MVEL_SCRIPT = "function nextRelation(metadata, msg) {\n" + private static final String DEFAULT_TBEL_SCRIPT = "function nextRelation(metadata, msg) {\n" +
" return ['one','nine'];\n" + " return ['one','nine'];\n" +
"}\n" + "}\n" +
"if(msgType == 'POST_TELEMETRY_REQUEST') {\n" + "if(msgType == 'POST_TELEMETRY_REQUEST') {\n" +
@ -43,14 +40,14 @@ public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitch
private ScriptLanguage scriptLang; private ScriptLanguage scriptLang;
private String jsScript; private String jsScript;
private String mvelScript; private String tbelScript;
@Override @Override
public TbJsSwitchNodeConfiguration defaultConfiguration() { public TbJsSwitchNodeConfiguration defaultConfiguration() {
TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration(); TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL); configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setJsScript(DEFAULT_JS_SCRIPT); configuration.setJsScript(DEFAULT_JS_SCRIPT);
configuration.setMvelScript(DEFAULT_MVEL_SCRIPT); configuration.setTbelScript(DEFAULT_TBEL_SCRIPT);
return configuration; return configuration;
} }
} }

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -78,16 +79,20 @@ public class TbFetchDeviceCredentialsNode implements TbNode {
} }
TbMsg transformedMsg; TbMsg transformedMsg;
String credentialsType = deviceCredentials.getCredentialsType().name(); DeviceCredentialsType credentialsType = deviceCredentials.getCredentialsType();
JsonNode credentialsInfo = ctx.getDeviceCredentialsService().toCredentialsInfo(deviceCredentials); JsonNode credentialsInfo = ctx.getDeviceCredentialsService().toCredentialsInfo(deviceCredentials);
if (fetchToMetadata) { if (fetchToMetadata) {
TbMsgMetaData metaData = msg.getMetaData(); TbMsgMetaData metaData = msg.getMetaData();
metaData.putValue(CREDENTIALS_TYPE, credentialsType); metaData.putValue(CREDENTIALS_TYPE, credentialsType.name());
metaData.putValue(CREDENTIALS, JacksonUtil.toString(credentialsInfo)); if (credentialsType.equals(DeviceCredentialsType.ACCESS_TOKEN) || credentialsType.equals(DeviceCredentialsType.X509_CERTIFICATE)) {
metaData.putValue(CREDENTIALS, credentialsInfo.asText());
} else {
metaData.putValue(CREDENTIALS, JacksonUtil.toString(credentialsInfo));
}
transformedMsg = TbMsg.transformMsg(msg, msg.getType(), originator, metaData, msg.getData()); transformedMsg = TbMsg.transformMsg(msg, msg.getType(), originator, metaData, msg.getData());
} else { } else {
ObjectNode data = (ObjectNode) JacksonUtil.toJsonNode(msg.getData()); ObjectNode data = (ObjectNode) JacksonUtil.toJsonNode(msg.getData());
data.put(CREDENTIALS_TYPE, credentialsType); data.put(CREDENTIALS_TYPE, credentialsType.name());
data.set(CREDENTIALS, credentialsInfo); data.set(CREDENTIALS, credentialsInfo);
transformedMsg = TbMsg.transformMsg(msg, msg.getType(), originator, msg.getMetaData(), JacksonUtil.toString(data)); transformedMsg = TbMsg.transformMsg(msg, msg.getType(), originator, msg.getMetaData(), JacksonUtil.toString(data));
} }

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

Loading…
Cancel
Save