Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into bug/fix-edge-fetcher

pull/7651/head
Volodymyr Babak 4 years ago
parent
commit
6766d54b86
  1. 1
      application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java
  2. 49
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  3. 6
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  4. 6
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  5. 6
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  6. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  7. 15
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  8. 4
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  9. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  10. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  11. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  12. 23
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  13. 162
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java
  14. 69
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
  15. 39
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java
  16. 35
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java
  17. 23
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java
  18. 17
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  19. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  20. 32
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  21. 6
      application/src/main/resources/thingsboard.yml
  22. 122
      application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java
  23. 4
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  24. 98
      application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java
  25. 10
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  26. 68
      application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandlerTest.java
  27. 2
      application/src/test/resources/logback-test.xml
  28. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java
  29. 9
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java
  30. 30
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java
  31. 4
      common/script/script-api/pom.xml
  32. 78
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java
  33. 1
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java
  34. 2
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  35. 11
      dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java
  36. 19
      dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java
  37. 17
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java
  38. 5
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  39. 8
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  40. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  41. 79
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  42. 3
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
  43. 39
      dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java
  44. 133
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningDaysAlwaysExistsTest.java
  45. 134
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningHoursAlwaysExistsTest.java
  46. 78
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningIndefiniteAlwaysExistsTest.java
  47. 121
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java
  48. 134
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java
  49. 116
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java
  50. 3
      docker/tb-js-executor.env
  51. 1
      lombok.config
  52. 4
      msa/black-box-tests/README.md
  53. 38
      msa/black-box-tests/pom.xml
  54. 130
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java
  55. 221
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  56. 53
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java
  57. 61
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java
  58. 262
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  59. 9
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java
  60. 112
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java
  61. 226
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  62. 220
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java
  63. 40
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java
  64. 2
      msa/black-box-tests/src/test/resources/config.properties
  65. 27
      msa/black-box-tests/src/test/resources/testNG.xml
  66. 2
      msa/js-executor/api/jsExecutor.models.ts
  67. 11
      msa/js-executor/api/jsInvokeMessageProcessor.ts
  68. 1
      msa/js-executor/config/custom-environment-variables.yml
  69. 1
      msa/js-executor/config/default.yml
  70. 2
      msa/js-executor/docker/start-js-executor.sh
  71. 4
      packaging/java/scripts/install/logback.xml
  72. 30
      pom.xml
  73. 19
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  74. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java
  75. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  76. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  77. 69
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java
  78. 22
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
  79. 16
      ui-ngx/src/app/core/api/alarm-data-subscription.ts
  80. 16
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  81. 22
      ui-ngx/src/app/core/auth/auth.service.ts
  82. 18
      ui-ngx/src/app/core/http/admin.service.ts
  83. 4
      ui-ngx/src/app/core/http/attribute.service.ts
  84. 5
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  85. 2
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  86. 375
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html
  87. 23
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss
  88. 142
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts
  89. 2
      ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html
  90. 7
      ui-ngx/src/app/shared/models/settings.models.ts
  91. 22
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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",

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 {

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

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

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

6
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
@ -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}"
@ -625,6 +626,7 @@ mvel:
max_black_list_duration_sec: "${MVEL_MAX_BLACKLIST_DURATION_SEC:60}" max_black_list_duration_sec: "${MVEL_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: "${MVEL_THREAD_POOL_SIZE:50}"
compiled_scripts_cache_size: "${MVEL_COMPILED_SCRIPTS_CACHE_SIZE:1000}"
stats: stats:
enabled: "${TB_MVEL_STATS_ENABLED:false}" enabled: "${TB_MVEL_STATS_ENABLED:false}"
print_interval_ms: "${TB_MVEL_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_MVEL_STATS_PRINT_INTERVAL_MS:10000}"

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

