Browse Source

Merge branch 'master' into lwm2m_default_object_id_version_front

pull/10716/head
nick 2 years ago
parent
commit
64c847930e
  1. 2
      application/src/main/data/json/system/widget_types/asset_admin_table.json
  2. 2
      application/src/main/data/json/system/widget_types/device_admin_table.json
  3. 2
      application/src/main/data/json/system/widget_types/entities_hierarchy.json
  4. 2
      application/src/main/data/json/system/widget_types/entities_table.json
  5. 2
      application/src/main/data/json/system/widget_types/timeseries_table.json
  6. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  7. 135
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  8. 10
      application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java
  9. 3
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  11. 43
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  12. 37
      application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java
  13. 1
      application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java
  14. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  15. 1
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  17. 80
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java
  18. 7
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
  19. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java
  20. 72
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  21. 6
      application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java
  22. 2
      application/src/main/resources/thingsboard.yml
  23. 17
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  24. 4
      application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java
  25. 2
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
  26. 35
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  27. 11
      common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java
  28. 2
      common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java
  29. 30
      common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java
  30. 3
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java
  32. 7
      common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java
  33. 9
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  34. 43
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
  35. 12
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  36. 8
      dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
  37. 40
      dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
  38. 8
      dao/src/test/resources/sql/system-data.sql
  39. 48
      pom.xml
  40. 7
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts
  41. 7
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  42. 7
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  43. 4
      ui-ngx/src/assets/locale/locale.constant-ar_AE.json
  44. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  45. 4
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  46. 4
      ui-ngx/src/assets/locale/locale.constant-nl_BE.json
  47. 4
      ui-ngx/src/assets/locale/locale.constant-pl_PL.json
  48. 4
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json

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

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",

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

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",

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

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-entities-hierarchy-widget \n [ctx]=\"ctx\">\n</tb-entities-hierarchy-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-hierarchy-widget-settings",

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

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",

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

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onLatestDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.timeseriesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"latestDataKeySettingsSchema": "",

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

@ -136,6 +136,7 @@ public class ThingsboardInstallService {
dataUpdateService.updateData("3.6.4");
entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists();
systemDataLoaderService.updateDefaultNotificationConfigs(false);
systemDataLoaderService.updateJwtSettings();
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:

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

@ -19,13 +19,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@ -81,6 +85,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
@ -100,14 +105,11 @@ import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
@ -117,79 +119,51 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE;
import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault;
import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateTokenSigningKeyLength;
@Service
@Profile("install")
@Slf4j
@RequiredArgsConstructor
public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
public static final String CUSTOMER_CRED = "customer";
public static final String ACTIVITY_STATE = "active";
@Autowired
private InstallScripts installScripts;
private final InstallScripts installScripts;
private final UserService userService;
private final AdminSettingsService adminSettingsService;
private final TenantService tenantService;
private final TenantProfileService tenantProfileService;
private final CustomerService customerService;
private final DeviceService deviceService;
private final DeviceProfileService deviceProfileService;
private final AttributesService attributesService;
private final DeviceCredentialsService deviceCredentialsService;
private final RuleChainService ruleChainService;
private final TimeseriesService tsService;
private final DeviceConnectivityConfiguration connectivityConfiguration;
private final QueueService queueService;
private final JwtSettingsService jwtSettingsService;
private final NotificationSettingsService notificationSettingsService;
private final NotificationTargetService notificationTargetService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private WidgetTypeService widgetTypeService;
@Autowired
private WidgetsBundleService widgetsBundleService;
@Autowired
private TenantService tenantService;
@Autowired
private TenantProfileService tenantProfileService;
@Autowired
private CustomerService customerService;
@Autowired
private DeviceService deviceService;
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private AttributesService attributesService;
@Autowired
private DeviceCredentialsService deviceCredentialsService;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private TimeseriesService tsService;
@Autowired
private DeviceConnectivityConfiguration connectivityConfiguration;
@Value("${state.persistToTelemetry:false}")
@Getter
private boolean persistActivityToTelemetry;
@Lazy
@Autowired
private QueueService queueService;
@Autowired
private JwtSettingsService jwtSettingsService;
@Autowired
private NotificationSettingsService notificationSettingsService;
@Autowired
private NotificationTargetService notificationTargetService;
@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;
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
@ -295,7 +269,42 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Override
public void createRandomJwtSettings() throws Exception {
jwtSettingsService.createRandomJwtSettings();
if (jwtSettingsService.getJwtSettings() == null) {
log.info("Creating JWT admin settings...");
var jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey);
if (isSigningKeyDefault(jwtSettings) || !validateTokenSigningKeyLength(jwtSettings)) {
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
}
jwtSettingsService.saveJwtSettings(jwtSettings);
} else {
log.info("Skip creating JWT admin settings because they already exist.");
}
}
@Override
public void updateJwtSettings() {
JwtSettings jwtSettings = jwtSettingsService.getJwtSettings();
boolean invalidSignKey = false;
String warningMessage = null;
if (isSigningKeyDefault(jwtSettings)) {
warningMessage = "The platform is using the default JWT Signing Key, which is a security risk.";
invalidSignKey = true;
} else if (!validateTokenSigningKeyLength(jwtSettings)) {
warningMessage = "The JWT Signing Key is shorter than 512 bits, which is a security risk.";
invalidSignKey = true;
}
if (invalidSignKey) {
log.warn("WARNING: {}. A new JWT Signing Key has been added automatically. " +
"You can change the JWT Signing Key using the Web UI: " +
"Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage);
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
jwtSettingsService.saveJwtSettings(jwtSettings);
}
}
@Override

10
application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java

@ -84,13 +84,17 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
}
protected void executeQuery(String query) {
executeQuery(query, null);
}
protected void executeQuery(String query, String logQuery) {
logQuery = logQuery != null ? logQuery : query;
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
conn.createStatement().execute(query); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
log.info("Successfully executed query: {}", query);
log.info("Successfully executed query: {}", logQuery);
Thread.sleep(5000);
} catch (InterruptedException | SQLException e) {
log.error("Failed to execute query: {} due to: {}", query, e.getMessage());
throw new RuntimeException("Failed to execute query: " + query, e);
throw new RuntimeException("Failed to execute query: " + logQuery, e);
}
}

