From f9342a4778890a12d6efcbe1022831fe83050be7 Mon Sep 17 00:00:00 2001 From: rusikv Date: Fri, 22 Mar 2024 13:20:20 +0200 Subject: [PATCH 01/30] UI: fixed widgets in search mode not editable --- .../data/json/system/widget_types/asset_admin_table.json | 2 +- .../data/json/system/widget_types/device_admin_table.json | 2 +- .../data/json/system/widget_types/entities_hierarchy.json | 2 +- .../main/data/json/system/widget_types/entities_table.json | 2 +- .../data/json/system/widget_types/timeseries_table.json | 2 +- .../lib/entity/entities-hierarchy-widget.component.ts | 7 +++++++ .../widget/lib/entity/entities-table-widget.component.ts | 7 +++++++ .../widget/lib/timeseries-table-widget.component.ts | 7 +++++++ 8 files changed, 26 insertions(+), 5 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/asset_admin_table.json b/application/src/main/data/json/system/widget_types/asset_admin_table.json index 0a2eb2c37a..fe5722a01a 100644 --- a/application/src/main/data/json/system/widget_types/asset_admin_table.json +++ b/application/src/main/data/json/system/widget_types/asset_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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", diff --git a/application/src/main/data/json/system/widget_types/device_admin_table.json b/application/src/main/data/json/system/widget_types/device_admin_table.json index af460a286c..35cb457590 100644 --- a/application/src/main/data/json/system/widget_types/device_admin_table.json +++ b/application/src/main/data/json/system/widget_types/device_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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", diff --git a/application/src/main/data/json/system/widget_types/entities_hierarchy.json b/application/src/main/data/json/system/widget_types/entities_hierarchy.json index 48e9c3e632..5dc2788f12 100644 --- a/application/src/main/data/json/system/widget_types/entities_hierarchy.json +++ b/application/src/main/data/json/system/widget_types/entities_hierarchy.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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", diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json index 1ebf8d4fde..73a546cb87 100644 --- a/application/src/main/data/json/system/widget_types/entities_table.json +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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", diff --git a/application/src/main/data/json/system/widget_types/timeseries_table.json b/application/src/main/data/json/system/widget_types/timeseries_table.json index d267d42763..767f6e8c33 100644 --- a/application/src/main/data/json/system/widget_types/timeseries_table.json +++ b/application/src/main/data/json/system/widget_types/timeseries_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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": "", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts index b524bd7cf8..63355805ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts +++ b/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]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index 93b3a06ac2..9f9d9268c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/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); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 77b86a29f4..b21614cb50 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/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]; From ff640448e09ce9c7547c7218c78c32dde7fd763a Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 29 Apr 2024 12:35:48 +0300 Subject: [PATCH 02/30] asset search text is being applied to name, label and customer title where possible --- .../server/dao/sql/asset/AssetRepository.java | 33 ++++++++++----- .../server/dao/sql/asset/JpaAssetDaoTest.java | 40 ++++++++++++++++--- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index f65e0db1c6..3e984d9823 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -41,7 +41,8 @@ public interface AssetRepository extends JpaRepository, 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) ") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -61,7 +62,8 @@ public interface AssetRepository extends JpaRepository, Expor @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) ") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, @@ -69,7 +71,8 @@ public interface AssetRepository extends JpaRepository, 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 findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, @Param("profileId") UUID profileId, @Param("searchText") String searchText, @@ -81,7 +84,9 @@ public interface AssetRepository extends JpaRepository, 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) ") Page findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -95,7 +100,8 @@ public interface AssetRepository extends JpaRepository, 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 findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -132,7 +138,8 @@ public interface AssetRepository extends JpaRepository, 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 findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -146,7 +153,9 @@ public interface AssetRepository extends JpaRepository, 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 findAssetInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -160,7 +169,9 @@ public interface AssetRepository extends JpaRepository, 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) ") Page findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("assetProfileId") UUID assetProfileId, @@ -172,7 +183,8 @@ public interface AssetRepository extends JpaRepository, 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) ") Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -182,7 +194,8 @@ public interface AssetRepository extends JpaRepository, 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 findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java index bd6669f471..3e4b7ca38a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java +++ b/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 assets1 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(20, assets1.getData().size()); + + pageLink = pageLink.nextPageLink(); + PageData assets2 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(10, assets2.getData().size()); + + pageLink = pageLink.nextPageLink(); + PageData assets3 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(0, assets3.getData().size()); + } + @Test public void testFindAssetsByTenantIdAndIdsAsync() throws ExecutionException, InterruptedException, TimeoutException { List 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 foundedAssetsByType = assetDao .findAssetsByTenantIdAndType(tenantId2, type, new PageLink(3)).getData(); compareFoundedAssetByType(foundedAssetsByType, type); + + List 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 foundedAssetsByType = assetDao .findAssetsByTenantIdAndCustomerIdAndType(tenantId2, customerId2, type, new PageLink(3)).getData(); compareFoundedAssetByType(foundedAssetsByType, type); + + List foundedAssetsByTypeAndLabel = assetDao + .findAssetsByTenantIdAndCustomerIdAndType(tenantId2, customerId2, type, new PageLink(3, 0, testLabel)).getData(); + assertEquals(1, foundedAssetsByTypeAndLabel.size()); } private void compareFoundedAssetByType(List 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); } From c8b63bd481fe73d7507439848b58b4878fb260f3 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 29 Apr 2024 12:44:38 +0300 Subject: [PATCH 03/30] asset search text is being applied to asset type where possible --- .../server/dao/sql/asset/AssetRepository.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index 3e984d9823..eb83a5805e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -129,7 +129,8 @@ public interface AssetRepository extends JpaRepository, 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 findAssetInfosByTenantIdAndAssetProfileId(@Param("tenantId") UUID tenantId, @Param("assetProfileId") UUID assetProfileId, @Param("textSearch") String textSearch, @@ -171,7 +172,8 @@ public interface AssetRepository extends JpaRepository, 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 findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("assetProfileId") UUID assetProfileId, @@ -184,7 +186,8 @@ public interface AssetRepository extends JpaRepository, 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 (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " + - " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :searchText, '%')) = true) ") Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, From e32797400c522591dd4525867ed05f4928fe34f5 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Apr 2024 15:18:37 +0200 Subject: [PATCH 04/30] updated jjwt version --- .../install/ThingsboardInstallService.java | 1 + .../DefaultSystemDataLoaderService.java | 117 ++++++++---------- .../install/SystemDataLoaderService.java | 2 + .../settings/DefaultJwtSettingsService.java | 24 +--- .../settings/DefaultJwtSettingsValidator.java | 4 +- .../auth/jwt/settings/JwtSettingsService.java | 2 - .../security/model/token/JwtTokenFactory.java | 40 +++--- .../model/token/OAuth2AppTokenFactory.java | 6 +- .../server/controller/AbstractWebTest.java | 15 +-- .../controller/AdminControllerTest.java | 4 +- .../security/auth/JwtTokenFactoryTest.java | 35 +----- .../data/security/model/JwtSettings.java | 2 +- .../notification/DefaultNotifications.java | 9 -- dao/src/test/resources/sql/system-data.sql | 8 ++ pom.xml | 2 +- 15 files changed, 115 insertions(+), 156 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index eb9d908bab..cdb808ade9 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -133,6 +133,7 @@ public class ThingsboardInstallService { case "3.6.4": log.info("Upgrading ThingsBoard from version 3.6.4 to 3.7.0 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.4"); + systemDataLoaderService.updateJwtSettings(); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 4da6a383e8..4cd1b7a12f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/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,80 +119,42 @@ 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.JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT; @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; - @Bean protected BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -298,6 +262,33 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { jwtSettingsService.createRandomJwtSettings(); } + @Override + public void updateJwtSettings() { + JwtSettings jwtSettings = jwtSettingsService.getJwtSettings(); + + boolean invalidSignKey = false; + + if (TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { + log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + + "Added new temporary 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."); + invalidSignKey = true; + } else if (Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()).length * Byte.SIZE < 512) { + log.warn("WARNING: The platform is configured to use JWT Signing Key with length less then 512 bits of data. " + + "Added new temporary 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."); + invalidSignKey = true; + } + + if (invalidSignKey) { + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + jwtSettingsService.saveJwtSettings(jwtSettings); + } + } + @Override public void createOAuth2Templates() throws Exception { installScripts.createOAuth2Templates(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index eeac1b6aa4..366ae0bda6 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/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; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index e5143a6023..dd7cd8ae04 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -21,14 +21,11 @@ import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Value; 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 java.nio.charset.StandardCharsets; @@ -43,7 +40,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final AdminSettingsService adminSettingsService; private final Optional tbClusterService; - private final Optional notificationCenter; private final JwtSettingsValidator jwtSettingsValidator; @Value("${security.jwt.tokenExpirationTime:9000}") @@ -75,17 +71,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { } } - /** - * 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); @@ -123,14 +108,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { 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; } } @@ -138,6 +115,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { return this.jwtSettings; } + @Deprecated(since = "3.7.0", forRemoval = true) private JwtSettings getJwtSettingsFromYml() { return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index b807f65d36..4ca02a1add 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -59,8 +59,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 < 512 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { + 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 diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java index 19095ad0b9..12b1d017c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java @@ -28,8 +28,6 @@ public interface JwtSettingsService { void createRandomJwtSettings(); - void saveLegacyYmlSettings(); - JwtSettings saveJwtSettings(JwtSettings jwtSettings); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 08622f578a..abdcf76888 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -16,14 +16,15 @@ 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.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.security.authentication.BadCredentialsException; @@ -41,7 +42,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; @@ -95,7 +99,7 @@ public class JwtTokenFactory { public SecurityUser parseAccessJwtToken(String token) { Jws jwsClaims = parseTokenClaims(token); - Claims claims = jwsClaims.getBody(); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); @SuppressWarnings("unchecked") List scopes = claims.get(SCOPES, List.class); @@ -140,14 +144,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 jwsClaims = parseTokenClaims(token); - Claims claims = jwsClaims.getBody(); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); @SuppressWarnings("unchecked") List scopes = claims.get(SCOPES, List.class); @@ -183,28 +187,29 @@ 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()); + claimsBuilder.add(USER_ID, securityUser.getId().getId().toString()); + claimsBuilder.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(toSecretKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()), Jwts.SIG.HS512); } public Jws parseTokenClaims(String token) { try { return Jwts.parser() - .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) - .parseClaimsJws(token); + .verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey()))) + .build() + .parseSignedClaims(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); throw new BadCredentialsException("Invalid JWT token: ", ex); @@ -220,4 +225,9 @@ public class JwtTokenFactory { return new JwtPair(accessToken.getToken(), refreshToken.getToken()); } + private SecretKey toSecretKey(String base64Key) { + byte[] decodedToken = Base64.getDecoder().decode(base64Key); + return new SecretKeySpec(decodedToken, "HmacSHA512"); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java index 7f956f6970..cf01dec208 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java +++ b/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 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"); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index e14b041e62..b626f7f959 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -22,6 +22,7 @@ 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.Jws; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; @@ -120,7 +121,9 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; 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 javax.crypto.SecretKey; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; @@ -237,6 +240,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected ClaimDevicesService claimDevicesService; + @Autowired + private JwtTokenFactory jwtTokenFactory; + @SpyBean protected MailService mailService; @@ -558,13 +564,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 jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); - Claims claims = jwsClaims.getBody(); + Jws jwsClaims = jwtTokenFactory.parseTokenClaims(token); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); Assert.assertEquals(username, subject); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java index 430ff2eb09..8876bf8c24 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java +++ b/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()); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 2d39dd9905..75868e6508 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/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()); } 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()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java index d8368f247b..07c158830f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java +++ b/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; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index fd17618bd1..22b8b39f2b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -358,15 +358,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; diff --git a/dao/src/test/resources/sql/system-data.sql b/dao/src/test/resources/sql/system-data.sql index 7a79f1114d..0b17d4f108 100644 --- a/dao/src/test/resources/sql/system-data.sql +++ b/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}', diff --git a/pom.xml b/pom.xml index c13c25566b..1053768af3 100755 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ 6.2.4 6.2.4 5.1.2 - 0.9.1 + 0.12.5 2.0.13 2.23.1 1.5.5 From 997aba3f856b59862e72b5c25171e4a15f9a1b15 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Apr 2024 15:43:06 +0200 Subject: [PATCH 05/30] added length check for createRandomJwtSettings --- .../security/auth/jwt/settings/DefaultJwtSettingsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index dd7cd8ae04..f7922f6a68 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -61,7 +61,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { if (getJwtSettingsFromDb() == null) { log.info("Creating JWT admin settings..."); this.jwtSettings = getJwtSettingsFromYml(); - if (isSigningKeyDefault(jwtSettings)) { + if (isSigningKeyDefault(jwtSettings) || Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()).length * Byte.SIZE < 512) { this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); } From 1a61e4d0649416b33a20546e6b2e2cc74ea664ae Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Apr 2024 15:46:20 +0200 Subject: [PATCH 06/30] removed unnecessary comment --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1053768af3..2a9afbba7a 100755 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ 6.2.4 6.2.4 5.1.2 - 0.12.5 + 0.12.5 2.0.13 2.23.1 1.5.5 From 6fd0e13516def708c834096bea0cb178ca27d142 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Apr 2024 16:18:49 +0200 Subject: [PATCH 07/30] removed deprecation from getJwtSettingsFromYml --- .../security/auth/jwt/settings/DefaultJwtSettingsService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index f7922f6a68..cfb540fbf1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -115,7 +115,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { return this.jwtSettings; } - @Deprecated(since = "3.7.0", forRemoval = true) private JwtSettings getJwtSettingsFromYml() { return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); } From e993ec8aa483b31f073a1e0d64fb281886c65b16 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 13:42:47 +0200 Subject: [PATCH 08/30] minor refactoring --- .../DefaultSystemDataLoaderService.java | 28 +++++++++-- .../settings/DefaultJwtSettingsService.java | 48 +++---------------- .../settings/DefaultJwtSettingsValidator.java | 4 +- .../auth/jwt/settings/JwtSettingsService.java | 2 - .../security/model/token/JwtTokenFactory.java | 7 +-- .../server/controller/AbstractWebTest.java | 4 -- 6 files changed, 38 insertions(+), 55 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index fbbc73ab82..fbfbd9e87b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -119,7 +119,8 @@ 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.JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT; +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") @@ -155,6 +156,15 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Getter private boolean persistActivityToTelemetry; + @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() { return new BCryptPasswordEncoder(); @@ -259,7 +269,17 @@ 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 @@ -268,13 +288,13 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { boolean invalidSignKey = false; - if (TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { + if (isSigningKeyDefault(jwtSettings)) { log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + "Added new temporary 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."); invalidSignKey = true; - } else if (Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()).length * Byte.SIZE < 512) { + } else if (!validateTokenSigningKeyLength(jwtSettings)) { log.warn("WARNING: The platform is configured to use JWT Signing Key with length less then 512 bits of data. " + "Added new temporary JWT Signing Key. " + "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index cfb540fbf1..92019341c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -15,10 +15,9 @@ */ package org.thingsboard.server.service.security.auth.jwt.settings; +import io.jsonwebtoken.Jwts; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; @@ -28,7 +27,6 @@ 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; @@ -42,35 +40,8 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final Optional 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) || Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()).length * Byte.SIZE < 512) { - 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."); - } - } - @Override public JwtSettings saveJwtSettings(JwtSettings jwtSettings) { jwtSettingsValidator.validate(jwtSettings); @@ -103,22 +74,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."); - } - 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; @@ -138,8 +100,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 >= Jwts.SIG.HS512.getKeyBitLength(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index 4ca02a1add..1de9e2c0c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -27,6 +27,8 @@ 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; + @Component @RequiredArgsConstructor public class DefaultJwtSettingsValidator implements JwtSettingsValidator { @@ -59,7 +61,7 @@ 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 < 512 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { + if (decodedKey.length * Byte.SIZE < 512 && !isSigningKeyDefault(jwtSettings)) { throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 512 bits of data!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java index 12b1d017c2..d3aa261b00 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java @@ -26,8 +26,6 @@ public interface JwtSettingsService { JwtSettings reloadJwtSettings(); - void createRandomJwtSettings(); - JwtSettings saveJwtSettings(JwtSettings jwtSettings); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index abdcf76888..6be020b1c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -187,9 +187,10 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); - ClaimsBuilder claimsBuilder = Jwts.claims().subject(principal.getValue()); - claimsBuilder.add(USER_ID, securityUser.getId().getId().toString()); - claimsBuilder.add(SCOPES, scopes); + ClaimsBuilder claimsBuilder = Jwts.claims() + .subject(principal.getValue()) + .add(USER_ID, securityUser.getId().getId().toString()) + .add(SCOPES, scopes); if (securityUser.getSessionId() != null) { claimsBuilder.add(SESSION_ID, securityUser.getSessionId()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index ffc37dc7a1..b83a1b567c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -21,10 +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.Jws; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; import org.awaitility.Awaitility; import org.hamcrest.Matcher; @@ -124,7 +121,6 @@ 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 javax.crypto.SecretKey; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; From 1f648806f3f05771bc094a569126fcf5d725dc97 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 16:13:46 +0200 Subject: [PATCH 09/30] refactored creationing Jwt SecretKey and JwtParcer --- .../queue/DefaultTbCoreConsumerService.java | 5 ++- .../DefaultTbRuleEngineConsumerService.java | 7 ++- .../processing/AbstractConsumerService.java | 4 ++ .../settings/DefaultJwtSettingsService.java | 5 ++- .../settings/DefaultJwtSettingsValidator.java | 3 +- .../security/model/token/JwtTokenFactory.java | 43 +++++++++++++++---- 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index cdb3f19601..445d1a6bba 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -94,6 +94,7 @@ import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; @@ -172,10 +173,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> nfConsumer; protected final JwtSettingsService jwtSettingsService; + protected final JwtTokenFactory jwtTokenFactory; public void init(String nfConsumerThreadName) { this.notificationsConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(nfConsumerThreadName)); @@ -163,6 +166,7 @@ public abstract class AbstractConsumerService= Jwts.SIG.HS512.getKeyBitLength(); + return Base64.getDecoder().decode(settings.getTokenSigningKey()).length * Byte.SIZE >= KEY_LENGTH; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index 1de9e2c0c2..ea8c67e3dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -28,6 +28,7 @@ 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 @@ -61,7 +62,7 @@ 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 < 512 && !isSigningKeyDefault(jwtSettings)) { + 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!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 6be020b1c5..9542a9de2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -20,6 +20,7 @@ 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.SignatureException; @@ -57,6 +58,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"; @@ -69,6 +72,9 @@ public class JwtTokenFactory { private final JwtSettingsService jwtSettingsService; + private volatile JwtParser jwtParser; + private volatile SecretKey secretKey; + /** * Factory method for issuing new JWT Tokens. */ @@ -180,6 +186,11 @@ public class JwtTokenFactory { return new AccessJwtToken(jwtBuilder.compact()); } + public void reload() { + getSecretKey(true); + getJwtParser(true); + } + private JwtBuilder setUpToken(SecurityUser securityUser, List scopes, long expirationTime) { if (StringUtils.isBlank(securityUser.getEmail())) { throw new IllegalArgumentException("Cannot create JWT Token without username/email"); @@ -202,15 +213,12 @@ public class JwtTokenFactory { .issuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) .issuedAt(Date.from(currentTime.toInstant())) .expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) - .signWith(toSecretKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()), Jwts.SIG.HS512); + .signWith(getSecretKey(false), Jwts.SIG.HS512); } public Jws parseTokenClaims(String token) { try { - return Jwts.parser() - .verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey()))) - .build() - .parseSignedClaims(token); + return getJwtParser(false).parseSignedClaims(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); throw new BadCredentialsException("Invalid JWT token: ", ex); @@ -226,9 +234,28 @@ public class JwtTokenFactory { return new JwtPair(accessToken.getToken(), refreshToken.getToken()); } - private SecretKey toSecretKey(String base64Key) { - byte[] decodedToken = Base64.getDecoder().decode(base64Key); - return new SecretKeySpec(decodedToken, "HmacSHA512"); + 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; + } } From 2d1ed7871f24e618f82e54196dcde4909a556e4a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 16:44:12 +0200 Subject: [PATCH 10/30] updated jwt translation --- ui-ngx/src/assets/locale/locale.constant-ar_AE.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-en_US.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-es_ES.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-nl_BE.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-pl_PL.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-zh_CN.json | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 104eb18abd..2c552d6ab6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/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": "وقت انتهاء صلاحية الرمز مطلوب.", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1ba94ab8bb..0d755bcf19 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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.", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index e80fbf6c3e..00035bc3cc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/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.", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index 4cd3bb1e49..f62f352723 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/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.", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index 58d4a0168f..d5d9ac8f83 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -456,9 +456,9 @@ "issuer-name":"Nazwa wydawcy", "issuer-name-required":"Nazwa wydawcy jest wymagana.", "signings-key":"Klucz podpisujący", - "signings-key-hint":"Kodowany Base64 ciąg reprezentujący co najmniej 256 bitów danych.", + "signings-key-hint":"Kodowany Base64 ciąg reprezentujący co najmniej 512 bitów danych.", "signings-key-required":"Klucz podpisujący jest wymagany.", - "signings-key-min-length":"Klucz podpisujący musi reprezentować co najmniej 256 bitów danych.", + "signings-key-min-length":"Klucz podpisujący musi reprezentować co najmniej 512 bitów danych.", "signings-key-base64":"Klucz podpisujący musi być w formacie base64.", "expiration-time":"Czas wygaśnięcia tokena (sek)", "expiration-time-required":"Czas wygaśnięcia tokena jest wymagany.", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 886151fdf2..c691bbea76 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/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": "令牌过期时间是必填。", From 4df0a41178082762f8bf546bd867f9e6486ed4b0 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 17:48:31 +0200 Subject: [PATCH 11/30] UpdateJwtSettings warn msg improvements --- .../install/DefaultSystemDataLoaderService.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index fbfbd9e87b..bdd0494443 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -285,24 +285,22 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Override public void updateJwtSettings() { JwtSettings jwtSettings = jwtSettingsService.getJwtSettings(); - boolean invalidSignKey = false; + String warningMessage = null; if (isSigningKeyDefault(jwtSettings)) { - log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + - "Added new temporary 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."); + warningMessage = "The platform is using the default JWT Signing Key, which is a security risk."; invalidSignKey = true; } else if (!validateTokenSigningKeyLength(jwtSettings)) { - log.warn("WARNING: The platform is configured to use JWT Signing Key with length less then 512 bits of data. " + - "Added new temporary 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."); + 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); From d25f271d8965529df40bbb46fa7b602b253b622a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 22:58:44 +0200 Subject: [PATCH 12/30] JwtFactory reload --- .../auth/jwt/settings/DefaultJwtSettingsService.java | 8 +++++++- .../server/service/security/auth/JwtTokenFactoryTest.java | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index 537f063bf2..df822812ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.jwt.settings; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; @@ -25,6 +26,7 @@ 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 org.thingsboard.server.service.security.model.token.JwtTokenFactory; import java.util.Base64; import java.util.Objects; @@ -40,6 +42,8 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final AdminSettingsService adminSettingsService; private final Optional tbClusterService; private final JwtSettingsValidator jwtSettingsValidator; + @Lazy + private final JwtTokenFactory jwtTokenFactory; private volatile JwtSettings jwtSettings = null; //lazy init @@ -62,7 +66,9 @@ public class DefaultJwtSettingsService implements JwtSettingsService { @Override public JwtSettings reloadJwtSettings() { log.trace("Executing reloadJwtSettings"); - return getJwtSettings(true); + var settings = getJwtSettings(true); + jwtTokenFactory.reload(); + return settings; } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 75868e6508..264bb87979 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -53,6 +53,7 @@ public class JwtTokenFactoryTest { private JwtTokenFactory tokenFactory; private AdminSettingsService adminSettingsService; private JwtSettingsService jwtSettingsService; + private JwtTokenFactory jwtTokenFactory; private JwtSettings jwtSettings; @@ -170,7 +171,7 @@ public class JwtTokenFactoryTest { } private DefaultJwtSettingsService mockJwtSettingsService() { - return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator()); + return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), jwtTokenFactory); } private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { From e59c4a950c4bf203082768727ac403679532f280 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 3 May 2024 09:36:12 +0200 Subject: [PATCH 13/30] refactoring --- .../server/service/queue/DefaultTbCoreConsumerService.java | 4 +--- .../service/queue/DefaultTbRuleEngineConsumerService.java | 6 ++---- .../service/queue/processing/AbstractConsumerService.java | 2 -- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 445d1a6bba..42c516a8a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -94,7 +94,6 @@ import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; -import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; @@ -173,12 +172,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> nfConsumer; protected final JwtSettingsService jwtSettingsService; - protected final JwtTokenFactory jwtTokenFactory; public void init(String nfConsumerThreadName) { this.notificationsConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(nfConsumerThreadName)); @@ -166,7 +165,6 @@ public abstract class AbstractConsumerService Date: Fri, 3 May 2024 13:09:52 +0300 Subject: [PATCH 14/30] updated log entry for adding customer_title_unq_key --- .../install/SqlAbstractDatabaseSchemaService.java | 10 +++++++--- .../install/SqlEntityDatabaseSchemaService.java | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java index 9058db4172..3ba938bd90 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java +++ b/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); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java index b1a41aadcd..5e1357a48e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java +++ b/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!"); } } From 510e4b14fe39dcae7853b9fadc7494f1f115e62a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 3 May 2024 15:27:21 +0200 Subject: [PATCH 15/30] fixed instalation --- .../security/auth/jwt/settings/DefaultJwtSettingsService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index df822812ec..139b177926 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -43,7 +43,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final Optional tbClusterService; private final JwtSettingsValidator jwtSettingsValidator; @Lazy - private final JwtTokenFactory jwtTokenFactory; + private final Optional jwtTokenFactory; private volatile JwtSettings jwtSettings = null; //lazy init @@ -67,7 +67,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { public JwtSettings reloadJwtSettings() { log.trace("Executing reloadJwtSettings"); var settings = getJwtSettings(true); - jwtTokenFactory.reload(); + jwtTokenFactory.ifPresent(JwtTokenFactory::reload); return settings; } From 898d69a4ade586c27ba976da779abb9dcd69c11a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 3 May 2024 20:18:13 +0200 Subject: [PATCH 16/30] fixed events ordering during tenant creation --- .../server/dao/tenant/TenantServiceImpl.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 01ea937ba4..41680f76dd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -136,18 +136,20 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Fri, 3 May 2024 21:19:08 +0200 Subject: [PATCH 17/30] fixed circular dependency --- .../security/auth/jwt/settings/DefaultJwtSettingsService.java | 2 +- .../server/service/security/model/token/JwtTokenFactory.java | 2 ++ .../server/service/security/auth/JwtTokenFactoryTest.java | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index 139b177926..c5c04ac312 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.jwt.settings; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -42,7 +43,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final AdminSettingsService adminSettingsService; private final Optional tbClusterService; private final JwtSettingsValidator jwtSettingsValidator; - @Lazy private final Optional jwtTokenFactory; private volatile JwtSettings jwtSettings = null; //lazy init diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 9542a9de2d..f2e276a0ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -28,6 +28,7 @@ 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; @@ -70,6 +71,7 @@ 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; diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 264bb87979..e6c6cfbac4 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -53,7 +53,6 @@ public class JwtTokenFactoryTest { private JwtTokenFactory tokenFactory; private AdminSettingsService adminSettingsService; private JwtSettingsService jwtSettingsService; - private JwtTokenFactory jwtTokenFactory; private JwtSettings jwtSettings; @@ -171,7 +170,7 @@ public class JwtTokenFactoryTest { } private DefaultJwtSettingsService mockJwtSettingsService() { - return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), jwtTokenFactory); + return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), Optional.empty()); } private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { From f23061a9aed95c10f534de03304bd85f884181a2 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 6 May 2024 12:22:26 +0300 Subject: [PATCH 18/30] Fix notification deduplication service (ConcurrentModificationException for deduplicationDurations map) --- .../DefaultNotificationDeduplicationService.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java index 714da709f7..93fe03e5cf 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java +++ b/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 deduplicationDurations; + private ConcurrentMap 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)); }); From c554246218979c2fb7a7646847834aec6cd02aec Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 6 May 2024 14:21:40 +0300 Subject: [PATCH 19/30] Ignore tenant not found for Cassandra rate limits --- .../server/cache/limits/DefaultRateLimitService.java | 11 ++++++++++- .../server/cache/limits/RateLimitService.java | 2 ++ .../server/dao/util/AbstractBufferedRateExecutor.java | 8 ++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java index f3530e99d2..9680fd2b96 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java +++ b/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() diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java index 84c22c514b..3573ee2b8c 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java +++ b/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); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index 48b2471217..34316bc615 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/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 Date: Mon, 6 May 2024 14:30:19 +0300 Subject: [PATCH 20/30] Increase test timeout for verifyMsgProcessed --- .../queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 6bd0cd9c0e..8e0b294241 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/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()); })); From b78c4fd21abf26e812d294162631fabdcf7744bb Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 6 May 2024 12:04:00 +0200 Subject: [PATCH 21/30] Add per-tenant rate limits for emails sending --- .../server/service/mail/DefaultMailService.java | 13 +++++++++++++ application/src/main/resources/thingsboard.yml | 2 ++ .../server/common/data/limit/LimitedApi.java | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 29db1d7d76..470127246d 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -22,6 +22,7 @@ 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; @@ -33,6 +34,7 @@ import org.springframework.stereotype.Service; import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; 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; @@ -43,6 +45,7 @@ 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; @@ -84,6 +87,12 @@ public class DefaultMailService implements MailService { @Autowired private TbMailContextComponent tbMailContextComponent; + @Autowired + private RateLimitService rateLimitService; + + @Value("${mail.per_tenant_rate_limits:}") + private String perTenantRateLimitConfig; + private TbMailSender mailSender; private String mailFrom; @@ -214,6 +223,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 ThingsboardException("Rate limit for emails sending is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); + } try { MimeMessage mailMsg = javaMailSender.createMimeMessage(); boolean multipart = (tbEmail.getImages() != null && !tbEmail.getImages().isEmpty()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 191d810c64..2fc2473851 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/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 + per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}" # Usage statistics parameters usage: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 1a0f038c90..48f8fdfc92 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/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", true); private Function configExtractor; @Getter From b750077c717132f0dd366338789e9ba044e16343 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 6 May 2024 12:18:53 +0200 Subject: [PATCH 22/30] Minor refactoring for mail rate limits --- .../service/mail/DefaultMailService.java | 3 +- .../exception/RateLimitExceededException.java | 30 +++++++++++++++++++ .../server/common/data/limit/LimitedApi.java | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 470127246d..04bf1f40a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -41,6 +41,7 @@ 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; @@ -225,7 +226,7 @@ public class DefaultMailService implements MailService { if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) { if (tenantId != null && !tenantId.isSysTenantId() && StringUtils.isNotEmpty(perTenantRateLimitConfig) && !rateLimitService.checkRateLimit(LimitedApi.EMAILS, (Object) tenantId, perTenantRateLimitConfig)) { - throw new ThingsboardException("Rate limit for emails sending is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); + throw new RateLimitExceededException(LimitedApi.EMAILS); } try { MimeMessage mailMsg = javaMailSender.createMimeMessage(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java new file mode 100644 index 0000000000..27be0f65d2 --- /dev/null +++ b/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"); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 48f8fdfc92..4afead3435 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -40,7 +40,7 @@ public enum LimitedApi { TWO_FA_VERIFICATION_CODE_CHECK(false, true), TRANSPORT_MESSAGES_PER_TENANT("transport messages", true), TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false), - EMAILS("emails", true); + EMAILS("emails sending", true); private Function configExtractor; @Getter From b8480cd456d1ef98f18e075c593e419c25f3b126 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 13:30:40 +0200 Subject: [PATCH 23/30] MailSenderInternalExecutorService hotfix (cherry picked from commit 5c5b93d6a6f8b2b4ac1dbd3b42d0b564206f2c70) --- .../service/mail/DefaultMailService.java | 2 +- .../MailSenderInternalExecutorService.java | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 04bf1f40a0..176449a213 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -80,7 +80,7 @@ public class DefaultMailService implements MailService { private TbApiUsageStateService apiUsageStateService; @Autowired - private MailExecutorService mailExecutorService; + private MailSenderInternalExecutorService mailExecutorService; @Autowired private PasswordResetExecutorService passwordResetExecutorService; diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java new file mode 100644 index 0000000000..ac57946916 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java @@ -0,0 +1,48 @@ +/** + * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * + * Copyright © 2016-2024 ThingsBoard, Inc. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of ThingsBoard, Inc. and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to ThingsBoard, Inc. + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material is strictly forbidden + * unless prior written permission is obtained from COMPANY. + * + * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, + * managers or contractors who have executed Confidentiality and Non-disclosure agreements + * explicitly covering such access. + * + * The copyright notice above does not evidence any actual or intended publication + * or disclosure of this source code, which includes + * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. + * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, + * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT + * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, + * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. + * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION + * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, + * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + */ +package org.thingsboard.server.service.mail; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.AbstractListeningExecutor; + +@Component +public class MailSenderInternalExecutorService extends AbstractListeningExecutor { + + @Value("${actors.rule.mail_thread_pool_size}") + private int mailExecutorThreadPoolSize; + + @Override + protected int getThreadPollSize() { + return mailExecutorThreadPoolSize; + } + +} From 38d37224f3f5834bfb72d5a449d247faa2d0ceab Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 16:06:08 +0200 Subject: [PATCH 24/30] DefaultMailService with timeout by mail-service-watchdog --- .../service/mail/DefaultMailService.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 176449a213..b9c7f71d66 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -16,6 +16,7 @@ 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; @@ -32,6 +33,7 @@ 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; @@ -53,11 +55,14 @@ 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; @@ -94,6 +99,8 @@ public class DefaultMailService implements MailService { @Value("${mail.per_tenant_rate_limits:}") private String perTenantRateLimitConfig; + private final ScheduledExecutorService scheduler; + private TbMailSender mailSender; private String mailFrom; @@ -105,6 +112,7 @@ public class DefaultMailService implements MailService { this.freemarkerConfig = freemarkerConfig; this.adminSettingsService = adminSettingsService; this.apiUsageClient = apiUsageClient; + this.scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog")); } @PostConstruct @@ -112,6 +120,13 @@ public class DefaultMailService implements MailService { updateMailConfiguration(); } + @PreDestroy + public void destroy() { + if (scheduler != null) { + scheduler.shutdownNow(); + } + } + @Override public void updateMailConfiguration() { AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); @@ -429,8 +444,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, scheduler); 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!"); From 97f0078288b70dd2911688e7991c46b24aa58e85 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 18:11:04 +0200 Subject: [PATCH 25/30] mvn license:format --- .../MailSenderInternalExecutorService.java | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java index ac57946916..03aea6c7f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java @@ -1,32 +1,17 @@ /** - * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * Copyright © 2016-2024 The Thingsboard Authors * - * Copyright © 2016-2024 ThingsBoard, Inc. All Rights Reserved. + * 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 * - * NOTICE: All information contained herein is, and remains - * the property of ThingsBoard, Inc. and its suppliers, - * if any. The intellectual and technical concepts contained - * herein are proprietary to ThingsBoard, Inc. - * and its suppliers and may be covered by U.S. and Foreign Patents, - * patents in process, and are protected by trade secret or copyright law. + * http://www.apache.org/licenses/LICENSE-2.0 * - * Dissemination of this information or reproduction of this material is strictly forbidden - * unless prior written permission is obtained from COMPANY. - * - * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, - * managers or contractors who have executed Confidentiality and Non-disclosure agreements - * explicitly covering such access. - * - * The copyright notice above does not evidence any actual or intended publication - * or disclosure of this source code, which includes - * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. - * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, - * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT - * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, - * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. - * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION - * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, - * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + * 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; From 3d26e854250a21d56093fd530ffde713680185b1 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 18:26:20 +0200 Subject: [PATCH 26/30] Mail service minor refactoring --- .../server/service/mail/DefaultMailService.java | 11 +++++------ .../mail/MailSenderInternalExecutorService.java | 4 ++++ .../server/service/mail/TbMailContextComponent.java | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index b9c7f71d66..d68837e1f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -19,7 +19,6 @@ 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; @@ -99,7 +98,7 @@ public class DefaultMailService implements MailService { @Value("${mail.per_tenant_rate_limits:}") private String perTenantRateLimitConfig; - private final ScheduledExecutorService scheduler; + private final ScheduledExecutorService timeoutScheduler; private TbMailSender mailSender; @@ -112,7 +111,7 @@ public class DefaultMailService implements MailService { this.freemarkerConfig = freemarkerConfig; this.adminSettingsService = adminSettingsService; this.apiUsageClient = apiUsageClient; - this.scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog")); + this.timeoutScheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog")); } @PostConstruct @@ -122,8 +121,8 @@ public class DefaultMailService implements MailService { @PreDestroy public void destroy() { - if (scheduler != null) { - scheduler.shutdownNow(); + if (timeoutScheduler != null) { + timeoutScheduler.shutdownNow(); } } @@ -446,7 +445,7 @@ 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, scheduler); + timeout, TimeUnit.MILLISECONDS, timeoutScheduler); try { submittedMail.get(timeout, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java index 03aea6c7f9..0c8feff6c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java @@ -19,6 +19,10 @@ 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 { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java index 4df152936f..97c20fdcda 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java +++ b/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 From 4daa229d83bba1b0d6a2caab4aa6535fb7038b15 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 18:31:02 +0200 Subject: [PATCH 27/30] MAIL_PER_TENANT_RATE_LIMITS example --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2fc2473851..4c4fad1238 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -164,7 +164,7 @@ 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 + # 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 From 9b5962e447c8b8d2cac5329772891cefaab4efe7 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 18:52:37 +0200 Subject: [PATCH 28/30] DefaultMailService tbMailContextComponent renamed to the ctx --- .../thingsboard/server/service/mail/DefaultMailService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index d68837e1f6..257dfa14b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -90,7 +90,7 @@ public class DefaultMailService implements MailService { private PasswordResetExecutorService passwordResetExecutorService; @Autowired - private TbMailContextComponent tbMailContextComponent; + private TbMailContextComponent ctx; @Autowired private RateLimitService rateLimitService; @@ -131,7 +131,7 @@ public class DefaultMailService implements MailService { 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 { @@ -146,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); From cfab9c2d349a765f37983c0181300de822a54079 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 21:49:46 +0200 Subject: [PATCH 29/30] Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts --- pom.xml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pom.xml b/pom.xml index c06166e497..c3451544a4 100755 --- a/pom.xml +++ b/pom.xml @@ -1476,6 +1476,12 @@ org.apache.httpcomponents httpclient ${apache-httpclient.version} + + + commons-logging + commons-logging + + org.apache.httpcomponents @@ -1943,6 +1949,12 @@ org.elasticsearch.client elasticsearch-rest-client ${elasticsearch.version} + + + commons-logging + commons-logging + + org.javadelight @@ -1968,11 +1980,23 @@ com.amazonaws aws-java-sdk-sqs ${aws.sdk.version} + + + commons-logging + commons-logging + + com.amazonaws aws-java-sdk-sns ${aws.sdk.version} + + + commons-logging + commons-logging + + com.google.cloud @@ -2102,6 +2126,10 @@ org.slf4j * + + commons-logging + commons-logging + com.github.spotbugs spotbugs-annotations @@ -2136,6 +2164,12 @@ mockserver-netty ${mock-server.version} test + + + commons-logging + commons-logging + + org.mock-server @@ -2168,6 +2202,12 @@ com.google.firebase firebase-admin ${firebase-admin.version} + + + commons-logging + commons-logging + + org.eclipse.jgit @@ -2203,6 +2243,12 @@ org.apache.xmlgraphics batik-transcoder ${apache-xmlgraphics.version} + + + commons-logging + commons-logging + + org.apache.xmlgraphics From 994bb6c825607c897cae913d55c8ad8f762c9974 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 7 May 2024 14:01:23 +0300 Subject: [PATCH 30/30] Add search by type for assets where missing --- .../server/dao/sql/asset/AssetRepository.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index eb83a5805e..bf4798fa4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -42,7 +42,8 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + - " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -55,7 +56,7 @@ public interface AssetRepository extends JpaRepository, 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 findAssetInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -63,7 +64,8 @@ public interface AssetRepository extends JpaRepository, Expor @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 " + - " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, @@ -72,7 +74,7 @@ public interface AssetRepository extends JpaRepository, 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 " + - " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true)") Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, @Param("profileId") UUID profileId, @Param("searchText") String searchText, @@ -86,7 +88,8 @@ public interface AssetRepository extends JpaRepository, Expor "AND a.customerId = :customerId " + "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(c.title, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(p.name, CONCAT('%', :searchText, '%')) = true) ") Page findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -101,7 +104,7 @@ public interface AssetRepository extends JpaRepository, 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 " + - " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -198,7 +201,7 @@ public interface AssetRepository extends JpaRepository, Expor "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 " + - " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ") Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type,