98
application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.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;
@ -24,15 +25,22 @@ 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.mvel.MvelInvokeService;
import org.thingsboard.script.api.mvel.MvelScript;
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
@ -41,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
"mvel.max_total_args_size=50", "mvel.max_total_args_size=50",
"mvel.max_result_size=50", "mvel.max_result_size=50",
"mvel.max_errors=2", "mvel.max_errors=2",
"mvel.compiled_scripts_cache_size=100"
}) })
class MvelInvokeServiceTest extends AbstractControllerTest { class MvelInvokeServiceTest extends AbstractControllerTest {
@ -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, MvelScript> 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, MvelScript> 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++) { // mvel.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"/>

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;

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

4
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>

78
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java

@ -15,6 +15,10 @@
*/ */
package org.thingsboard.script.api.mvel; package org.thingsboard.script.api.mvel;
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,12 +46,15 @@ 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
@ -55,7 +62,10 @@ import java.util.regex.Pattern;
@Service @Service
public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService { public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService {
protected Map<UUID, MvelScript> scriptMap = new ConcurrentHashMap<>(); protected final Map<UUID, String> scriptIdToHash = new ConcurrentHashMap<>();
protected final Map<String, MvelScript> scriptMap = new ConcurrentHashMap<>();
protected Cache<String, Serializable> compiledScriptsCache;
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");
@ -92,8 +102,13 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Value("${mvel.max_memory_limit_mb:8}") @Value("${mvel.max_memory_limit_mb:8}")
private long maxMemoryLimitMb; private long maxMemoryLimitMb;
@Value("${mvel.compiled_scripts_cache_size:1000}")
private int compiledScriptsCacheSize;
private ListeningExecutorService executor; private ListeningExecutorService executor;
private final Lock lock = new ReentrantLock();
protected DefaultMvelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) { protected DefaultMvelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
super(apiUsageStateClient, apiUsageReportClient); super(apiUsageStateClient, apiUsageReportClient);
} }
@ -115,11 +130,14 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "mvel-executor")); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "mvel-executor"));
try { try {
// Special command to warm up MVEL engine // Special command to warm up MVEL 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
@ -141,16 +159,26 @@ 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 -> {
scriptMap.put(scriptId, script); return compileScript(scriptBody);
});
lock.lock();
try {
scriptIdToHash.put(scriptId, scriptHash);
scriptMap.computeIfAbsent(scriptHash, k -> {
return new MvelScript(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);
@ -162,12 +190,16 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
protected MvelScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) { protected MvelScriptExecutionTask 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 MvelScriptExecutionTask(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!"));
} }
MvelScript script = scriptMap.get(scriptHash);
Serializable compiledScript = compiledScriptsCache.get(scriptHash, k -> {
return 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) {
@ -178,6 +210,32 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Override @Override
protected void doRelease(UUID scriptId) throws Exception { protected void doRelease(UUID scriptId) throws Exception {
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();
}
} }

1
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java

@ -24,7 +24,6 @@ import java.util.Map;
@Data @Data
public class MvelScript { public class MvelScript {
private final Serializable compiledScript;
private final String scriptBody; private final String scriptBody;
private final String[] argNames; private final String[] argNames;

2
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -731,6 +731,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_rule_chain_device_profile")) { if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_rule_chain_device_profile")) {
throw new DataValidationException("The rule chain referenced by the device profiles cannot be deleted!"); throw new DataValidationException("The rule chain referenced by the device profiles cannot be deleted!");
} else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_rule_chain_asset_profile")) {
throw new DataValidationException("The rule chain referenced by the asset profiles cannot be deleted!");
} else { } else {
throw t; throw t;
} }

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

30
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> <mvel.version>2.4.25TB</mvel.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>
@ -1634,6 +1638,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>

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

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

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

@ -41,6 +41,7 @@ import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.TbRelationTypes;
@ -59,8 +60,11 @@ import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Deque; import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
@Data @Data
@Slf4j @Slf4j
@ -244,17 +248,33 @@ public class TbHttpClient {
metaData.putValue(STATUS, response.getStatusCode().name()); metaData.putValue(STATUS, response.getStatusCode().name());
metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); metaData.putValue(STATUS_CODE, response.getStatusCode().value() + "");
metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
response.getHeaders().toSingleValueMap().forEach(metaData::putValue); headersToMetaData(response.getHeaders(), metaData::putValue);
String body = response.getBody() == null ? "{}" : response.getBody(); String body = response.getBody() == null ? "{}" : response.getBody();
return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, body); return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, body);
} }
void headersToMetaData(Map<String, List<String>> headers, BiConsumer<String, String> consumer) {
if (headers == null) {
return;
}
headers.forEach((key, values) -> {
if (values != null && !values.isEmpty()) {
if (values.size() == 1) {
consumer.accept(key, values.get(0));
} else {
consumer.accept(key, JacksonUtil.toString(values));
}
}
});
}
private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) { private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) {
TbMsgMetaData metaData = origMsg.getMetaData(); TbMsgMetaData metaData = origMsg.getMetaData();
metaData.putValue(STATUS, response.getStatusCode().name()); metaData.putValue(STATUS, response.getStatusCode().name());
metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); metaData.putValue(STATUS_CODE, response.getStatusCode().value() + "");
metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
metaData.putValue(ERROR_BODY, response.getBody()); metaData.putValue(ERROR_BODY, response.getBody());
headersToMetaData(response.getHeaders(), metaData::putValue);
return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
} }

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -127,6 +127,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
log.error("EntityNamePattern not specified for type [{}]", conf.getEntityType()); log.error("EntityNamePattern not specified for type [{}]", conf.getEntityType());
throw new IllegalArgumentException("Wrong config for [{}] in TbChangeOriginatorNode!" + ENTITY_SOURCE); throw new IllegalArgumentException("Wrong config for [{}] in TbChangeOriginatorNode!" + ENTITY_SOURCE);
} }
EntitiesByNameAndTypeLoader.checkEntityType(EntityType.valueOf(conf.getEntityType()));
} }
} }

69
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java