3
application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java

@ -56,6 +56,7 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
@Override
public void createCustomerTitleUniqueConstraintIfNotExists() {
executeQuery("DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'customer_title_unq_key') THEN " +
"ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;");
"ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;",
"create 'customer_title_unq_key' constraint if it doesn't already exist!");
}
}

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

@ -25,6 +25,8 @@ public interface SystemDataLoaderService {
void createRandomJwtSettings() throws Exception;
void updateJwtSettings() throws Exception;
void createOAuth2Templates() throws Exception;
void loadSystemWidgets() throws Exception;

43
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -16,12 +16,13 @@
package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import freemarker.template.Configuration;
import freemarker.template.Template;
import jakarta.xml.bind.DatatypeConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.NestedRuntimeException;
@ -31,29 +32,36 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.TbEmail;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiFeature;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageRecordState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.RateLimitExceededException;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.mail.internet.MimeMessage;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -76,13 +84,21 @@ public class DefaultMailService implements MailService {
private TbApiUsageStateService apiUsageStateService;
@Autowired
private MailExecutorService mailExecutorService;
private MailSenderInternalExecutorService mailExecutorService;
@Autowired
private PasswordResetExecutorService passwordResetExecutorService;
@Autowired
private TbMailContextComponent tbMailContextComponent;
private TbMailContextComponent ctx;
@Autowired
private RateLimitService rateLimitService;
@Value("${mail.per_tenant_rate_limits:}")
private String perTenantRateLimitConfig;
private final ScheduledExecutorService timeoutScheduler;
private TbMailSender mailSender;
@ -95,6 +111,7 @@ public class DefaultMailService implements MailService {
this.freemarkerConfig = freemarkerConfig;
this.adminSettingsService = adminSettingsService;
this.apiUsageClient = apiUsageClient;
this.timeoutScheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog"));
}
@PostConstruct
@ -102,12 +119,19 @@ public class DefaultMailService implements MailService {
updateMailConfiguration();
}
@PreDestroy
public void destroy() {
if (timeoutScheduler != null) {
timeoutScheduler.shutdownNow();
}
}
@Override
public void updateMailConfiguration() {
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (settings != null) {
JsonNode jsonConfig = settings.getJsonValue();
mailSender = new TbMailSender(tbMailContextComponent, jsonConfig);
mailSender = new TbMailSender(ctx, jsonConfig);
mailFrom = jsonConfig.get("mailFrom").asText();
timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT);
} else {
@ -122,7 +146,7 @@ public class DefaultMailService implements MailService {
@Override
public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
TbMailSender testMailSender = new TbMailSender(tbMailContextComponent, jsonConfig);
TbMailSender testMailSender = new TbMailSender(ctx, jsonConfig);
String mailFrom = jsonConfig.get("mailFrom").asText();
String subject = messages.getMessage("test.message.subject", null, Locale.US);
long timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT);
@ -214,6 +238,10 @@ public class DefaultMailService implements MailService {
private void sendMail(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender, long timeout) throws ThingsboardException {
if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) {
if (tenantId != null && !tenantId.isSysTenantId() && StringUtils.isNotEmpty(perTenantRateLimitConfig) &&
!rateLimitService.checkRateLimit(LimitedApi.EMAILS, (Object) tenantId, perTenantRateLimitConfig)) {
throw new RateLimitExceededException(LimitedApi.EMAILS);
}
try {
MimeMessage mailMsg = javaMailSender.createMimeMessage();
boolean multipart = (tbEmail.getImages() != null && !tbEmail.getImages().isEmpty());
@ -415,8 +443,11 @@ public class DefaultMailService implements MailService {
}
private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) {
var submittedMail = Futures.withTimeout(
mailExecutorService.submit(() -> mailSender.send(msg)),
timeout, TimeUnit.MILLISECONDS, timeoutScheduler);
try {
mailExecutorService.submit(() -> mailSender.send(msg)).get(timeout, TimeUnit.MILLISECONDS);
submittedMail.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.debug("Error during mail submission", e);
throw new RuntimeException("Timeout!");

37
application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2024 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.mail;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
/**
* Executor have the sole purpose to send mails. It should be used only by Mail Service.
* For other purposes please use the MailExecutorService component
* */
@Component
public class MailSenderInternalExecutorService extends AbstractListeningExecutor {
@Value("${actors.rule.mail_thread_pool_size}")
private int mailExecutorThreadPoolSize;
@Override
protected int getThreadPollSize() {
return mailExecutorThreadPoolSize;
}
}

1
application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java

@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@Data

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

@ -175,7 +175,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
NotificationSchedulerService notificationSchedulerService,
NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService,
eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();

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

@ -56,6 +56,7 @@ import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

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

@ -48,6 +48,8 @@ import org.thingsboard.server.service.queue.TbPackProcessingContext;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import jakarta.annotation.PreDestroy;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

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

@ -17,25 +17,24 @@ 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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter;
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.notification.targets.platform.SystemAdministratorsFilter;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
import static org.thingsboard.server.service.security.model.token.JwtTokenFactory.KEY_LENGTH;
@Service
@RequiredArgsConstructor
@Slf4j
@ -43,49 +42,11 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
private final AdminSettingsService adminSettingsService;
private final Optional<TbClusterService> tbClusterService;
private final Optional<NotificationCenter> notificationCenter;
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 final Optional<JwtTokenFactory> jwtTokenFactory;
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);
@ -105,7 +66,9 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
@Override
public JwtSettings reloadJwtSettings() {
log.trace("Executing reloadJwtSettings");
return getJwtSettings(true);
var settings = getJwtSettings(true);
jwtTokenFactory.ifPresent(JwtTokenFactory::reload);
return settings;
}
@Override
@ -118,30 +81,13 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
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.");
notificationCenter.ifPresent(notificationCenter -> {
notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(), DefaultNotifications.jwtSigningKeyIssue.toTemplate());
});
}
this.jwtSettings = result;
jwtSettings = getJwtSettingsFromDb();
}
}
}
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;
@ -161,8 +107,12 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
return adminJwtSettings;
}
private boolean isSigningKeyDefault(JwtSettings settings) {
public static boolean isSigningKeyDefault(JwtSettings settings) {
return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey());
}
public static boolean validateTokenSigningKeyLength(JwtSettings settings) {
return Base64.getDecoder().decode(settings.getTokenSigningKey()).length * Byte.SIZE >= KEY_LENGTH;
}
}

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