@ -16,75 +16,52 @@
package org.thingsboard.rule.engine.util; package org.thingsboard.rule.engine.util;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DashboardInfo;
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.EntityView; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.util.Optional; import java.util.List;
public class EntitiesByNameAndTypeLoader { public class EntitiesByNameAndTypeLoader {
private static final List<EntityType> AVAILABLE_ENTITY_TYPES = List.of(
EntityType.DEVICE,
EntityType.ASSET,
EntityType.ENTITY_VIEW,
EntityType.EDGE,
EntityType.USER);
public static EntityId findEntityId(TbContext ctx, EntityType entityType, String entityName) { public static EntityId findEntityId(TbContext ctx, EntityType entityType, String entityName) {
EntityId targetEntityId = null; SearchTextBasedWithAdditionalInfo<? extends EntityId> targetEntity;
switch (entityType) { switch (entityType) {
case DEVICE: case DEVICE:
Device device = ctx.getDeviceService().findDeviceByTenantIdAndName(ctx.getTenantId(), entityName); targetEntity = ctx.getDeviceService().findDeviceByTenantIdAndName(ctx.getTenantId(), entityName);
if (device != null) {
targetEntityId = device.getId();
}
break; break;
case ASSET: case ASSET:
Asset asset = ctx.getAssetService().findAssetByTenantIdAndName(ctx.getTenantId(), entityName); targetEntity = ctx.getAssetService().findAssetByTenantIdAndName(ctx.getTenantId(), entityName);
if (asset != null) {
targetEntityId = asset.getId();
}
break;
case CUSTOMER:
Optional<Customer> customerOptional = ctx.getCustomerService().findCustomerByTenantIdAndTitle(ctx.getTenantId(), entityName);
if (customerOptional.isPresent()) {
targetEntityId = customerOptional.get().getId();
}
break;
case TENANT:
targetEntityId = ctx.getTenantId();
break; break;
case ENTITY_VIEW: case ENTITY_VIEW:
EntityView entityView = ctx.getEntityViewService().findEntityViewByTenantIdAndName(ctx.getTenantId(), entityName); targetEntity = ctx.getEntityViewService().findEntityViewByTenantIdAndName(ctx.getTenantId(), entityName);
if (entityView != null) {
targetEntityId = entityView.getId();
}
break; break;
case EDGE: case EDGE:
Edge edge = ctx.getEdgeService().findEdgeByTenantIdAndName(ctx.getTenantId(), entityName); targetEntity = ctx.getEdgeService().findEdgeByTenantIdAndName(ctx.getTenantId(), entityName);
if (edge != null) {
targetEntityId = edge.getId();
}
break;
case DASHBOARD:
DashboardInfo dashboardInfo = ctx.getDashboardService().findFirstDashboardInfoByTenantIdAndName(ctx.getTenantId(), entityName);
if (dashboardInfo != null) {
targetEntityId = dashboardInfo.getId();
}
break; break;
case USER: case USER:
User user = ctx.getUserService().findUserByEmail(ctx.getTenantId(), entityName); targetEntity = ctx.getUserService().findUserByTenantIdAndEmail(ctx.getTenantId(), entityName);
if (user != null) {
targetEntityId = user.getId();
}
break; break;
default: default:
throw new IllegalStateException("Unexpected entity type " + entityType.name()); throw new IllegalStateException("Unexpected entity type " + entityType.name());
} }
if (targetEntityId == null) { if (targetEntity == null) {
throw new IllegalStateException("Failed to found " + entityType.name() + " entity by name: '" + entityName + "'!"); throw new IllegalStateException("Failed to found " + entityType.name() + " entity by name: '" + entityName + "'!");
} }
return targetEntityId; return targetEntity.getId();
}
public static void checkEntityType(EntityType entityType) {
if (!AVAILABLE_ENTITY_TYPES.contains(entityType)) {
throw new IllegalStateException("Unexpected entity type " + entityType.name());
}
} }
} }

22
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java

@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.rest;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
@ -26,6 +27,7 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockserver.integration.ClientAndServer; import org.mockserver.integration.ClientAndServer;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.client.AsyncRestTemplate;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -34,6 +36,8 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.net.URI; import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -200,5 +204,23 @@ public class TbHttpClientTest {
); );
} }
@Test
public void testHeadersToMetaData() {
Map<String, List<String>> headers = new LinkedMultiValueMap<>();
headers.put("Content-Type", List.of("binary"));
headers.put("Set-Cookie", List.of("sap-context=sap-client=075; path=/", "sap-token=sap-client=075; path=/"));
TbMsgMetaData metaData = new TbMsgMetaData();
willCallRealMethod().given(client).headersToMetaData(any(), any());
client.headersToMetaData(headers, metaData::putValue);
Map<String, String> data = metaData.getData();
Assertions.assertThat(data).hasSize(2);
Assertions.assertThat(data.get("Content-Type")).isEqualTo("binary");
Assertions.assertThat(data.get("Set-Cookie")).isEqualTo("[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]");
}
} }

16
ui-ngx/src/app/core/api/alarm-data-subscription.ts