@ -27,6 +27,9 @@ import java.util.Base64;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault;
import static org.thingsboard.server.service.security.model.token.JwtTokenFactory.KEY_LENGTH;
@Component
@RequiredArgsConstructor
public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
@ -59,8 +62,8 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
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!");
if (decodedKey.length * Byte.SIZE < KEY_LENGTH && !isSigningKeyDefault(jwtSettings)) {
throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 512 bits of data!");
}
System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory

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

@ -26,10 +26,6 @@ public interface JwtSettingsService {
JwtSettings reloadJwtSettings();
void createRandomJwtSettings();
void saveLegacyYmlSettings();
JwtSettings saveJwtSettings(JwtSettings jwtSettings);
}

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

@ -16,16 +16,19 @@
package org.thingsboard.server.service.security.model.token;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ClaimsBuilder;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
@ -41,7 +44,10 @@ import org.thingsboard.server.service.security.exception.JwtExpiredTokenExceptio
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@ -53,6 +59,8 @@ import java.util.stream.Collectors;
@Slf4j
public class JwtTokenFactory {
public static int KEY_LENGTH = Jwts.SIG.HS512.getKeyBitLength();
private static final String SCOPES = "scopes";
private static final String USER_ID = "userId";
private static final String FIRST_NAME = "firstName";
@ -63,8 +71,12 @@ public class JwtTokenFactory {
private static final String CUSTOMER_ID = "customerId";
private static final String SESSION_ID = "sessionId";
@Lazy
private final JwtSettingsService jwtSettingsService;
private volatile JwtParser jwtParser;
private volatile SecretKey secretKey;
/**
* Factory method for issuing new JWT Tokens.
*/
@ -95,7 +107,7 @@ public class JwtTokenFactory {
public SecurityUser parseAccessJwtToken(String token) {
Jws<Claims> jwsClaims = parseTokenClaims(token);
Claims claims = jwsClaims.getBody();
Claims claims = jwsClaims.getPayload();
String subject = claims.getSubject();
@SuppressWarnings("unchecked")
List<String> scopes = claims.get(SCOPES, List.class);
@ -140,14 +152,14 @@ public class JwtTokenFactory {
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), jwtSettingsService.getJwtSettings().getRefreshTokenExpTime())
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID)
.setId(UUID.randomUUID().toString()).compact();
.id(UUID.randomUUID().toString()).compact();
return new AccessJwtToken(token);
}
public SecurityUser parseRefreshToken(String token) {
Jws<Claims> jwsClaims = parseTokenClaims(token);
Claims claims = jwsClaims.getBody();
Claims claims = jwsClaims.getPayload();
String subject = claims.getSubject();
@SuppressWarnings("unchecked")
List<String> scopes = claims.get(SCOPES, List.class);
@ -176,6 +188,11 @@ public class JwtTokenFactory {
return new AccessJwtToken(jwtBuilder.compact());
}
public void reload() {
getSecretKey(true);
getJwtParser(true);
}
private JwtBuilder setUpToken(SecurityUser securityUser, List<String> scopes, long expirationTime) {
if (StringUtils.isBlank(securityUser.getEmail())) {
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
@ -183,28 +200,27 @@ public class JwtTokenFactory {
UserPrincipal principal = securityUser.getUserPrincipal();
Claims claims = Jwts.claims().setSubject(principal.getValue());
claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(SCOPES, scopes);
ClaimsBuilder claimsBuilder = Jwts.claims()
.subject(principal.getValue())
.add(USER_ID, securityUser.getId().getId().toString())
.add(SCOPES, scopes);
if (securityUser.getSessionId() != null) {
claims.put(SESSION_ID, securityUser.getSessionId());
claimsBuilder.add(SESSION_ID, securityUser.getSessionId());
}
ZonedDateTime currentTime = ZonedDateTime.now();
return Jwts.builder()
.setClaims(claims)
.setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
.setIssuedAt(Date.from(currentTime.toInstant()))
.setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey());
.claims(claimsBuilder.build())
.issuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
.issuedAt(Date.from(currentTime.toInstant()))
.expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(getSecretKey(false), Jwts.SIG.HS512);
}
public Jws<Claims> parseTokenClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey())
.parseClaimsJws(token);
return getJwtParser(false).parseSignedClaims(token);
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) {
log.debug("Invalid JWT Token", ex);
throw new BadCredentialsException("Invalid JWT token: ", ex);
@ -220,4 +236,28 @@ public class JwtTokenFactory {
return new JwtPair(accessToken.getToken(), refreshToken.getToken());
}
private SecretKey getSecretKey(boolean forceReload) {
if (secretKey == null || forceReload) {
synchronized (this) {
if (secretKey == null || forceReload) {
byte[] decodedToken = Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey());
secretKey = new SecretKeySpec(decodedToken, "HmacSHA512");
}
}
}
return secretKey;
}
private JwtParser getJwtParser(boolean forceReload) {
if (jwtParser == null || forceReload) {
synchronized (this) {
if (jwtParser == null || forceReload) {
jwtParser = Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey())))
.build();
}
}
}
return jwtParser;
}
}

6
application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java

@ -22,10 +22,12 @@ import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@ -40,14 +42,14 @@ public class OAuth2AppTokenFactory {
public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) {
Jws<Claims> jwsClaims;
try {
jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken);
jwsClaims = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(appSecret))).build().parseSignedClaims(appToken);
}
catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
throw new IllegalArgumentException("Invalid Application token: ", ex);
} catch (ExpiredJwtException expiredEx) {
throw new IllegalArgumentException("Application token expired", expiredEx);
}
Claims claims = jwsClaims.getBody();
Claims claims = jwsClaims.getPayload();
Date expiration = claims.getExpiration();
if (expiration == null) {
throw new IllegalArgumentException("Application token must have expiration date");

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

@ -164,6 +164,8 @@ mail:
oauth2:
# Interval for checking refresh token expiration in seconds(by default, 1 day).
refreshTokenCheckingInterval: "${REFRESH_TOKEN_EXPIRATION_CHECKING_INTERVAL:86400}"
# Rate limits for sending mails per tenant. As example for 1000 per minute and 10000 per hour is "1000:60,10000:3600"
per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}"
# Usage statistics parameters
usage:

17
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -21,9 +21,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jws;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
@ -121,6 +119,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
@ -241,6 +240,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired
protected ClaimDevicesService claimDevicesService;
@Autowired
private JwtTokenFactory jwtTokenFactory;
@SpyBean
protected MailService mailService;
@ -561,13 +563,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
protected void validateJwtToken(String token, String username) {
Assert.assertNotNull(token);
Assert.assertFalse(token.isEmpty());
int i = token.lastIndexOf('.');
Assert.assertTrue(i > 0);
String withoutSignature = token.substring(0, i + 1);
Jwt<Header, Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature);
Claims claims = jwsClaims.getBody();
Jws<Claims> jwsClaims = jwtTokenFactory.parseTokenClaims(token);
Claims claims = jwsClaims.getPayload();
String subject = claims.getSubject();
Assert.assertEquals(username, subject);
}

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

@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Slf4j
@DaoSqlTest
public class AdminControllerTest extends AbstractControllerTest {
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey");
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "QmlicmJkZk9tSzZPVFozcWY0Sm94UVhybmtBWXZ5YmZMOUZSZzZvcUFiOVhsb3VHUThhUWJGaXp3UHhtcGZ6Tw==");
@Test
public void testFindAdminSettingsByKey() throws Exception {
@ -168,7 +168,7 @@ public class AdminControllerTest extends AbstractControllerTest {
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8)));
RandomStringUtils.randomAlphanumeric(512 / Byte.SIZE).getBytes(StandardCharsets.UTF_8)));
doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());

2
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java