@ -62,6 +62,7 @@ export class AlarmDataSubscription {
private alarmDataCommand: AlarmDataCmd; private alarmDataCommand: AlarmDataCmd;
private pageData: PageData<AlarmData>; private pageData: PageData<AlarmData>;
private prematureUpdates: Array<Array<AlarmData>>;
private alarmIdToDataIndex: {[id: string]: number}; private alarmIdToDataIndex: {[id: string]: number};
private subsTw: SubscriptionTimewindow; private subsTw: SubscriptionTimewindow;
@ -136,8 +137,21 @@ export class AlarmDataSubscription {
this.subscriber.alarmData$.subscribe((alarmDataUpdate) => { this.subscriber.alarmData$.subscribe((alarmDataUpdate) => {
if (alarmDataUpdate.data) { if (alarmDataUpdate.data) {
this.onPageData(alarmDataUpdate.data, alarmDataUpdate.allowedEntities, alarmDataUpdate.totalEntities); this.onPageData(alarmDataUpdate.data, alarmDataUpdate.allowedEntities, alarmDataUpdate.totalEntities);
if (this.prematureUpdates) {
for (const update of this.prematureUpdates) {
this.onDataUpdate(update);
}
this.prematureUpdates = null;
}
} else if (alarmDataUpdate.update) { } else if (alarmDataUpdate.update) {
this.onDataUpdate(alarmDataUpdate.update); if (!this.pageData) {
if (!this.prematureUpdates) {
this.prematureUpdates = [];
}
this.prematureUpdates.push(alarmDataUpdate.update);
} else {
this.onDataUpdate(alarmDataUpdate.update);
}
} }
}); });

16
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -120,6 +120,7 @@ export class EntityDataSubscription {
private entityDataResolveSubject: Subject<EntityDataLoadResult>; private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>; private pageData: PageData<EntityData>;
private prematureUpdates: Array<Array<EntityData>>;
private data: Array<Array<DataSetHolder>>; private data: Array<Array<DataSetHolder>>;
private subsTw: SubscriptionTimewindow; private subsTw: SubscriptionTimewindow;
private latestTsOffset: number; private latestTsOffset: number;
@ -348,8 +349,21 @@ export class EntityDataSubscription {
(entityDataUpdate) => { (entityDataUpdate) => {
if (entityDataUpdate.data) { if (entityDataUpdate.data) {
this.onPageData(entityDataUpdate.data); this.onPageData(entityDataUpdate.data);
if (this.prematureUpdates) {
for (const update of this.prematureUpdates) {
this.onDataUpdate(update);
}
this.prematureUpdates = null;
}
} else if (entityDataUpdate.update) { } else if (entityDataUpdate.update) {
this.onDataUpdate(entityDataUpdate.update); if (!this.pageData) {
if (!this.prematureUpdates) {
this.prematureUpdates = [];
}
this.prematureUpdates.push(entityDataUpdate.update);
} else {
this.onDataUpdate(entityDataUpdate.update);
}
} }
} }
); );

22
ui-ngx/src/app/core/auth/auth.service.ts

@ -195,18 +195,22 @@ export class AuthService {
)); ));
} }
public logout(captureLastUrl: boolean = false) { public logout(captureLastUrl: boolean = false, ignoreRequest = false) {
if (captureLastUrl) { if (captureLastUrl) {
this.redirectUrl = this.router.url; this.redirectUrl = this.router.url;
} }
this.http.post('/api/auth/logout', null, defaultHttpOptions(true, true)) if (!ignoreRequest) {
.subscribe(() => { this.http.post('/api/auth/logout', null, defaultHttpOptions(true, true))
this.clearJwtToken(); .subscribe(() => {
}, this.clearJwtToken();
() => { },
this.clearJwtToken(); () => {
} this.clearJwtToken();
); }
);
} else {
this.clearJwtToken();
}
} }
private notifyUserLoaded(isUserLoaded: boolean) { private notifyUserLoaded(isUserLoaded: boolean) {

18
ui-ngx/src/app/core/http/admin.service.ts

@ -20,16 +20,18 @@ import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { import {
AdminSettings, AdminSettings,
RepositorySettings, AutoCommitSettings,
JwtSettings,
MailServerSettings, MailServerSettings,
RepositorySettings,
RepositorySettingsInfo,
SecuritySettings, SecuritySettings,
TestSmsRequest, TestSmsRequest,
UpdateMessage, UpdateMessage
AutoCommitSettings,
RepositorySettingsInfo
} from '@shared/models/settings.models'; } from '@shared/models/settings.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { LoginResponse } from '@shared/models/login.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -70,6 +72,14 @@ export class AdminService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getJwtSettings(config?: RequestConfig): Observable<JwtSettings> {
return this.http.get<JwtSettings>(`/api/admin/jwtSettings`, defaultHttpOptionsFromConfig(config));
}
public saveJwtSettings(jwtSettings: JwtSettings, config?: RequestConfig): Observable<LoginResponse> {
return this.http.post<LoginResponse>('/api/admin/jwtSettings', jwtSettings, defaultHttpOptionsFromConfig(config));
}
public getRepositorySettings(config?: RequestConfig): Observable<RepositorySettings> { public getRepositorySettings(config?: RequestConfig): Observable<RepositorySettings> {
return this.http.get<RepositorySettings>(`/api/admin/repositorySettings`, defaultHttpOptionsFromConfig(config)); return this.http.get<RepositorySettings>(`/api/admin/repositorySettings`, defaultHttpOptionsFromConfig(config));
} }

4
ui-ngx/src/app/core/http/attribute.service.ts

@ -43,7 +43,7 @@ export class AttributeService {
public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>, public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
config?: RequestConfig): Observable<any> { config?: RequestConfig): Observable<any> {
const keys = attributes.map(attribute => encodeURI(attribute.key)).join(','); const keys = attributes.map(attribute => encodeURIComponent(attribute.key)).join(',');
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` +
`?keys=${keys}`, `?keys=${keys}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
@ -51,7 +51,7 @@ export class AttributeService {
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false, public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
startTs?: number, endTs?: number, config?: RequestConfig): Observable<any> { startTs?: number, endTs?: number, config?: RequestConfig): Observable<any> {
const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(','); const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(',');
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` +
`?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`; `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`;
if (isDefinedAndNotNull(startTs)) { if (isDefinedAndNotNull(startTs)) {

5
ui-ngx/src/app/core/interceptors/global-http-interceptor.ts

@ -103,7 +103,8 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
const ignoreErrors = config.ignoreErrors; const ignoreErrors = config.ignoreErrors;
const resendRequest = config.resendRequest; const resendRequest = config.resendRequest;
const errorCode = errorResponse.error ? errorResponse.error.errorCode : null; const errorCode = errorResponse.error ? errorResponse.error.errorCode : null;
if (errorResponse.error && errorResponse.error.refreshTokenPending || errorResponse.status === 401) { if (errorResponse.error && errorResponse.error.refreshTokenPending ||
errorResponse.status === 401 && req.url !== Constants.entryPoints.tokenRefresh) {
if (errorResponse.error && errorResponse.error.refreshTokenPending || if (errorResponse.error && errorResponse.error.refreshTokenPending ||
errorCode && errorCode === Constants.serverErrorCode.jwtTokenExpired) { errorCode && errorCode === Constants.serverErrorCode.jwtTokenExpired) {
return this.refreshTokenAndRetry(req, next); return this.refreshTokenAndRetry(req, next);
@ -153,7 +154,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
return this.jwtIntercept(req, next); return this.jwtIntercept(req, next);
}), }),
catchError((err: Error) => { catchError((err: Error) => {
this.authService.logout(true); this.authService.logout(true, true);
const message = err ? err.message : 'Unauthorized!'; const message = err ? err.message : 'Unauthorized!';
return this.handleResponseError(req, next, new HttpErrorResponse({error: {message, timeout: 200}, status: 401})); return this.handleResponseError(req, next, new HttpErrorResponse({error: {message, timeout: 200}, status: 401}));
})); }));

2
ui-ngx/src/app/core/ws/telemetry-websocket.service.ts

@ -233,7 +233,7 @@ export class TelemetryWebsocketService implements TelemetryService {
}, },
() => { () => {
this.isOpening = false; this.isOpening = false;
this.authService.logout(true); this.authService.logout(true, true);
} }
); );
} }

375
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html

@ -15,144 +15,239 @@
limitations under the License. limitations under the License.
--> -->
<div> <mat-card class="settings-card">
<mat-card class="settings-card"> <mat-card-title>
<mat-card-title> <div fxLayout="row">
<div fxLayout="row"> <span class="mat-headline" translate>admin.security-settings</span>
<span class="mat-headline" translate>admin.security-settings</span> <span fxFlex></span>
<span fxFlex></span> <div tb-help="securitySettings"></div>
<div tb-help="securitySettings"></div> </div>
</div> </mat-card-title>
</mat-card-title> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> </mat-progress-bar>
</mat-progress-bar> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <mat-card-content style="padding-top: 16px;">
<mat-card-content style="padding-top: 16px;"> <form [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()" autocomplete="off">
<form [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()" autocomplete="off"> <fieldset [disabled]="isLoading$ | async">
<fieldset [disabled]="isLoading$ | async"> <fieldset class="fields-group">
<div class="mat-accordion-container"> <legend class="group-title" translate>admin.general-policy</legend>
<mat-accordion multi="true"> <mat-form-field class="mat-block">
<mat-expansion-panel [expanded]="true"> <mat-label translate>admin.max-failed-login-attempts</mat-label>
<mat-expansion-panel-header> <input matInput type="number"
<mat-panel-title> formControlName="maxFailedLoginAttempts"
<div class="tb-panel-title" translate>admin.general-policy</div> step="1"
</mat-panel-title> min="0"/>
</mat-expansion-panel-header> <mat-error *ngIf="securitySettingsFormGroup.get('maxFailedLoginAttempts').hasError('min')">
<mat-form-field class="mat-block"> {{ 'admin.minimum-max-failed-login-attempts-range' | translate }}
<mat-label translate>admin.max-failed-login-attempts</mat-label> </mat-error>
<input matInput type="number" </mat-form-field>
formControlName="maxFailedLoginAttempts" <mat-form-field class="mat-block">
step="1" <mat-label translate>admin.user-lockout-notification-email</mat-label>
min="0"/> <input matInput type="email"
<mat-error *ngIf="securitySettingsFormGroup.get('maxFailedLoginAttempts').hasError('min')"> formControlName="userLockoutNotificationEmail"/>
{{ 'admin.minimum-max-failed-login-attempts-range' | translate }} </mat-form-field>
</mat-error> </fieldset>
</mat-form-field>
<mat-form-field class="mat-block"> <fieldset class="fields-group">
<mat-label translate>admin.user-lockout-notification-email</mat-label> <legend class="group-title" translate>admin.password-policy</legend>
<input matInput type="email" <section formGroupName="passwordPolicy">
formControlName="userLockoutNotificationEmail"/> <mat-form-field class="mat-block">
</mat-form-field> <mat-label translate>admin.minimum-password-length</mat-label>
</mat-expansion-panel> <input matInput type="number"
<mat-expansion-panel [expanded]="true"> formControlName="minimumLength"
<mat-expansion-panel-header> step="1"
<mat-panel-title> min="5"
<div class="tb-panel-title" translate>admin.password-policy</div> max="50"
</mat-panel-title> required/>
</mat-expansion-panel-header> <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')">
<section formGroupName="passwordPolicy"> {{ 'admin.minimum-password-length-required' | translate }}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>admin.minimum-password-length</mat-label> <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('min')">
<input matInput type="number" {{ 'admin.minimum-password-length-range' | translate }}
formControlName="minimumLength" </mat-error>
step="1" <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('max')">
min="5" {{ 'admin.minimum-password-length-range' | translate }}
max="50" </mat-error>
required/> </mat-form-field>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
{{ 'admin.minimum-password-length-required' | translate }} <mat-form-field fxFlex class="mat-block">
</mat-error> <mat-label translate>admin.minimum-uppercase-letters</mat-label>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('min')"> <input matInput type="number"
{{ 'admin.minimum-password-length-range' | translate }} formControlName="minimumUppercaseLetters"
</mat-error> step="1"
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('max')"> min="0"/>
{{ 'admin.minimum-password-length-range' | translate }} <mat-error
</mat-error> *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumUppercaseLetters').hasError('min')">
</mat-form-field> {{ 'admin.minimum-uppercase-letters-range' | translate }}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>admin.minimum-uppercase-letters</mat-label> </mat-form-field>
<input matInput type="number" <mat-form-field fxFlex class="mat-block">
formControlName="minimumUppercaseLetters" <mat-label translate>admin.minimum-lowercase-letters</mat-label>
step="1" <input matInput type="number"
min="0"/> formControlName="minimumLowercaseLetters"
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumUppercaseLetters').hasError('min')"> step="1"
{{ 'admin.minimum-uppercase-letters-range' | translate }} min="0"/>
</mat-error> <mat-error
</mat-form-field> *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLowercaseLetters').hasError('min')">
<mat-form-field class="mat-block"> {{ 'admin.minimum-lowercase-letters-range' | translate }}
<mat-label translate>admin.minimum-lowercase-letters</mat-label> </mat-error>
<input matInput type="number" </mat-form-field>
formControlName="minimumLowercaseLetters" </div>
step="1" <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
min="0"/> <mat-form-field fxFlex class="mat-block">
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLowercaseLetters').hasError('min')"> <mat-label translate>admin.minimum-digits</mat-label>
{{ 'admin.minimum-lowercase-letters-range' | translate }} <input matInput type="number"
</mat-error> formControlName="minimumDigits"
</mat-form-field> step="1"
<mat-form-field class="mat-block"> min="0"/>
<mat-label translate>admin.minimum-digits</mat-label> <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumDigits').hasError('min')">
<input matInput type="number" {{ 'admin.minimum-digits-range' | translate }}
formControlName="minimumDigits" </mat-error>
step="1" </mat-form-field>
min="0"/> <mat-form-field fxFlex class="mat-block">
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumDigits').hasError('min')"> <mat-label translate>admin.minimum-special-characters</mat-label>
{{ 'admin.minimum-digits-range' | translate }} <input matInput type="number"
</mat-error> formControlName="minimumSpecialCharacters"
</mat-form-field> step="1"
<mat-form-field class="mat-block"> min="0"/>
<mat-label translate>admin.minimum-special-characters</mat-label> <mat-error
<input matInput type="number" *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumSpecialCharacters').hasError('min')">
formControlName="minimumSpecialCharacters" {{ 'admin.minimum-special-characters-range' | translate }}
step="1" </mat-error>
min="0"/> </mat-form-field>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumSpecialCharacters').hasError('min')"> </div>
{{ 'admin.minimum-special-characters-range' | translate }} <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
</mat-error> <mat-form-field fxFlex class="mat-block">
</mat-form-field> <mat-label translate>admin.password-expiration-period-days</mat-label>
<mat-form-field class="mat-block"> <input matInput type="number"
<mat-label translate>admin.password-expiration-period-days</mat-label> formControlName="passwordExpirationPeriodDays"
<input matInput type="number" step="1"
formControlName="passwordExpirationPeriodDays" min="0"/>
step="1" <mat-error
min="0"/> *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordExpirationPeriodDays').hasError('min')">
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordExpirationPeriodDays').hasError('min')"> {{ 'admin.password-expiration-period-days-range' | translate }}
{{ 'admin.password-expiration-period-days-range' | translate }} </mat-error>
</mat-error> </mat-form-field>
</mat-form-field> <mat-form-field fxFlex class="mat-block">
<mat-form-field class="mat-block"> <mat-label translate>admin.password-reuse-frequency-days</mat-label>
<mat-label translate>admin.password-reuse-frequency-days</mat-label> <input matInput type="number"
<input matInput type="number" formControlName="passwordReuseFrequencyDays"
formControlName="passwordReuseFrequencyDays" step="1"
step="1" min="0"/>
min="0"/> <mat-error
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordReuseFrequencyDays').hasError('min')"> *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordReuseFrequencyDays').hasError('min')">
{{ 'admin.password-reuse-frequency-days-range' | translate }} {{ 'admin.password-reuse-frequency-days-range' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName = "allowWhitespaces" > </div>
<mat-label translate>admin.allow-whitespace</mat-label> <mat-checkbox formControlName="allowWhitespaces" style="margin-bottom: 16px">
</mat-checkbox> <mat-label translate>admin.allow-whitespace</mat-label>
</section> </mat-checkbox>
</mat-expansion-panel> </section>
</mat-accordion>
</div>
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || securitySettingsFormGroup.invalid || !securitySettingsFormGroup.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset> </fieldset>
</form> <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px" class="layout-wrap" style="margin-top: 16px">
</mat-card-content> <button mat-button color="primary"
</mat-card> [disabled]="securitySettingsFormGroup.pristine"
</div> (click)="discardSetting()"
type="button">{{'action.undo' | translate}}
</button>
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || securitySettingsFormGroup.invalid || !securitySettingsFormGroup.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
<mat-card class="settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.jwt.security-settings</span>
</div>
</mat-card-title>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="jwtSecuritySettingsFormGroup" (ngSubmit)="saveJwtSettings()" autocomplete="off">
<fieldset [disabled]="isLoading$ | async" fxLayout="column" fxLayoutGap="8px">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.jwt.issuer-name</mat-label>
<input matInput required formControlName="tokenIssuer"/>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenIssuer').hasError('required')">
{{ 'admin.jwt.issuer-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.jwt.signings-key</mat-label>
<input matInput (focus)="markAsTouched()" required formControlName="tokenSigningKey"/>
<button type="button"
style="line-height: 32px"
matSuffix
mat-button
(click)="generateSigningKey()"
color="primary">
{{ 'admin.jwt.generate-key' | translate }}
</button>
<mat-hint translate>admin.jwt.signings-key-hint</mat-hint>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('required')">
{{ 'admin.jwt.signings-key-required' | translate }}
</mat-error>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('base64')">
{{ 'admin.jwt.signings-key-base64' | translate }}
</mat-error>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('minLength')">
{{ 'admin.jwt.signings-key-min-length' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.jwt.expiration-time</mat-label>
<input matInput type="number" required
formControlName="tokenExpirationTime"
step="1"
min="0"/>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('required')">
{{ 'admin.jwt.expiration-time-required' | translate }}
</mat-error>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('pattern')">
{{ 'admin.jwt.expiration-time-pattern' | translate }}
</mat-error>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('min')">
{{ 'admin.jwt.expiration-time-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.jwt.refresh-expiration-time</mat-label>
<input matInput type="number" required
formControlName="refreshTokenExpTime"
step="1"
min="0"/>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('required')">
{{ 'admin.jwt.refresh-expiration-time-required' | translate }}
</mat-error>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('pattern')">
{{ 'admin.jwt.refresh-expiration-time-pattern' | translate }}
</mat-error>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('min')">
{{ 'admin.jwt.refresh-expiration-time-min' | translate }}
</mat-error>
<mat-error *ngIf="jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('lessToken')">
{{ 'admin.jwt.refresh-expiration-time-less-token' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px" class="layout-wrap">
<button mat-button color="primary"
[disabled]="jwtSecuritySettingsFormGroup.pristine"
(click)="discardJwtSetting()"
type="button">{{'action.undo' | translate}}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async) || jwtSecuritySettingsFormGroup.invalid || !jwtSecuritySettingsFormGroup.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>

23
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss

@ -14,7 +14,26 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.mat-accordion-container { .mat-headline {
margin-bottom: 16px; margin-bottom: 8px;
}
.mat-card-title {
margin: 0;
}
.mat-card-content {
padding: 0 !important;
}
.fields-group {
padding: 8px 16px 0;
margin: 10px 0;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
legend {
color: rgba(0, 0, 0, .7);
}
} }
} }

142
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts

@ -14,40 +14,50 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { SecuritySettings } from '@shared/models/settings.models'; import { JwtSettings, SecuritySettings } from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service'; import { AdminService } from '@core/http/admin.service';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { mergeMap, tap } from 'rxjs/operators';
import { randomAlphanumeric } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
@Component({ @Component({
selector: 'tb-security-settings', selector: 'tb-security-settings',
templateUrl: './security-settings.component.html', templateUrl: './security-settings.component.html',
styleUrls: ['./security-settings.component.scss', './settings-card.scss'] styleUrls: ['./security-settings.component.scss', './settings-card.scss']
}) })
export class SecuritySettingsComponent extends PageComponent implements OnInit, HasConfirmForm { export class SecuritySettingsComponent extends PageComponent implements HasConfirmForm {
securitySettingsFormGroup: FormGroup; securitySettingsFormGroup: FormGroup;
securitySettings: SecuritySettings; jwtSecuritySettingsFormGroup: FormGroup;
private securitySettings: SecuritySettings;
private jwtSettings: JwtSettings;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private router: Router, private router: Router,
private adminService: AdminService, private adminService: AdminService,
public fb: FormBuilder) { private authService: AuthService,
private dialogService: DialogService,
private translate: TranslateService,
private fb: FormBuilder) {
super(store); super(store);
}
ngOnInit() {
this.buildSecuritySettingsForm(); this.buildSecuritySettingsForm();
this.buildJwtSecuritySettingsForm();
this.adminService.getSecuritySettings().subscribe( this.adminService.getSecuritySettings().subscribe(
(securitySettings) => { securitySettings => this.processSecuritySettings(securitySettings)
this.securitySettings = securitySettings; );
this.securitySettingsFormGroup.reset(this.securitySettings); this.adminService.getJwtSettings().subscribe(
} jwtSettings => this.processJwtSettings(jwtSettings)
); );
} }
@ -70,18 +80,114 @@ export class SecuritySettingsComponent extends PageComponent implements OnInit,
}); });
} }
buildJwtSecuritySettingsForm() {
this.jwtSecuritySettingsFormGroup = this.fb.group({
tokenIssuer: ['', Validators.required],
tokenSigningKey: ['', [Validators.required, this.base64Format]],
tokenExpirationTime: [0, [Validators.required, Validators.pattern('[0-9]*'), Validators.min(60)]],
refreshTokenExpTime: [0, [Validators.required, Validators.pattern('[0-9]*'), Validators.min(900)]]
}, {validators: this.refreshTokenTimeGreatTokenTime.bind(this)});
this.jwtSecuritySettingsFormGroup.get('tokenExpirationTime').valueChanges.subscribe(
() => this.jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').updateValueAndValidity({onlySelf: true})
);
}
save(): void { save(): void {
this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value}; this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value};
this.adminService.saveSecuritySettings(this.securitySettings).subscribe( this.adminService.saveSecuritySettings(this.securitySettings).subscribe(
(securitySettings) => { securitySettings => this.processSecuritySettings(securitySettings)
this.securitySettings = securitySettings;
this.securitySettingsFormGroup.reset(this.securitySettings);
}
); );
} }
saveJwtSettings() {
const jwtFormSettings = this.jwtSecuritySettingsFormGroup.value;
this.confirmChangeJWTSettings().pipe(mergeMap(value => {
if (value) {
return this.adminService.saveJwtSettings(jwtFormSettings).pipe(
tap((data) => this.authService.setUserFromJwtToken(data.token, data.refreshToken, false)),
mergeMap(() => this.adminService.getJwtSettings()),
tap(jwtSettings => this.processJwtSettings(jwtSettings))
);
}
return of(null);
})).subscribe(() => {});
}
discardSetting() {
this.securitySettingsFormGroup.reset(this.securitySettings);
}
discardJwtSetting() {
this.jwtSecuritySettingsFormGroup.reset(this.jwtSettings);
}
markAsTouched() {
this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsTouched();
}
private confirmChangeJWTSettings(): Observable<boolean> {
if (this.jwtSecuritySettingsFormGroup.get('tokenIssuer').value !== (this.jwtSettings?.tokenIssuer || '') ||
this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').value !== (this.jwtSettings?.tokenSigningKey || '')) {
return this.dialogService.confirm(
this.translate.instant('admin.jwt.info-header'),
`<div style="max-width: 640px">${this.translate.instant('admin.jwt.info-message')}</div>`,
this.translate.instant('action.discard-changes'),
this.translate.instant('action.confirm')
);
}
return of(true);
}
generateSigningKey() {
this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').setValue(btoa(randomAlphanumeric(64)));
if (this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').pristine) {
this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsDirty();
this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsTouched();
}
}
private processSecuritySettings(securitySettings: SecuritySettings) {
this.securitySettings = securitySettings;
this.securitySettingsFormGroup.reset(this.securitySettings);
}
private processJwtSettings(jwtSettings: JwtSettings) {
this.jwtSettings = jwtSettings;
this.jwtSecuritySettingsFormGroup.reset(jwtSettings);
}
private refreshTokenTimeGreatTokenTime(formGroup: FormGroup): { [key: string]: boolean } | null {
if (formGroup) {
const tokenTime = formGroup.value.tokenExpirationTime;
const refreshTokenTime = formGroup.value.refreshTokenExpTime;
if (tokenTime >= refreshTokenTime ) {
if (formGroup.get('refreshTokenExpTime').untouched) {
formGroup.get('refreshTokenExpTime').markAsTouched();
}
formGroup.get('refreshTokenExpTime').setErrors({lessToken: true});
return {lessToken: true};
}
}
return null;
}
private base64Format(control: FormControl): { [key: string]: boolean } | null {
if (control.value === '' || control.value === 'thingsboardDefaultSigningKey') {
return null;
}
try {
const value = atob(control.value);
if (value.length < 32) {
return {minLength: true};
}
return null;
} catch (e) {
return {base64: true};
}
}
confirmForm(): FormGroup { confirmForm(): FormGroup {
return this.securitySettingsFormGroup; return this.securitySettingsFormGroup.dirty ? this.securitySettingsFormGroup : this.jwtSecuritySettingsFormGroup;
} }
} }

2
ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html

@ -16,7 +16,7 @@
--> -->
<h2 mat-dialog-title>{{data.title}}</h2> <h2 mat-dialog-title>{{data.title}}</h2>
<div mat-dialog-content [innerHTML]="data.message"></div> <div mat-dialog-content [innerHTML]="data.message | safe: 'html'"></div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary" [mat-dialog-close]="false">{{data.cancel}}</button> <button mat-button color="primary" [mat-dialog-close]="false">{{data.cancel}}</button>
<button mat-button color="primary" [mat-dialog-close]="true" cdkFocusInitial>{{data.ok}}</button> <button mat-button color="primary" [mat-dialog-close]="true" cdkFocusInitial>{{data.ok}}</button>

7
ui-ngx/src/app/shared/models/settings.models.ts

@ -63,6 +63,13 @@ export interface SecuritySettings {
passwordPolicy: UserPasswordPolicy; passwordPolicy: UserPasswordPolicy;
} }
export interface JwtSettings {
tokenIssuer: string;
tokenSigningKey: string;
tokenExpirationTime: number;
refreshTokenExpTime: number;
}
export interface UpdateMessage { export interface UpdateMessage {
message: string; message: string;
updateAvailable: boolean; updateAvailable: boolean;

22
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -381,6 +381,28 @@
"within-time": "Within time (sec)", "within-time": "Within time (sec)",
"within-time-pattern": "Time must be a positive integer.", "within-time-pattern": "Time must be a positive integer.",
"within-time-required": "Time is required." "within-time-required": "Time is required."
},
"jwt": {
"security-settings": "JWT security settings",
"issuer-name": "Issuer name",
"issuer-name-required": "Issuer name is required.",
"signings-key": "Signing key",
"signings-key-hint": "Base64 encoded string representing at least 256 bits of data.",
"signings-key-required": "Signing key is required.",
"signings-key-min-length": "Signing key must be at least 256 bits of data.",
"signings-key-base64": "Signing key must be base64 format.",
"expiration-time": "Token expiration time (sec)",
"expiration-time-required": "Token expiration time is required.",
"expiration-time-pattern": "Token expiration time be a positive integer.",
"expiration-time-min": "Minimum time is 60 seconds (1 minute).",
"refresh-expiration-time": "Refresh token expiration time (sec)",
"refresh-expiration-time-required": "Refresh token expiration time is required.",
"refresh-expiration-time-pattern": "Refresh token expiration time be a positive integer.",
"refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).",
"refresh-expiration-time-less-token": "Refresh token time must be greater token time.",
"generate-key": "Generate key",
"info-header": "All users will be to re-logined",
"info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets."
} }
}, },
"alarm": { "alarm": {

Loading…
Cancel
Save