@ -640,7 +640,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
}
private void verifyMsgProcessed(TbMsg tbMsg) {
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
await().atMost(15, TimeUnit.SECONDS).untilAsserted(() -> {
verify(actorContext, atLeastOnce()).tell(argThat(msg -> {
return ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId());
}));

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

@ -16,16 +16,14 @@
package org.thingsboard.server.service.security.auth;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.common.data.security.model.JwtToken;
@ -38,6 +36,8 @@ 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.JwtTokenFactory;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.Optional;
@ -45,19 +45,13 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JwtTokenFactoryTest {
private JwtTokenFactory tokenFactory;
private AdminSettingsService adminSettingsService;
private NotificationCenter notificationCenter;
private JwtSettingsService jwtSettingsService;
private JwtSettings jwtSettings;
@ -66,12 +60,11 @@ public class JwtTokenFactoryTest {
public void beforeEach() {
jwtSettings = new JwtSettings();
jwtSettings.setTokenIssuer("tb");
jwtSettings.setTokenSigningKey("abewafaf");
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2));
jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7));
adminSettingsService = mock(AdminSettingsService.class);
notificationCenter = mock(NotificationCenter.class);
jwtSettingsService = mockJwtSettingsService();
mockJwtSettings(jwtSettings);
@ -169,21 +162,6 @@ public class JwtTokenFactoryTest {
});
}
@Test
public void testJwtSigningKeyIssueNotification() {
JwtSettings badJwtSettings = jwtSettings;
badJwtSettings.setTokenSigningKey(JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT);
mockJwtSettings(badJwtSettings);
jwtSettingsService = mockJwtSettingsService();
for (int i = 0; i < 5; i++) { // to check if notification is not sent twice
jwtSettingsService.getJwtSettings();
}
verify(notificationCenter, times(1)).sendGeneralWebNotification(eq(TenantId.SYS_TENANT_ID),
isA(SystemAdministratorsFilter.class), argThat(template -> template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.WEB)
.getBody().contains("The platform is configured to use default JWT Signing Key")));
}
private void mockJwtSettings(JwtSettings settings) {
AdminSettings adminJwtSettings = new AdminSettings();
adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(settings));
@ -192,12 +170,11 @@ public class JwtTokenFactoryTest {
}
private DefaultJwtSettingsService mockJwtSettingsService() {
return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(),
Optional.of(notificationCenter), new DefaultJwtSettingsValidator());
return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), Optional.empty());
}
private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) {
Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getBody();
Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getPayload();
assertThat(claims.getExpiration()).matches(actualExpirationTime -> {
Calendar expirationTime = Calendar.getInstance();
expirationTime.setTime(new Date());

11
common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java

@ -63,12 +63,21 @@ public class DefaultRateLimitService implements RateLimitService {
@Override
public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level) {
return checkRateLimit(api, tenantId, level, false);
}
@Override
public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level, boolean ignoreTenantNotFound) {
if (tenantId.isSysTenantId()) {
return true;
}
TenantProfile tenantProfile = tenantProfileProvider.get(tenantId);
if (tenantProfile == null) {
throw new TenantProfileNotFoundException(tenantId);
if (ignoreTenantNotFound) {
return true;
} else {
throw new TenantProfileNotFoundException(tenantId);
}
}
String rateLimitConfig = tenantProfile.getProfileConfiguration()

2
common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java

@ -24,6 +24,8 @@ public interface RateLimitService {
boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level);
boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level, boolean ignoreTenantNotFound);
boolean checkRateLimit(LimitedApi api, Object level, String rateLimitConfig);
void cleanUp(LimitedApi api, Object level);

30
common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.exception;
import org.thingsboard.server.common.data.limit.LimitedApi;
public class RateLimitExceededException extends AbstractRateLimitException {
public RateLimitExceededException(String message) {
super(message);
}
public RateLimitExceededException(LimitedApi api) {
super("Rate limit for " + api.getLabel() + " is exceeded");
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -39,7 +39,8 @@ public enum LimitedApi {
TWO_FA_VERIFICATION_CODE_SEND(false, true),
TWO_FA_VERIFICATION_CODE_CHECK(false, true),
TRANSPORT_MESSAGES_PER_TENANT("transport messages", true),
TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false);
TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false),
EMAILS("emails sending", true);
private Function<DefaultTenantProfileConfiguration, String> configExtractor;
@Getter

2
common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java

@ -48,7 +48,7 @@ public class JwtSettings {
* Key is used to sign {@link JwtToken}.
* Base64 encoded
*/
@Schema(description = "The JWT key is used to sing token. Base64 encoded.", example = "cTU4WnNqemI2aU5wbWVjdm1vYXRzanhjNHRUcXliMjE=")
@Schema(description = "The JWT key is used to sing token. Base64 encoded.", example = "dkVTUzU2M2VMWUNwVVltTUhQU2o5SUM0Tkc3M0k2Ykdwcm85QTl6R0RaQ252OFlmVDk2OEptZXBNcndGeExFZg==")
private String tokenSigningKey;
}

7
common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java

@ -29,9 +29,8 @@ import org.thingsboard.server.common.data.notification.rule.trigger.Notification
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.queue.util.PropertyUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.SOFT;
@ -41,7 +40,7 @@ import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.
@Slf4j
public class DefaultNotificationDeduplicationService implements NotificationDeduplicationService {
private Map<NotificationRuleTriggerType, Long> deduplicationDurations;
private ConcurrentMap<NotificationRuleTriggerType, Long> deduplicationDurations;
@Autowired(required = false)
private CacheManager cacheManager;
@ -116,7 +115,7 @@ public class DefaultNotificationDeduplicationService implements NotificationDedu
@Autowired
public void setDeduplicationDurations(@Value("${notification_system.rules.deduplication_durations:}")
String deduplicationDurationsStr) {
this.deduplicationDurations = new HashMap<>();
this.deduplicationDurations = new ConcurrentHashMap<>();
PropertyUtils.getProps(deduplicationDurationsStr).forEach((triggerType, duration) -> {
this.deduplicationDurations.put(NotificationRuleTriggerType.valueOf(triggerType), Long.parseLong(duration));
});

9
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java

@ -372,15 +372,6 @@ public class DefaultNotifications {
.build())
.build();
public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder()
.name("JWT Signing Key issue notification")
.type(NotificationType.GENERAL)
.subject("WARNING: security issue")
.text("The platform is configured to use default JWT Signing Key. Please change it on the security settings page")
.icon("warning").color(YELLOW_COLOR)
.button("Go to settings").link("/security-settings/general")
.build();
private final NotificationTemplateService templateService;
private final NotificationRuleService ruleService;

43
dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java

@ -41,7 +41,9 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
AssetInfoEntity findAssetInfoById(@Param("assetId") UUID assetId);
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)")
Page<AssetEntity> findByTenantId(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@ -54,14 +56,16 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(p.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ")
" OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true)")
Page<AssetInfoEntity> findAssetInfosByTenantId(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)")
Page<AssetEntity> findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("textSearch") String textSearch,
@ -69,7 +73,8 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.assetProfileId = :profileId " +
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)")
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :searchText, '%')) = true)")
Page<AssetEntity> findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId,
@Param("profileId") UUID profileId,
@Param("searchText") String searchText,
@ -81,7 +86,10 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
"LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " +
"WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)")
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :searchText, '%')) = true " +
" OR ilike(c.title, CONCAT('%', :searchText, '%')) = true " +
" OR ilike(p.name, CONCAT('%', :searchText, '%')) = true) ")
Page<AssetInfoEntity> findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("searchText") String searchText,
@ -95,7 +103,8 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.type = :type " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true)")
Page<AssetEntity> findByTenantIdAndType(@Param("tenantId") UUID tenantId,
@Param("type") String type,
@Param("textSearch") String textSearch,
@ -123,7 +132,8 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
"AND a.assetProfileId = :assetProfileId " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ")
" OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true) ")
Page<AssetInfoEntity> findAssetInfosByTenantIdAndAssetProfileId(@Param("tenantId") UUID tenantId,
@Param("assetProfileId") UUID assetProfileId,
@Param("textSearch") String textSearch,
@ -132,7 +142,8 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId AND a.type = :type " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ")
Page<AssetEntity> findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("type") String type,
@ -146,7 +157,9 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
"WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
"AND a.type = :type " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ")
Page<AssetInfoEntity> findAssetInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("type") String type,
@ -160,7 +173,10 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
"WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
"AND a.assetProfileId = :assetProfileId " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true) ")
Page<AssetInfoEntity> findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("assetProfileId") UUID assetProfileId,
@ -172,7 +188,9 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
@Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " +
"AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " +
"AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " +
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)")
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :searchText, '%')) = true " +
" OR ilike(a.type, CONCAT('%', :searchText, '%')) = true) ")
Page<AssetEntity> findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId,
@Param("edgeId") UUID edgeId,
@Param("searchText") String searchText,
@ -182,7 +200,8 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
"AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " +
"AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " +
"AND a.type = :type " +
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)")
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ")
Page<AssetEntity> findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId,
@Param("edgeId") UUID edgeId,
@Param("type") String type,

12
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -136,18 +136,20 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
TenantId tenantId = savedTenant.getId();
publishEvictEvent(new TenantEvictEvent(tenantId, create));
if (create && defaultEntitiesCreator != null) {
defaultEntitiesCreator.accept(tenantId);
}
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(tenantId).entity(savedTenant).created(create).build());
if (create) {
deviceProfileService.createDefaultDeviceProfile(tenantId);
assetProfileService.createDefaultAssetProfile(tenantId);
apiUsageStateService.createDefaultApiUsageState(tenantId, null);
notificationSettingsService.createDefaultNotificationConfigs(tenantId);
if (defaultEntitiesCreator != null) {
defaultEntitiesCreator.accept(tenantId);
}
}
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(tenantId).entity(savedTenant).created(create).build());
return savedTenant;
}

8
dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java

@ -27,20 +27,20 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.cache.limits.RateLimitService;
import jakarta.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -114,7 +114,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
boolean perTenantLimitReached = false;
TenantId tenantId = task.getTenantId();
if (tenantId != null && !tenantId.isSysTenantId()) {
if (!rateLimitService.checkRateLimit(LimitedApi.CASSANDRA_QUERIES, tenantId)) {
if (!rateLimitService.checkRateLimit(LimitedApi.CASSANDRA_QUERIES, tenantId, tenantId, true)) {
stats.incrementRateLimitedTenant(tenantId);
stats.getTotalRateLimited().increment();
settableFuture.setException(new TenantRateLimitException());

40
dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java

@ -78,7 +78,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
UUID assetId = Uuids.timeBased();
UUID tenantId = i % 2 == 0 ? tenantId1 : tenantId2;
UUID customerId = i % 2 == 0 ? customerId1 : customerId2;
assets.add(saveAsset(assetId, tenantId, customerId, "ASSET_" + i));
assets.add(saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "label_" + i));
}
assertEquals(assets.size(), assetDao.find(TenantId.fromUUID(tenantId1)).size());
}
@ -131,6 +131,21 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
assertEquals(0, assets3.getData().size());
}
@Test
public void testFindAssetsByTenantIdAndCustomerIdByLabel() {
PageLink pageLink = new PageLink(20, 0, "label_");
PageData<Asset> assets1 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink);
assertEquals(20, assets1.getData().size());
pageLink = pageLink.nextPageLink();
PageData<Asset> assets2 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink);
assertEquals(10, assets2.getData().size());
pageLink = pageLink.nextPageLink();
PageData<Asset> assets3 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink);
assertEquals(0, assets3.getData().size());
}
@Test
public void testFindAssetsByTenantIdAndIdsAsync() throws ExecutionException, InterruptedException, TimeoutException {
List<UUID> searchIds = getAssetsUuids(tenantId1);
@ -180,21 +195,31 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
@Test
public void testFindAssetsByTenantIdAndType() {
String type = "TYPE_2";
assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type));
String testLabel = "test_label";
assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type, testLabel));
List<Asset> foundedAssetsByType = assetDao
.findAssetsByTenantIdAndType(tenantId2, type, new PageLink(3)).getData();
compareFoundedAssetByType(foundedAssetsByType, type);
List<Asset> foundedAssetsByTypeAndLabel = assetDao
.findAssetsByTenantIdAndType(tenantId2, type, new PageLink(3, 0, testLabel)).getData();
assertEquals(1, foundedAssetsByTypeAndLabel.size());
}
@Test
public void testFindAssetsByTenantIdAndCustomerIdAndType() {
String type = "TYPE_2";
assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type));
String testLabel = "test_label";
assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type, testLabel));
List<Asset> foundedAssetsByType = assetDao
.findAssetsByTenantIdAndCustomerIdAndType(tenantId2, customerId2, type, new PageLink(3)).getData();
compareFoundedAssetByType(foundedAssetsByType, type);
List<Asset> foundedAssetsByTypeAndLabel = assetDao
.findAssetsByTenantIdAndCustomerIdAndType(tenantId2, customerId2, type, new PageLink(3, 0, testLabel)).getData();
assertEquals(1, foundedAssetsByTypeAndLabel.size());
}
private void compareFoundedAssetByType(List<Asset> foundedAssetsByType, String type) {
@ -228,10 +253,14 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
}
private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name) {
return saveAsset(id, tenantId, customerId, name, null);
return saveAsset(id, tenantId, customerId, name, null, null);
}
private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String label) {
return saveAsset(id, tenantId, customerId, name, null, label);
}
private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type) {
private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type, String label) {
if (type == null) {
type = "default";
}
@ -241,6 +270,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
asset.setCustomerId(new CustomerId(customerId));
asset.setName(name);
asset.setType(type);
asset.setLabel(label);
asset.setAssetProfileId(assetProfileId(type));
return assetDao.save(TenantId.fromUUID(tenantId), asset);
}

8
dao/src/test/resources/sql/system-data.sql

@ -53,6 +53,14 @@ VALUES ( '23199d80-6e7e-11ee-8829-ef9fd52a6141', 1697719852888, '13814000-1dd2-1
"coaps":{"enabled":false,"host":"","port":"5684"}
}' );
INSERT INTO admin_settings ( id, created_time, tenant_id, key, json_value )
VALUES ( '1e33c6f0-061e-11ef-b5b7-dba0ee077a1b', 1714391189727, '13814000-1dd2-11b2-8080-808080808080', 'jwt', '{
"tokenExpirationTime": "9000",
"refreshTokenExpTime": "604800",
"tokenIssuer": "thingsboard.io",
"tokenSigningKey": "QmlicmJkZk9tSzZPVFozcWY0Sm94UVhybmtBWXZ5YmZMOUZSZzZvcUFiOVhsb3VHUThhUWJGaXp3UHhtcGZ6Tw=="
}' );
INSERT INTO queue ( id, created_time, tenant_id, name, topic, poll_interval, partitions, consumer_per_partition, pack_processing_timeout, submit_strategy, processing_strategy )
VALUES ( '6eaaefa6-4612-11e7-a919-92ebcb67fe33', 1592576748000 ,'13814000-1dd2-11b2-8080-808080808080', 'Main' ,'tb_rule_engine.main', 25, 10, true, 2000,
'{"type": "BURST", "batchSize": 1000}',

48
pom.xml

@ -49,7 +49,7 @@
<spring-redis.version>6.2.4</spring-redis.version>
<spring-security.version>6.2.4</spring-security.version>
<jedis.version>5.1.2</jedis.version>
<jjwt.version>0.9.1</jjwt.version> <!-- 0.12.5 requires JWT usage refactoring-->
<jjwt.version>0.12.5</jjwt.version>
<slf4j.version>2.0.13</slf4j.version>
<log4j.version>2.23.1</log4j.version>
<logback.version>1.5.5</logback.version>
@ -1476,6 +1476,12 @@
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${apache-httpclient.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
@ -1943,6 +1949,12 @@
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.javadelight</groupId>
@ -1968,11 +1980,23 @@
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
<version>${aws.sdk.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sns</artifactId>
<version>${aws.sdk.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
@ -2102,6 +2126,10 @@
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
<exclusion>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
@ -2136,6 +2164,12 @@
<artifactId>mockserver-netty</artifactId>
<version>${mock-server.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
@ -2168,6 +2202,12 @@
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>${firebase-admin.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
@ -2203,6 +2243,12 @@
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-transcoder</artifactId>
<version>${apache-xmlgraphics.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>

7
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts

@ -163,6 +163,13 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
this.updateNodeData(this.subscription.data);
}
public onEditModeChanged() {
if (this.textSearchMode) {
this.ctx.hideTitlePanel = !this.ctx.isEdit;
this.ctx.detectChanges(true);
}
}
private initializeConfig() {
this.ctx.widgetActions = [this.searchAction];

7
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -266,6 +266,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.ctx.detectChanges();
}
public onEditModeChanged() {
if (this.textSearchMode) {
this.ctx.hideTitlePanel = !this.ctx.isEdit;
this.ctx.detectChanges(true);
}
}
public pageLinkSortDirection(): SortDirection {
return entityDataPageLinkSortDirection(this.pageLink);
}

7
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -307,6 +307,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.ctx.detectChanges();
}
public onEditModeChanged() {
if (this.textSearchMode) {
this.ctx.hideTitlePanel = !this.ctx.isEdit;
this.ctx.detectChanges(true);
}
}
private initialize() {
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];

4
ui-ngx/src/assets/locale/locale.constant-ar_AE.json

@ -482,9 +482,9 @@
"issuer-name": "اسم الجهة المصدرة",
"issuer-name-required": "اسم الجهة المصدرة مطلوب.",
"signings-key": "مفتاح التوقيع",
"signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 256 بت من البيانات.",
"signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 512 بت من البيانات.",
"signings-key-required": "مفتاح التوقيع مطلوب.",
"signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 256 بت من البيانات.",
"signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 512 بت من البيانات.",
"signings-key-base64": "يجب أن يكون مفتاح التوقيع بتنسيق base64.",
"expiration-time": "وقت انتهاء صلاحية الرمز (ثانية)",
"expiration-time-required": "وقت انتهاء صلاحية الرمز مطلوب.",

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

@ -457,9 +457,9 @@
"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-hint": "Base64 encoded string representing at least 512 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-min-length": "Signing key must be at least 512 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.",

4
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -431,9 +431,9 @@
"issuer-name": "Nombre del emisor",
"issuer-name-required": "Se requiere nombre del emisor.",
"signings-key": "Clave de firma",
"signings-key-hint": "Una string codificada en Base64 representando por lo menos 256 bits de datos.",
"signings-key-hint": "Una string codificada en Base64 representando por lo menos 512 bits de datos.",
"signings-key-required": "Se requiere clave de firma.",
"signings-key-min-length": "La clave de firma debe tener al menos 256 bits de datos.",
"signings-key-min-length": "La clave de firma debe tener al menos 512 bits de datos.",
"signings-key-base64": "La clave de firma debe estar en formato base64.",
"expiration-time": "Caducidad del token (en segundos)",
"expiration-time-required": "Se requiere caducidad del token.",

4
ui-ngx/src/assets/locale/locale.constant-nl_BE.json

@ -425,9 +425,9 @@
"issuer-name": "Naam van de uitgever",
"issuer-name-required": "De naam van de uitgever is vereist.",
"signings-key": "Sleutel ondertekenen",
"signings-key-hint": "Base64-gecodeerde tekenreeks die ten minste 256 bits aan gegevens vertegenwoordigt.",
"signings-key-hint": "Base64-gecodeerde tekenreeks die ten minste 512 bits aan gegevens vertegenwoordigt.",
"signings-key-required": "Ondertekeningssleutel is vereist.",
"signings-key-min-length": "De ondertekeningssleutel moet ten minste 256 bits aan gegevens bevatten.",
"signings-key-min-length": "De ondertekeningssleutel moet ten minste 512 bits aan gegevens bevatten.",
"signings-key-base64": "De ondertekeningssleutel moet de base64-indeling hebben.",
"expiration-time": "Vervaltijd token (sec)",
"expiration-time-required": "De vervaltijd van het token is vereist.",

4
ui-ngx/src/assets/locale/locale.constant-pl_PL.json

@ -457,9 +457,9 @@
"issuer-name": "Nazwa emitenta",
"issuer-name-required": "Nazwa emitenta jest wymagana.",
"signings-key": "Klucz do podpisu",
"signings-key-hint": "Ciąg zakodowany w formacie Base64 reprezentujący co najmniej 256 bitów danych.",
"signings-key-hint": "Ciąg zakodowany w formacie Base64 reprezentujący co najmniej 512 bitów danych.",
"signings-key-required": "Klucz do podpisu jest wymagany.",
"signings-key-min-length": "Klucz podpisujący musi mieć co najmniej 256 bitów danych.",
"signings-key-min-length": "Klucz podpisujący musi mieć co najmniej 512 bitów danych.",
"signings-key-base64": "Klucz podpisujący musi być w formacie base64.",
"expiration-time": "Czas ważności tokena (s)",
"expiration-time-required": "Czas ważności tokena jest wymagany.",

4
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -451,9 +451,9 @@
"issuer-name": "发行者名称",
"issuer-name-required": "发行者名称必填。",
"signings-key": "签名密钥",
"signings-key-hint": "Base64编码的字符串,至少256位数据。",
"signings-key-hint": "Base64编码的字符串,至少512位数据。",
"signings-key-required": "签名密钥必填。",
"signings-key-min-length": "签名密钥必须至少为256位的数据。",
"signings-key-min-length": "签名密钥必须至少为512位的数据。",
"signings-key-base64": "签名密钥必须是Base64格式。",
"expiration-time": "令牌过期时间(秒)",
"expiration-time-required": "令牌过期时间是必填。",

Loading…
Cancel
Save