Browse Source

Merge remote-tracking branch 'upstream/master' into fix_lwm2m-boostrap-failed-for-BootstrapReadRequest-/1

# Conflicts:
#	ui-ngx/src/app/core/http/device.service.ts
pull/14084/head
Vladyslav_Prykhodko 11 months ago
parent
commit
765f29a855
  1. 4
      application/pom.xml
  2. 13
      application/src/main/data/json/system/widget_types/alarms_table.json
  3. 13
      application/src/main/data/json/system/widget_types/entities_table.json
  4. 15
      application/src/main/data/json/system/widget_types/markdown_html_card.json
  5. 2
      application/src/main/data/upgrade/basic/schema_update.sql
  6. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  7. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java
  8. 50
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  9. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
  10. 110
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  11. 19
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  12. 16
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  13. 14
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  14. 14
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  15. 16
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  16. 27
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  17. 16
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  18. 5
      application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
  19. 17
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  20. 2
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  21. 47
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  22. 50
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  23. 40
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  24. 23
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  25. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  26. 44
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  27. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  28. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  29. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java
  30. 87
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  31. 2
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  32. 7
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  33. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  34. 16
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  35. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  36. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  37. 130
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java
  38. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java
  39. 81
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java
  40. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
  41. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
  42. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
  43. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java
  44. 15
      application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
  45. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java
  46. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
  47. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java
  48. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  49. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  50. 9
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  51. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  52. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  53. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  54. 5
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  55. 4
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java
  56. 1
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  57. 13
      application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java
  58. 24
      application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java
  59. 22
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  60. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java
  61. 49
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  62. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java
  63. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java
  64. 8
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  65. 32
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  66. 22
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  67. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  68. 13
      application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java
  69. 28
      application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java
  70. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  71. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  72. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  73. 19
      application/src/main/java/org/thingsboard/server/utils/CsvUtils.java
  74. 8
      application/src/main/resources/thingsboard.yml
  75. 88
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  76. 24
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  77. 12
      application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java
  78. 24
      application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
  79. 66
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  80. 22
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  81. 81
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  82. 160
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  83. 25
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  84. 23
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  85. 23
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  86. 152
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java
  87. 74
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  88. 196
      application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java
  89. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  90. 23
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  91. 3
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  92. 2
      application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java
  93. 83
      application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java
  94. 125
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  95. 2
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  96. 61
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  97. 92
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java
  98. 92
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java
  99. 39
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java
  100. 11
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java

4
application/pom.xml

@ -419,6 +419,10 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

13
application/src/main/data/json/system/widget_types/alarms_table.json

File diff suppressed because one or more lines are too long

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

@ -11,19 +11,13 @@
"resources": [], "resources": [],
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>", "templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "", "templateCss": "",
"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 supportsUnitConversion: 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 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\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 supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'displayName', 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 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings", "settingsDirective": "tb-entities-table-widget-settings",
"dataKeySettingsDirective": "tb-entities-table-key-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-entities-table-basic-config", "basicModeDirective": "tb-entities-table-basic-config",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}"
}, },
"tags": [
"administration",
"management"
],
"resources": [ "resources": [
{ {
"link": "/api/images/system/entities_table_system_widget_image.png", "link": "/api/images/system/entities_table_system_widget_image.png",
@ -36,5 +30,10 @@
"data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAnhSURBVHja7d3rU5NXHsBx/7KAa8WuBRahaEAIiQRFQSAKXlrUpagYAmgBJbXKin3qKlaKYAOmtFlQLpWkgCgSRC6GiwZFQAi3J/nuC9Da2RGSTNtZmXNeJWfmOfl9Juc2c855zjoWnU8HPvD01LnAukXHpMwHnuRJx+I65yRrIE061z2V1wJEfrpugDWRBgREQAREQAREQATkQ4A4X/sBkXW34IHO7d9PfqXT6XS6Tr9DztXpsv/zP7mJZf5AFEEjNCn8nEZ2WE4HWSxjfkPUxyyXAxv/IEjoAZoUsnxxx24b5ScOaaqMquwFHqSoL3u8KK1mM0h6xrT9WYXalF7mC2MODnsPKYXdxfycFFdOR2qJKkfmB83hqDK601RFc2Sc1e61HlJVeQep/NjSpJDrEvuMERRHtF9UlHZs+nk6+N8PQhu9hLSuf10V4dakdx1RY4zvyU/2HlI43LKp9rmysUXR3xhQ3RpoHQysatlc5gr5qlt9lvAs++6/35M2LHoFuVuxxayQaSw5qJCLM+hRTBBXfjdQklTnvITIYebDxWiu0a14vf2AVBDodUVVb/xEUeKhtzQvoLnxI9hSW7UNEstaAxe5GU14DcZ99CvGvIO4E6MV8g8RTZVvIeprdwIlSWrxEkLRgaBHaK7Ro5iMPiRJ0oIPVSvtCI6NVY8Cmxs/gvCam9GQWGZbL1P1KeE1GHXeQ+hdr5CLdoycVcy/hYxvKnd++8RbSJciCjSZz/O2U6Ad6pR8aSMPA7qa1/fUBzQuQfoC7jwKKZsOufYs0eAbRGmDUqU8unvLKaXj8in6lVNk/IBVG3z4hReh1O8AUH4NmpStqk5mcsKjzV5DMq7DiVNydmhmgskWC4n1XI1IzqigPSHkn69JrOdKNkPK8b9qQBzd1A+aax/8yN4WkMeagMjjYq4lIAIiIAIiIAIiIAIiIALyZ0MG10QSVUtABERABERABERABERABOQDh3TchuYGuN3x62kZ6DeMAy3lUG8wGDrp0J9aYT+Ay2Aoaf1dTleVL7G4/vVFpZupkpNLy5XdBoOhDsau1PgOeZQIGamwy14R0ADkBzhgIiINci5YLCNPtj9sC3//hoDJTdb6/SXv5ox1+QLJPTeYcZMMqUfdCXAj22LpoU9Z3u87RA6enY/WuOZC5IqkDHB9qnbAcWMapPcCLT+Cru39kGBY3OrgSXZWtysXGuvuV/K6+OBVGXtW9uNVY0noozZ3PsRDTQHAVzUAGc1+tZH0tl9yC5vaM6goiRml6ssdDpqz7qeBWvtprgw83z63IoQTNRNRXfaomaQe0jvNeg5dGfvixliUvStqdrVYTJ/XJXe6wxZo+AzgpGrrgVeEHIk/NO075JtvCu+0nJEkKr68+jU7+2McM/Hj99OgdWp+fzXM7bGxMiTvpinFYtllqzo/Fesx62c3u3nZ9f1+i2XHqrttftpbstvOqayquFMA9mFPUcHMhl6KLvoO6Tiocc1pDndSUTChfJhClOPcLmN25A2A6jPIn1WyCmRva0WSJEmDU+rai5j1M1sArqRKkrTqppTQ19j2IdfVSpeXc7qT5kLgXqbvkIXwdMiIWKSigOPRdUQ5Hlut1+O7ZiLnOF1O7iVWgdSq5K74RboWyYxxYNYT20/9d22JMg9W3coR7qTxADC0fRgZVCNUnyDhAZLRj3Ek5TpcT4WKAjpDF4hyAJ1pUKlO3udqCtRqtRfeDwnURmc+hzJV0iEXdxLBrOd+3L7UF3yt2pM5t1osDap0zWM82lQb6NqxqXXxw9jVKUkTf+SAKLu870fl37VOF4A848Vznt82y7kBz1IxM/70WmKKIiACIiACIiACIiACIiAC8kFAxDq7qFoCIiACIiACIiACIiACIiDvJL8OlM+/+bDgPyRmnPH4jp4UAKKUSqW8WH/4KsB0dN1qZU2FaGOyfneAtD7fh1AcOp1ubyyt0QlZCwDlqp0Zs0D1Nj8gW164Ekx0awEWIgFqC7MlgNzIVdftJ4PBpJHB9e4C7tK5xJk5rzDVRs+2QU5WAcPRC2RVg1PziT+QZ/svswwZ3WVrdwOlEmDLzPMKwl4bl7QJ52fD5zh/zazHFpuhdXJ+Z7w3Lwpwq52z/4DG48DkY8irgc+b/YKkR8hvID0RZ5KOLkNmtU4vIfk37Qmye8dQTgOqF2a9RzmA6XzHHo8c+3z1aOqPg3KIsuVl3L74WX7KnfULcvLYuTcQQI4cXoKcvYmXkOOmGxE6Xdhd2xf9+zHrJ8MAvo3U6UKtq0ezuwcsW1N1ZwAY1zxmYsdr/yAvZmLuLEPsfUsll0rMRWu1IVvrvIAsRA7U5kxNTS26VedrMevlYDdz45X5U1NTq/dgHakAslx4C2Am8R7cjNHGB+7yB8KTiJHuGLvd/qptz0jztvnlNoI3/8jH9ub0LxmPbB06PYUxxIVZzxHJmVPpjPz1af7qq7MHmwDaL+ycp2l8Mc1ot/cDfv0jJdPQcHPUYDAY2rEczR0EGpd2HZlX3YLhMhiKmoD+3EwLOK5DVy2ui0eroVefeXfVWFzFHsBTZJoFaXDcYDAYSoHFIjGyC4iACIiACIiACIiACIiACMj/OUSss4uqJSACIiACIiACIiACIiAfOMQ6Bcz+eOsl4G7zqaxFi6W+782Xh8McAOCY65mXr5L3NFf2APS9Ws6YvMOoxWKx2PyAvNpwBRa0pd8px7msCvQJMhkkXdr55uxoXScbAQib7jZ59/zJE7diG/hp18Y3h8ezg+mTJCnzlB+Q8jNxMHIRjluwzHzkGyQY+mO4P4irjvuDbISe8s6w6aF2fh6+UedBNlU+6Xjv4/NaN7U5tIwlL0OaMoMBSO31A6IZzegEGIwbAnyEbB52nMunwIQzigITG7Fvu1UQOG3WE3uwZreJ7OxbcXkrFlFYASxDZuKfBQN07fOjjTxKoU4PnFFmy75D/nb0s6iGdyEFlRA6bdYT68BUMB0iY1oRYkuWf4Pk354NBjjS7AckP9l4ZpML8GT+6DskGCY/WXgHktkMYdNmPbGj1BqG1awM6Y1z8hbSEWm5HWQBh9rjO2QurMVq/dzUWwLnKvyCzATNF1fgWIaUXEUOfguZC3Xx/QqQUc0gv0HuS1JZ0BXIq/aj+zUfA2zJ84n5xrhx3yEbDLlxl7inNO5dhowqiw8HvYVwYU9x1AoQVbrRaHQvQQzdwGwwjEfO+wEZeA64bR75119cADafILLV2vES6G9+1cngGDaYaHZ2yi8H6JzjxQDuh7arKxyyt1mtVqsHumdgYA6Q2+DlE/yA/Mkp52KVsvdPKvsvhcw3Vo2IuZaACIiACIiACIiAeANZMxcEr40rmyec6xbWxiXa8rq1ca25zH8BTrZIsxZexqkAAAAASUVORK5CYII=", "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAnhSURBVHja7d3rU5NXHsBx/7KAa8WuBRahaEAIiQRFQSAKXlrUpagYAmgBJbXKin3qKlaKYAOmtFlQLpWkgCgSRC6GiwZFQAi3J/nuC9Da2RGSTNtZmXNeJWfmOfl9Juc2c855zjoWnU8HPvD01LnAukXHpMwHnuRJx+I65yRrIE061z2V1wJEfrpugDWRBgREQAREQAREQATkQ4A4X/sBkXW34IHO7d9PfqXT6XS6Tr9DztXpsv/zP7mJZf5AFEEjNCn8nEZ2WE4HWSxjfkPUxyyXAxv/IEjoAZoUsnxxx24b5ScOaaqMquwFHqSoL3u8KK1mM0h6xrT9WYXalF7mC2MODnsPKYXdxfycFFdOR2qJKkfmB83hqDK601RFc2Sc1e61HlJVeQep/NjSpJDrEvuMERRHtF9UlHZs+nk6+N8PQhu9hLSuf10V4dakdx1RY4zvyU/2HlI43LKp9rmysUXR3xhQ3RpoHQysatlc5gr5qlt9lvAs++6/35M2LHoFuVuxxayQaSw5qJCLM+hRTBBXfjdQklTnvITIYebDxWiu0a14vf2AVBDodUVVb/xEUeKhtzQvoLnxI9hSW7UNEstaAxe5GU14DcZ99CvGvIO4E6MV8g8RTZVvIeprdwIlSWrxEkLRgaBHaK7Ro5iMPiRJ0oIPVSvtCI6NVY8Cmxs/gvCam9GQWGZbL1P1KeE1GHXeQ+hdr5CLdoycVcy/hYxvKnd++8RbSJciCjSZz/O2U6Ad6pR8aSMPA7qa1/fUBzQuQfoC7jwKKZsOufYs0eAbRGmDUqU8unvLKaXj8in6lVNk/IBVG3z4hReh1O8AUH4NmpStqk5mcsKjzV5DMq7DiVNydmhmgskWC4n1XI1IzqigPSHkn69JrOdKNkPK8b9qQBzd1A+aax/8yN4WkMeagMjjYq4lIAIiIAIiIAIiIAIiIALyZ0MG10QSVUtABERABERABERABERABOQDh3TchuYGuN3x62kZ6DeMAy3lUG8wGDrp0J9aYT+Ay2Aoaf1dTleVL7G4/vVFpZupkpNLy5XdBoOhDsau1PgOeZQIGamwy14R0ADkBzhgIiINci5YLCNPtj9sC3//hoDJTdb6/SXv5ox1+QLJPTeYcZMMqUfdCXAj22LpoU9Z3u87RA6enY/WuOZC5IqkDHB9qnbAcWMapPcCLT+Cru39kGBY3OrgSXZWtysXGuvuV/K6+OBVGXtW9uNVY0noozZ3PsRDTQHAVzUAGc1+tZH0tl9yC5vaM6goiRml6ssdDpqz7qeBWvtprgw83z63IoQTNRNRXfaomaQe0jvNeg5dGfvixliUvStqdrVYTJ/XJXe6wxZo+AzgpGrrgVeEHIk/NO075JtvCu+0nJEkKr68+jU7+2McM/Hj99OgdWp+fzXM7bGxMiTvpinFYtllqzo/Fesx62c3u3nZ9f1+i2XHqrttftpbstvOqayquFMA9mFPUcHMhl6KLvoO6Tiocc1pDndSUTChfJhClOPcLmN25A2A6jPIn1WyCmRva0WSJEmDU+rai5j1M1sArqRKkrTqppTQ19j2IdfVSpeXc7qT5kLgXqbvkIXwdMiIWKSigOPRdUQ5Hlut1+O7ZiLnOF1O7iVWgdSq5K74RboWyYxxYNYT20/9d22JMg9W3coR7qTxADC0fRgZVCNUnyDhAZLRj3Ek5TpcT4WKAjpDF4hyAJ1pUKlO3udqCtRqtRfeDwnURmc+hzJV0iEXdxLBrOd+3L7UF3yt2pM5t1osDap0zWM82lQb6NqxqXXxw9jVKUkTf+SAKLu870fl37VOF4A848Vznt82y7kBz1IxM/70WmKKIiACIiACIiACIiACIiAC8kFAxDq7qFoCIiACIiACIiACIiACIiDvJL8OlM+/+bDgPyRmnPH4jp4UAKKUSqW8WH/4KsB0dN1qZU2FaGOyfneAtD7fh1AcOp1ubyyt0QlZCwDlqp0Zs0D1Nj8gW164Ekx0awEWIgFqC7MlgNzIVdftJ4PBpJHB9e4C7tK5xJk5rzDVRs+2QU5WAcPRC2RVg1PziT+QZ/svswwZ3WVrdwOlEmDLzPMKwl4bl7QJ52fD5zh/zazHFpuhdXJ+Z7w3Lwpwq52z/4DG48DkY8irgc+b/YKkR8hvID0RZ5KOLkNmtU4vIfk37Qmye8dQTgOqF2a9RzmA6XzHHo8c+3z1aOqPg3KIsuVl3L74WX7KnfULcvLYuTcQQI4cXoKcvYmXkOOmGxE6Xdhd2xf9+zHrJ8MAvo3U6UKtq0ezuwcsW1N1ZwAY1zxmYsdr/yAvZmLuLEPsfUsll0rMRWu1IVvrvIAsRA7U5kxNTS26VedrMevlYDdz45X5U1NTq/dgHakAslx4C2Am8R7cjNHGB+7yB8KTiJHuGLvd/qptz0jztvnlNoI3/8jH9ub0LxmPbB06PYUxxIVZzxHJmVPpjPz1af7qq7MHmwDaL+ycp2l8Mc1ot/cDfv0jJdPQcHPUYDAY2rEczR0EGpd2HZlX3YLhMhiKmoD+3EwLOK5DVy2ui0eroVefeXfVWFzFHsBTZJoFaXDcYDAYSoHFIjGyC4iACIiACIiACIiACIiACMj/OUSss4uqJSACIiACIiACIiACIiAfOMQ6Bcz+eOsl4G7zqaxFi6W+782Xh8McAOCY65mXr5L3NFf2APS9Ws6YvMOoxWKx2PyAvNpwBRa0pd8px7msCvQJMhkkXdr55uxoXScbAQib7jZ59/zJE7diG/hp18Y3h8ezg+mTJCnzlB+Q8jNxMHIRjluwzHzkGyQY+mO4P4irjvuDbISe8s6w6aF2fh6+UedBNlU+6Xjv4/NaN7U5tIwlL0OaMoMBSO31A6IZzegEGIwbAnyEbB52nMunwIQzigITG7Fvu1UQOG3WE3uwZreJ7OxbcXkrFlFYASxDZuKfBQN07fOjjTxKoU4PnFFmy75D/nb0s6iGdyEFlRA6bdYT68BUMB0iY1oRYkuWf4Pk354NBjjS7AckP9l4ZpML8GT+6DskGCY/WXgHktkMYdNmPbGj1BqG1awM6Y1z8hbSEWm5HWQBh9rjO2QurMVq/dzUWwLnKvyCzATNF1fgWIaUXEUOfguZC3Xx/QqQUc0gv0HuS1JZ0BXIq/aj+zUfA2zJ84n5xrhx3yEbDLlxl7inNO5dhowqiw8HvYVwYU9x1AoQVbrRaHQvQQzdwGwwjEfO+wEZeA64bR75119cADafILLV2vES6G9+1cngGDaYaHZ2yi8H6JzjxQDuh7arKxyyt1mtVqsHumdgYA6Q2+DlE/yA/Mkp52KVsvdPKvsvhcw3Vo2IuZaACIiACIiACIiAeANZMxcEr40rmyec6xbWxiXa8rq1ca25zH8BTrZIsxZexqkAAAAASUVORK5CYII=",
"public": true "public": true
} }
],
"scada": false,
"tags": [
"administration",
"management"
] ]
} }

15
application/src/main/data/json/system/widget_types/markdown_html_card.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/upgrade/basic/schema_update.sql

@ -27,7 +27,7 @@ SET profile_data = jsonb_set(
CASE CASE
WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF'
THEN NULL THEN NULL
ELSE to_jsonb(3600) ELSE to_jsonb(60)
END, END,
'maxRelationLevelPerCfArgument', 'maxRelationLevelPerCfArgument',
CASE CASE

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

@ -97,6 +97,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeStateService; import org.thingsboard.server.dao.rule.RuleNodeStateService;
@ -511,6 +512,10 @@ public class ActorSystemContext {
@Getter @Getter
private ResourceService resourceService; private ResourceService resourceService;
@Autowired
@Getter
private TbResourceDataCache resourceDataCache;
@Lazy @Lazy
@Autowired(required = false) @Autowired(required = false)
@Getter @Getter

3
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java

@ -75,9 +75,6 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG:
processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg);
break; break;
case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
default: default:
return false; return false;
} }

50
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -49,6 +49,7 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -227,18 +228,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
} }
public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException {
log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId());
CalculatedFieldState currentState = states.get(msg.getCfId());
if (currentState == null) {
log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId());
} else {
currentState.setDirty(true);
log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId());
}
msg.getCallback().onSuccess();
}
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
} }
@ -266,12 +255,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (state == null) { if (state == null) {
state = getOrInitState(ctx); state = getOrInitState(ctx);
justRestored = true; justRestored = true;
} else if (state.isDirty()) { } else if (ctx.shouldFetchDynamicArgumentsFromDb(state)) {
log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId()); log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId());
try { try {
Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId);
dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); dynamicArgsFromDb.forEach(newArgValues::putIfAbsent);
state.setDirty(false); var geofencingState = (GeofencingCalculatedFieldState) state;
geofencingState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
} catch (Exception e) { } catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
} }
@ -403,7 +393,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList); return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList);
} }
private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geoArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) { private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geofencingArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
Map<String, ArgumentEntry> arguments = new HashMap<>(); Map<String, ArgumentEntry> arguments = new HashMap<>();
for (AttributeValueProto item : attrDataList) { for (AttributeValueProto item : attrDataList) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
@ -411,7 +401,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (argName == null) { if (argName == null) {
continue; continue;
} }
if (geoArgNames.contains(argName)) { if (geofencingArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry(entityId, item)); arguments.put(argName, new GeofencingArgumentEntry(entityId, item));
continue; continue;
} }
@ -425,26 +415,32 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (argNames.isEmpty()) { if (argNames.isEmpty()) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys); List<String> geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames();
return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), geofencingArgumentNames, scope, removedAttrKeys);
} }
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<String> removedAttrKeys) { private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<String> removedAttrKeys) {
return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys); return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, removedAttrKeys);
} }
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, AttributeScopeProto scope, List<String> removedAttrKeys) { private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, List<String> geofencingArgNames, AttributeScopeProto scope, List<String> removedAttrKeys) {
Map<String, ArgumentEntry> arguments = new HashMap<>(); Map<String, ArgumentEntry> arguments = new HashMap<>();
for (String removedKey : removedAttrKeys) { for (String removedKey : removedAttrKeys) {
ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
String argName = argNames.get(key); String argName = argNames.get(key);
if (argName != null) { if (argName == null) {
Argument argument = configArguments.get(argName); continue;
String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
: new SingleValueArgumentEntry());
} }
if (geofencingArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry());
continue;
}
Argument argument = configArguments.get(argName);
String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
: new SingleValueArgumentEntry());
} }
return arguments; return arguments;
} }

3
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java

@ -79,9 +79,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG:
processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg);
break; break;
case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
default: default:
return false; return false;
} }

110
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -57,10 +56,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
@ -74,7 +70,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private final Map<CalculatedFieldId, CalculatedFieldCtx> calculatedFields = new HashMap<>(); private final Map<CalculatedFieldId, CalculatedFieldCtx> calculatedFields = new HashMap<>();
private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new HashMap<>(); private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new HashMap<>();
private final Map<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new HashMap<>(); private final Map<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new HashMap<>();
private final Map<CalculatedFieldId, ScheduledFuture<?>> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>();
private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldProcessingService cfExecService;
private final CalculatedFieldStateService cfStateService; private final CalculatedFieldStateService cfStateService;
@ -113,8 +108,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
calculatedFields.clear(); calculatedFields.clear();
entityIdCalculatedFields.clear(); entityIdCalculatedFields.clear();
entityIdCalculatedFieldLinks.clear(); entityIdCalculatedFieldLinks.clear();
cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true));
cfDynamicArgumentsRefreshTasks.clear();
ctx.stop(ctx.getSelf()); ctx.stop(ctx.getSelf());
} }
@ -274,7 +267,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
addLinks(cf); addLinks(cf);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb)); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb));
} }
} }
@ -300,36 +292,29 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
newCfCtx.init(); newCfCtx.init();
} catch (Exception e) { } catch (Exception e) {
throw CalculatedFieldException.builder().ctx(newCfCtx).eventEntity(newCfCtx.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build(); throw CalculatedFieldException.builder().ctx(newCfCtx).eventEntity(newCfCtx.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
} } finally {
calculatedFields.put(newCf.getId(), newCfCtx); calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx); boolean found = false;
if (hasSchedulingConfigChanges) { for (CalculatedFieldCtx oldCtx : oldCfList) {
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false); if (oldCtx.getCfId().equals(newCf.getId())) {
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx); newCfList.add(newCfCtx);
} found = true;
} else {
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>(); newCfList.add(oldCtx);
boolean found = false; }
for (CalculatedFieldCtx oldCtx : oldCfList) { }
if (oldCtx.getCfId().equals(newCf.getId())) { if (!found) {
newCfList.add(newCfCtx); newCfList.add(newCfCtx);
found = true;
} else {
newCfList.add(oldCtx);
} }
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.put(newCf.getEntityId(), newCfList);
deleteLinks(oldCfCtx);
addLinks(newCf);
} }
if (!found) {
newCfList.add(newCfCtx);
}
entityIdCalculatedFields.put(newCf.getEntityId(), newCfList);
deleteLinks(oldCfCtx);
addLinks(newCf);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); var stateChanges = newCfCtx.hasStateChanges(oldCfCtx);
if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) {
applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb)); applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb));
@ -350,19 +335,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
deleteLinks(cfCtx); deleteLinks(cfCtx);
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb));
} }
private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) {
var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId);
if (existingTask != null) {
existingTask.cancel(false);
String reason = cfDeleted ? "deletion" : "update";
log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason);
}
}
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {
EntityId entityId = msg.getEntityId(); EntityId entityId = msg.getEntityId();
log.debug("Received telemetry msg from entity [{}]", entityId); log.debug("Received telemetry msg from entity [{}]", entityId);
@ -442,43 +417,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return result; return result;
} }
private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) {
CalculatedField cf = cfCtx.getCalculatedField();
if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) {
return;
}
if (!scheduledCfConfig.isScheduledUpdateEnabled()) {
return;
}
if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) {
log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId());
return;
}
long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateInterval());
var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId());
ScheduledFuture<?> scheduledFuture = systemContext
.schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval);
cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture);
log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId());
}
public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) {
log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId());
CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId());
if (cfCtx == null) {
log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId());
cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true);
return;
}
applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb));
}
private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) {
log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback));
}
private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) {
log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(msg); getOrCreateActor(entityId).tell(msg);
@ -560,12 +498,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfCtx.init(); cfCtx.init();
} catch (Exception e) { } catch (Exception e) {
throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build(); throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
} finally {
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
} }
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
} }
private void initCalculatedFieldLink(CalculatedFieldLink link) { private void initCalculatedFieldLink(CalculatedFieldLink link) {

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

@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasRuleEngineProfile; import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -110,6 +112,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
@ -770,6 +773,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getResourceService(); return mainCtx.getResourceService();
} }
@Override
public TbResourceDataCache getTbResourceDataCache() {
return mainCtx.getResourceDataCache();
}
@Override @Override
public OtaPackageService getOtaPackageService() { public OtaPackageService getOtaPackageService() {
return mainCtx.getOtaPackageService(); return mainCtx.getOtaPackageService();
@ -1054,11 +1062,20 @@ public class DefaultTbContext implements TbContext {
@Override @Override
public void checkTenantEntity(EntityId entityId) throws TbNodeException { public void checkTenantEntity(EntityId entityId) throws TbNodeException {
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId);
if (!getTenantId().equals(actualTenantId)) {
throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true); throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true);
} }
} }
@Override
public <E extends HasId<I> & HasTenantId, I extends EntityId> void checkTenantOrSystemEntity(E entity) throws TbNodeException {
TenantId actualTenantId = entity.getTenantId();
if (!getTenantId().equals(actualTenantId) && !actualTenantId.isSysTenantId()) {
throw new TbNodeException("Entity with id: '" + entity.getId() + "' specified in the configuration doesn't belong to the current or system tenant.", true);
}
}
private static String getFailureMessage(Throwable th) { private static String getFailureMessage(Throwable th) {
String failureMessage; String failureMessage;
if (th != null) { if (th != null) {

16
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -34,6 +34,9 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.asset.AssetSearchQuery;
@ -76,6 +79,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -83,6 +88,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@ -137,10 +143,16 @@ public class AssetController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/asset", method = RequestMethod.POST) @RequestMapping(value = "/asset", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception { public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
asset.setTenantId(getTenantId()); asset.setTenantId(getTenantId());
checkEntity(asset.getId(), asset, Resource.ASSET); checkEntity(asset.getId(), asset, Resource.ASSET);
return tbAssetService.save(asset, getCurrentUser()); return tbAssetService.save(asset, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Delete asset (deleteAsset)", @ApiOperation(value = "Delete asset (deleteAsset)",

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

@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
@ -48,7 +49,9 @@ import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.auth.rest.RestAwareAuthenticationSuccessHandler;
import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ActivateUserRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
@ -74,7 +77,8 @@ public class AuthController extends BaseController {
private final SecuritySettingsService securitySettingsService; private final SecuritySettingsService securitySettingsService;
private final RateLimitService rateLimitService; private final RateLimitService rateLimitService;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final TwoFactorAuthService twoFactorAuthService;
private final RestAwareAuthenticationSuccessHandler authenticationSuccessHandler;
@ApiOperation(value = "Get current User (getUser)", @ApiOperation(value = "Get current User (getUser)",
notes = "Get the information about the User which credentials are used to perform this REST API call.") notes = "Get the information about the User which credentials are used to perform this REST API call.")
@ -221,7 +225,13 @@ public class AuthController extends BaseController {
} }
} }
var tokenPair = tokenFactory.createTokenPair(securityUser); JwtPair tokenPair;
if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId(), user)) {
tokenPair = authenticationSuccessHandler.createMfaTokenPair(securityUser, Authority.MFA_CONFIGURATION_TOKEN);
} else {
tokenPair = tokenFactory.createTokenPair(securityUser);
}
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGIN, null); systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGIN, null);
return tokenPair; return tokenPair;
} }

14
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -1744,4 +1744,18 @@ public class ControllerConstants {
MARKDOWN_CODE_BLOCK_END ; MARKDOWN_CODE_BLOCK_END ;
protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities)."; protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities).";
public static final String NAME_CONFLICT_POLICY_DESC = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
" If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
" UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.";
public static final String UNIQUIFY_SEPARATOR_DESC = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
"For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for entity name 'test-name', " +
"created entity will have name like 'test-name-7fsh4f'.";
public static final String UNIQUIFY_STRATEGY_DESC = "Optional value of uniquify strategy used by UNIQUIFY policy. Possible values: RANDOM or INCREMENTAL. " +
"By default, RANDOM strategy is used, which means random alphanumeric string will be added as a suffix to entity name. " +
"INCREMENTAL implies the first possible number starting from 1 will be added as a name suffix. " +
"For example, strategy is UNIQUIFY, uniquify strategy is INCREMENTAL; if a name conflict occurs for entity name 'test-name', " +
"created entity will have name like 'test-name-1.";
} }

16
application/src/main/java/org/thingsboard/server/controller/CustomerController.java

@ -32,6 +32,9 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -47,6 +50,8 @@ import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -54,6 +59,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController @RestController
@ -128,10 +134,16 @@ public class CustomerController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer", method = RequestMethod.POST) @RequestMapping(value = "/customer", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer) throws Exception { public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
customer.setTenantId(getTenantId()); customer.setTenantId(getTenantId());
checkEntity(customer.getId(), customer, Resource.CUSTOMER); checkEntity(customer.getId(), customer, Resource.CUSTOMER);
return tbCustomerService.save(customer, getCurrentUser()); return tbCustomerService.save(customer, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Delete Customer (deleteCustomer)", @ApiOperation(value = "Delete Customer (deleteCustomer)",

27
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -46,8 +46,11 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -108,6 +111,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -117,6 +122,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID; import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@ -177,14 +183,21 @@ public class DeviceController extends BaseController {
@ResponseBody @ResponseBody
public Device saveDevice(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the device.") @RequestBody Device device, public Device saveDevice(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the device.") @RequestBody Device device,
@Parameter(description = "Optional value of the device credentials to be used during device creation. " + @Parameter(description = "Optional value of the device credentials to be used during device creation. " +
"If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws Exception { "If omitted, access token will be auto-generated.")
@RequestParam(name = "accessToken", required = false) String accessToken,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
device.setTenantId(getCurrentUser().getTenantId()); device.setTenantId(getCurrentUser().getTenantId());
if (device.getId() != null) { if (device.getId() != null) {
checkDeviceId(device.getId(), Operation.WRITE); checkDeviceId(device.getId(), Operation.WRITE);
} else { } else {
checkEntity(null, device, Resource.DEVICE); checkEntity(null, device, Resource.DEVICE);
} }
return tbDeviceService.save(device, accessToken, getCurrentUser()); return tbDeviceService.save(device, accessToken, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Create Device (saveDevice) with credentials ", @ApiOperation(value = "Create Device (saveDevice) with credentials ",
@ -209,12 +222,18 @@ public class DeviceController extends BaseController {
@RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST) @RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.") public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.")
@Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws ThingsboardException {
Device device = deviceAndCredentials.getDevice(); Device device = deviceAndCredentials.getDevice();
DeviceCredentials credentials = deviceAndCredentials.getCredentials(); DeviceCredentials credentials = deviceAndCredentials.getCredentials();
device.setTenantId(getCurrentUser().getTenantId()); device.setTenantId(getCurrentUser().getTenantId());
checkEntity(device.getId(), device, Resource.DEVICE); checkEntity(device.getId(), device, Resource.DEVICE);
return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser()); return tbDeviceService.saveDeviceWithCredentials(device, credentials, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Delete device (deleteDevice)", @ApiOperation(value = "Delete device (deleteDevice)",

16
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -34,6 +34,9 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -69,6 +72,8 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -76,6 +81,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
/** /**
@ -128,7 +134,13 @@ public class EntityViewController extends BaseController {
@ResponseBody @ResponseBody
public EntityView saveEntityView( public EntityView saveEntityView(
@Parameter(description = "A JSON object representing the entity view.") @Parameter(description = "A JSON object representing the entity view.")
@RequestBody EntityView entityView) throws Exception { @RequestBody EntityView entityView,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
entityView.setTenantId(getCurrentUser().getTenantId()); entityView.setTenantId(getCurrentUser().getTenantId());
EntityView existingEntityView = null; EntityView existingEntityView = null;
if (entityView.getId() == null) { if (entityView.getId() == null) {
@ -137,7 +149,7 @@ public class EntityViewController extends BaseController {
} else { } else {
existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE); existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE);
} }
return tbEntityViewService.save(entityView, existingEntityView, getCurrentUser()); return tbEntityViewService.save(entityView, existingEntityView, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Delete entity view (deleteEntityView)", @ApiOperation(value = "Delete entity view (deleteEntityView)",

5
application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java

@ -27,6 +27,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -37,6 +39,7 @@ import org.thingsboard.server.service.lwm2m.LwM2MService;
import java.util.Map; import java.util.Map;
import static org.thingsboard.server.common.data.NameConflictStrategy.DEFAULT;
import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@ -73,6 +76,6 @@ public class Lwm2mController extends BaseController {
public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException { public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class));
DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials)); return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy());
} }
} }

17
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -55,14 +55,17 @@ import org.thingsboard.server.common.data.util.ThrowingSupplier;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
@ -263,6 +266,20 @@ public class TbResourceController extends BaseController {
} }
} }
@ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource", params = {"resourceIds"})
public List<TbResourceInfo> getSystemOrTenantResourcesByIds(
@Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException {
SecurityUser user = getCurrentUser();
List<TbResourceId> resourceIds = new ArrayList<>();
for (UUID resourceId : resourceUuids) {
resourceIds.add(new TbResourceId(resourceId));
}
return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds);
}
@ApiOperation(value = "Get All Resource Infos (getAllResources)", @ApiOperation(value = "Get All Resource Infos (getAllResources)",
notes = "Returns a page of Resource Info objects owned by tenant. " + notes = "Returns a page of Resource Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)

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

@ -115,7 +115,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Delete Tenant (deleteTenant)", @ApiOperation(value = "Delete Tenant (deleteTenant)",
notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE) @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteTenant(@Parameter(description = TENANT_ID_PARAM_DESCRIPTION) public void deleteTenant(@Parameter(description = TENANT_ID_PARAM_DESCRIPTION)

47
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java

@ -56,7 +56,6 @@ public class TwoFactorAuthConfigController extends BaseController {
private final TwoFaConfigManager twoFaConfigManager; private final TwoFaConfigManager twoFaConfigManager;
private final TwoFactorAuthService twoFactorAuthService; private final TwoFactorAuthService twoFactorAuthService;
@ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)", @ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)",
notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE + notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE +
"Example:\n" + "Example:\n" +
@ -67,13 +66,12 @@ public class TwoFactorAuthConfigController extends BaseController {
" }\n}\n```" + " }\n}\n```" +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@GetMapping("/account/settings") @GetMapping("/account/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException { public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()).orElse(null); return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user).orElse(null);
} }
@ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)", @ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)",
notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE + notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE +
"For TOTP, this will return a corresponding account config template " + "For TOTP, this will return a corresponding account config template " +
@ -99,7 +97,7 @@ public class TwoFactorAuthConfigController extends BaseController {
"Will throw an error (Bad Request) if the provider is not configured for usage. " + "Will throw an error (Bad Request) if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config/generate") @PostMapping("/account/config/generate")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", requiredMode = Schema.RequiredMode.REQUIRED)) public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", requiredMode = Schema.RequiredMode.REQUIRED))
@RequestParam TwoFaProviderType providerType) throws Exception { @RequestParam TwoFaProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -127,7 +125,7 @@ public class TwoFactorAuthConfigController extends BaseController {
"or if the provider is not configured for usage. " + "or if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config/submit") @PostMapping("/account/config/submit")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception { public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
@ -139,11 +137,11 @@ public class TwoFactorAuthConfigController extends BaseController {
"Will throw an error (Bad Request) if the provider is not configured for usage. " + "Will throw an error (Bad Request) if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config") @PostMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig, public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig,
@RequestParam(required = false) String verificationCode) throws Exception { @RequestParam(required = false) String verificationCode) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig.getProviderType()).isPresent()) { if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user, accountConfig.getProviderType()).isPresent()) {
throw new IllegalArgumentException("2FA provider is already configured"); throw new IllegalArgumentException("2FA provider is already configured");
} }
@ -154,7 +152,7 @@ public class TwoFactorAuthConfigController extends BaseController {
verificationSuccess = true; verificationSuccess = true;
} }
if (verificationSuccess) { if (verificationSuccess) {
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user, accountConfig);
} else { } else {
throw new IllegalArgumentException("Verification code is incorrect"); throw new IllegalArgumentException("Verification code is incorrect");
} }
@ -162,42 +160,41 @@ public class TwoFactorAuthConfigController extends BaseController {
@ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes = @ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes =
"Update config for a given provider type. \n" + "Update config for a given provider type. \n" +
"Update request example:\n" + "Update request example:\n" +
"```\n{\n \"useByDefault\": true\n}\n```\n" + "```\n{\n \"useByDefault\": true\n}\n```\n" +
"Returns whole account's 2FA settings object.\n" + "Returns whole account's 2FA settings object.\n" +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/account/config") @PutMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType, public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType,
@RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException { @RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType)
.orElseThrow(() -> new IllegalArgumentException("Config for " + providerType + " 2FA provider not found")); .orElseThrow(() -> new IllegalArgumentException("Config for " + providerType + " 2FA provider not found"));
accountConfig.setUseByDefault(updateRequest.isUseByDefault()); accountConfig.setUseByDefault(updateRequest.isUseByDefault());
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user, accountConfig);
} }
@ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes = @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes =
"Delete 2FA config for a given 2FA provider type. \n" + "Delete 2FA config for a given 2FA provider type. \n" +
"Returns whole account's 2FA settings object.\n" + "Returns whole account's 2FA settings object.\n" +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@DeleteMapping("/account/config") @DeleteMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException { public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType); return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user, providerType);
} }
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
"Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" + "Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" +
"Example of response:\n" + "Example of response:\n" +
"```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + "```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER
) )
@GetMapping("/providers") @GetMapping("/providers")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException { public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true) return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true)
.map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream() .map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream()
@ -205,7 +202,6 @@ public class TwoFactorAuthConfigController extends BaseController {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)", @ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)",
notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " + notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " +
"If 2FA is not configured, then an empty response will be returned." + "If 2FA is not configured, then an empty response will be returned." +
@ -260,11 +256,10 @@ public class TwoFactorAuthConfigController extends BaseController {
@PostMapping("/settings") @PostMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
public PlatformTwoFaSettings savePlatformTwoFaSettings(@Parameter(description = "Settings value", required = true) public PlatformTwoFaSettings savePlatformTwoFaSettings(@Parameter(description = "Settings value", required = true)
@RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException { @RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException {
return twoFaConfigManager.savePlatformTwoFaSettings(getTenantId(), twoFaSettings); return twoFaConfigManager.savePlatformTwoFaSettings(getTenantId(), twoFaSettings);
} }
@Data @Data
public static class TwoFaAccountConfigUpdateRequest { public static class TwoFaAccountConfigUpdateRequest {
private boolean useByDefault; private boolean useByDefault;

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

@ -28,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
@ -64,7 +63,6 @@ public class TwoFactorAuthController extends BaseController {
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final UserService userService; private final UserService userService;
@ApiOperation(value = "Request 2FA verification code (requestTwoFaVerificationCode)", @ApiOperation(value = "Request 2FA verification code (requestTwoFaVerificationCode)",
notes = "Request 2FA verification code." + NEW_LINE + notes = "Request 2FA verification code." + NEW_LINE +
"To make a request to this endpoint, you need an access token with the scope of PRE_VERIFICATION_TOKEN, " + "To make a request to this endpoint, you need an access token with the scope of PRE_VERIFICATION_TOKEN, " +
@ -92,30 +90,28 @@ public class TwoFactorAuthController extends BaseController {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
if (verificationSuccess) { if (verificationSuccess) {
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); logLogInAction(servletRequest, user, null);
user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); return createTokenPair(user);
return tokenFactory.createTokenPair(user);
} else { } else {
ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS); IllegalArgumentException error = new IllegalArgumentException("Verification code is incorrect");
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); logLogInAction(servletRequest, user, error);
throw error; throw error;
} }
} }
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
"Get the list of 2FA provider infos available for user to use. Example:\n" + "Get the list of 2FA provider infos available for user to use. Example:\n" +
"```\n[\n" + "```\n[\n" +
" {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + " {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" +
" {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" + " {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" +
" {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" + " {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" +
"]\n```") "]\n```")
@GetMapping("/providers") @GetMapping("/providers")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException { public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
Optional<PlatformTwoFaSettings> platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true); Optional<PlatformTwoFaSettings> platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true);
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()) return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user)
.map(settings -> settings.getConfigs().values()).orElse(Collections.emptyList()) .map(settings -> settings.getConfigs().values()).orElse(Collections.emptyList())
.stream().map(config -> { .stream().map(config -> {
String contact = null; String contact = null;
@ -139,6 +135,32 @@ public class TwoFactorAuthController extends BaseController {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ApiOperation(value = "Get regular token pair after successfully configuring 2FA",
notes = "Checks 2FA is configured, returning token pair on success.")
@PostMapping("/login")
@PreAuthorize("hasAuthority('MFA_CONFIGURATION_TOKEN')")
public JwtPair authenticateByTwoFaConfigurationToken(HttpServletRequest servletRequest) throws ThingsboardException {
SecurityUser user = getCurrentUser();
if (twoFactorAuthService.isTwoFaEnabled(user.getTenantId(), user)) {
logLogInAction(servletRequest, user, null);
return createTokenPair(user);
} else {
IllegalArgumentException error = new IllegalArgumentException("2FA is not configured");
logLogInAction(servletRequest, user, error);
throw error;
}
}
private JwtPair createTokenPair(SecurityUser user) {
log.debug("[{}][{}] Creating token pair for user", user.getTenantId(), user.getId());
user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal());
return tokenFactory.createTokenPair(user);
}
private void logLogInAction(HttpServletRequest servletRequest, SecurityUser user, Exception error) {
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error);
}
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder

40
application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java

@ -32,8 +32,10 @@ import dev.langchain4j.model.chat.request.ChatRequestParameters;
import dev.langchain4j.model.github.GitHubModelsChatModel; import dev.langchain4j.model.github.GitHubModelsChatModel;
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
import dev.langchain4j.model.mistralai.MistralAiChatModel; import dev.langchain4j.model.mistralai.MistralAiChatModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
@ -43,10 +45,12 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelC
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.Region;
@ -54,7 +58,11 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.Base64;
import static java.util.Collections.singletonMap;
@Component @Component
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@ -62,6 +70,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override @Override
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) { public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
return OpenAiChatModel.builder() return OpenAiChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.apiKey(chatModelConfig.providerConfig().apiKey()) .apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId()) .modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature()) .temperature(chatModelConfig.temperature())
@ -134,7 +143,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// set request timeout from model config // set request timeout from model config
if (chatModelConfig.timeoutSeconds() != null) { if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds())); retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
} }
// set updated retry settings // set updated retry settings
@ -262,6 +271,35 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build(); .build();
} }
@Override
public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) {
var builder = OllamaChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.numCtx(chatModelConfig.contextLength())
.numPredict(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries());
var auth = chatModelConfig.providerConfig().auth();
if (auth instanceof OllamaProviderConfig.OllamaAuth.Basic basicAuth) {
String credentials = basicAuth.username() + ":" + basicAuth.password();
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials));
} else if (auth instanceof OllamaProviderConfig.OllamaAuth.Token tokenAuth) {
builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + tokenAuth.token()));
} else if (auth instanceof OllamaProviderConfig.OllamaAuth.None) {
// do nothing
} else {
throw new UnsupportedOperationException("Unknown authentication type: " + auth.getClass().getSimpleName());
}
return builder.build();
}
private static Duration toDuration(Integer timeoutSeconds) { private static Duration toDuration(Integer timeoutSeconds) {
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
} }

23
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java

@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
@ -45,6 +45,7 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -102,6 +103,10 @@ public abstract class AbstractCalculatedFieldProcessingService {
return Futures.whenAllComplete(argFutures.values()).call(() -> { return Futures.whenAllComplete(argFutures.values()).call(() -> {
var result = createStateByType(ctx); var result = createStateByType(ctx);
result.updateState(ctx, resolveArgumentFutures(argFutures)); result.updateState(ctx, resolveArgumentFutures(argFutures));
// TODO: move to state.init() method after merge with alarm rules 2.0
if (ctx.hasRelationQueryDynamicArguments() && result instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) {
geofencingCalculatedFieldState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
}
return result; return result;
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
@ -159,19 +164,9 @@ public abstract class AbstractCalculatedFieldProcessingService {
} }
var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration();
return switch (refDynamicSourceConfiguration.getType()) { return switch (refDynamicSourceConfiguration.getType()) {
case RELATION_QUERY -> { case RELATION_PATH_QUERY -> {
var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
if (configuration.isSimpleRelation()) { yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)),
yield switch (configuration.getDirection()) {
case FROM ->
Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
case TO ->
Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
};
}
yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor); configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
} }
}; };

4
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -62,7 +62,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
boolean entryUpdated; boolean entryUpdated;
if (existingEntry == null || newEntry.isForceResetPrevious()) { if (existingEntry == null || newEntry.isForceResetPrevious()) {
validateNewEntry(newEntry); validateNewEntry(key, newEntry);
arguments.put(key, newEntry); arguments.put(key, newEntry);
entryUpdated = true; entryUpdated = true;
} else { } else {
@ -93,7 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
} }
} }
protected void validateNewEntry(ArgumentEntry newEntry) {} protected void validateNewEntry(String key, ArgumentEntry newEntry) {}
private void updateLastUpdateTimestamp(ArgumentEntry entry) { private void updateLastUpdateTimestamp(ArgumentEntry entry) {
long newTs = this.latestTimestamp; long newTs = this.latestTimestamp;

44
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -43,11 +43,13 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
@ -78,9 +80,12 @@ public class CalculatedFieldCtx {
private long maxStateSize; private long maxStateSize;
private long maxSingleValueArgumentSize; private long maxSingleValueArgumentSize;
private boolean relationQueryDynamicArguments;
private List<String> mainEntityGeofencingArgumentNames; private List<String> mainEntityGeofencingArgumentNames;
private List<String> linkedEntityGeofencingArgumentNames; private List<String> linkedEntityGeofencingArgumentNames;
private long scheduledUpdateIntervalMillis;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) { public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) {
this.calculatedField = calculatedField; this.calculatedField = calculatedField;
@ -101,6 +106,7 @@ public class CalculatedFieldCtx {
var refId = entry.getValue().getRefEntityId(); var refId = entry.getValue().getRefEntityId();
var refKey = entry.getValue().getRefEntityKey(); var refKey = entry.getValue().getRefEntityKey();
if (refId == null && entry.getValue().hasDynamicSource()) { if (refId == null && entry.getValue().hasDynamicSource()) {
relationQueryDynamicArguments = true;
continue; continue;
} }
if (refId == null || refId.equals(calculatedField.getEntityId())) { if (refId == null || refId.equals(calculatedField.getEntityId())) {
@ -126,6 +132,9 @@ public class CalculatedFieldCtx {
}); });
} }
} }
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) {
this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L;
}
this.tbelInvokeService = tbelInvokeService; this.tbelInvokeService = tbelInvokeService;
this.relationService = relationService; this.relationService = relationService;
@ -141,6 +150,7 @@ public class CalculatedFieldCtx {
this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService);
initialized = true; initialized = true;
} catch (Exception e) { } catch (Exception e) {
initialized = false;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e);
} }
} }
@ -156,6 +166,7 @@ public class CalculatedFieldCtx {
); );
initialized = true; initialized = true;
} else { } else {
initialized = false;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.");
} }
} }
@ -329,25 +340,42 @@ public class CalculatedFieldCtx {
public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) {
boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression); boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression);
boolean outputChanged = !output.equals(other.output); boolean outputChanged = !output.equals(other.output);
return expressionChanged || outputChanged; boolean scheduledUpdatesConfigChanged = scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis;
return expressionChanged || outputChanged || scheduledUpdatesConfigChanged;
} }
public boolean hasStateChanges(CalculatedFieldCtx other) { public boolean hasStateChanges(CalculatedFieldCtx other) {
boolean typeChanged = !cfType.equals(other.cfType); boolean typeChanged = !cfType.equals(other.cfType);
boolean argumentsChanged = !arguments.equals(other.arguments); boolean argumentsChanged = !arguments.equals(other.arguments);
return typeChanged || argumentsChanged; boolean geoZoneGroupsConfigChanged = hasGeofencingZoneGroupConfigurationChanges(other);
return typeChanged || argumentsChanged || geoZoneGroupsConfigChanged;
} }
public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups());
boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval();
return refreshTriggerChanged || refreshIntervalChanged;
} }
return false; return false;
} }
public boolean hasRelationQueryDynamicArguments() {
return relationQueryDynamicArguments && scheduledUpdateIntervalMillis != -1;
}
public boolean shouldFetchDynamicArgumentsFromDb(CalculatedFieldState state) {
if (!hasRelationQueryDynamicArguments()) {
return false;
}
if (!(state instanceof GeofencingCalculatedFieldState geofencingState)) {
return false;
}
if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) {
return true;
}
return geofencingState.getLastDynamicArgumentsRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis;
}
public String getSizeExceedsLimitMessage() { public String getSizeExceedsLimitMessage() {
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
} }

7
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java

@ -50,13 +50,6 @@ public interface CalculatedFieldState {
long getLatestTimestamp(); long getLatestTimestamp();
default void setDirty(boolean dirty) {
}
default boolean isDirty() {
return false;
}
void setRequiredArguments(List<String> requiredArguments); void setRequiredArguments(List<String> requiredArguments);
boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues); boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues);

5
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

@ -48,9 +48,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
protected void validateNewEntry(ArgumentEntry newEntry) { protected void validateNewEntry(String key, ArgumentEntry newEntry) {
if (newEntry instanceof TsRollingArgumentEntry) { if (newEntry instanceof TsRollingArgumentEntry) {
throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields."); throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " +
"Rolling argument entry is not supported for simple calculated fields.");
} }
} }

10
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java

@ -41,7 +41,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
} }
public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) { public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) {
this.zoneStates = toZones(Map.of(entityId, ProtoUtils.fromProto(entry))); this(Map.of(entityId, ProtoUtils.fromProto(entry)));
} }
public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdkvEntryMap) { public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdkvEntryMap) {
@ -63,6 +63,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType());
} }
if (geofencingArgumentEntry.isEmpty()) {
zoneStates.clear();
return true;
}
boolean updated = false; boolean updated = false;
for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) { for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) {
if (updateZone(zoneEntry)) { if (updateZone(zoneEntry)) {
@ -97,6 +101,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
zoneStates.put(zoneId, newZoneState); zoneStates.put(zoneId, newZoneState);
return true; return true;
} }
if (newZoneState.getPerimeterDefinition() == null) {
zoneStates.remove(zoneId);
return true;
}
return existingZoneState.update(newZoneState); return existingZoneState.update(newZoneState);
} }

87
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java

@ -15,16 +15,19 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state.geofencing; package org.thingsboard.server.service.cf.ctx.state.geofencing;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.Coordinates;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent;
@ -39,7 +42,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -51,18 +53,14 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Geo
@Data @Data
@Slf4j @Slf4j
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
private boolean dirty; private long lastDynamicArgumentsRefreshTs = -1;
public GeofencingCalculatedFieldState() { public GeofencingCalculatedFieldState(List<String> requiredArguments) {
super(new ArrayList<>(), new HashMap<>(), false, -1); super(requiredArguments);
this.dirty = false;
}
public GeofencingCalculatedFieldState(List<String> argNames) {
super(argNames);
} }
@Override @Override
@ -71,49 +69,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
public boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues) { protected void validateNewEntry(String key, ArgumentEntry newEntry) {
if (arguments == null) { switch (key) {
arguments = new HashMap<>(); case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
} if (!(newEntry instanceof SingleValueArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
boolean stateUpdated = false; "Only SINGLE_VALUE type is allowed.");
}
for (var entry : argumentValues.entrySet()) {
String key = entry.getKey();
ArgumentEntry newEntry = entry.getValue();
checkArgumentSize(key, newEntry, ctx);
ArgumentEntry existingEntry = arguments.get(key);
boolean entryUpdated;
if (existingEntry == null || newEntry.isForceResetPrevious()) {
entryUpdated = switch (key) {
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only SINGLE_VALUE type is allowed.");
}
arguments.put(key, singleValueArgumentEntry);
yield true;
}
default -> {
if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only GEOFENCING type is allowed.");
}
arguments.put(key, geofencingArgumentEntry);
yield true;
}
};
} else {
entryUpdated = existingEntry.updateEntry(newEntry);
} }
if (entryUpdated) { default -> {
stateUpdated = true; if (!(newEntry instanceof GeofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only GEOFENCING type is allowed.");
}
} }
} }
return stateUpdated;
} }
@Override @Override
@ -125,7 +95,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
Map<String, ZoneGroupConfiguration> zoneGroups = geofencingCfg.getZoneGroups(); Map<String, ZoneGroupConfiguration> zoneGroups = geofencingCfg.getZoneGroups();
ObjectNode resultNode = JacksonUtil.newObjectNode(); ObjectNode valuesNode = JacksonUtil.newObjectNode();
List<ListenableFuture<Boolean>> relationFutures = new ArrayList<>(); List<ListenableFuture<Boolean>> relationFutures = new ArrayList<>();
getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { getGeofencingArguments().forEach((argumentKey, argumentEntry) -> {
@ -154,10 +124,11 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
relationFutures.add(f); relationFutures.add(f);
} }
}); });
updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode); updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode);
}); });
var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); OutputType outputType = ctx.getOutput().getType();
var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), toResultNode(outputType, valuesNode));
if (relationFutures.isEmpty()) { if (relationFutures.isEmpty()) {
return Futures.immediateFuture(result); return Futures.immediateFuture(result);
} }
@ -171,7 +142,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue()));
} }
private void updateResultNode(String argumentKey, List<GeofencingEvalResult> zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { private void updateValuesNode(String argumentKey, List<GeofencingEvalResult> zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) {
GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults); GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults);
final String eventKey = argumentKey + "Event"; final String eventKey = argumentKey + "Event";
final String statusKey = argumentKey + "Status"; final String statusKey = argumentKey + "Status";
@ -185,6 +156,16 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
} }
} }
private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) {
if (OutputType.ATTRIBUTES.equals(outputType) || latestTimestamp == -1) {
return valuesNode;
}
ObjectNode resultNode = JacksonUtil.newObjectNode();
resultNode.put("ts", latestTimestamp);
resultNode.set("values", valuesNode);
return resultNode;
}
private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) { private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) {
boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status()));
boolean prevInside = zoneResults.stream() boolean prevInside = zoneResults.stream()

2
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -259,7 +259,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
transportConfiguration.setBootstrap(Collections.emptyList()); transportConfiguration.setBootstrap(Collections.emptyList());
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString())); transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString()));
transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), SINGLE)); transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), false, SINGLE));
DeviceProfileData deviceProfileData = new DeviceProfileData(); DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();

7
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -24,6 +24,7 @@ import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
@ -59,6 +60,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
@ -261,6 +263,11 @@ public class EdgeContextComponent {
@Autowired @Autowired
private CalculatedFieldProcessor calculatedFieldProcessor; private CalculatedFieldProcessor calculatedFieldProcessor;
@Autowired
private AiModelService aiModelService;
@Autowired
private AiModelProcessor aiModelProcessor;
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType); EdgeProcessor processor = processorMap.get(edgeEventType);
if (processor == null) { if (processor == null) {

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

@ -113,7 +113,7 @@ public class EdgeEventSourcingListener {
return; return;
} }
try { try {
if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) { if (EntityType.TENANT == entityType || EntityType.EDGE == entityType) {
return; return;
} }
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
@ -227,7 +227,7 @@ public class EdgeEventSourcingListener {
break; break;
case TENANT: case TENANT:
return !event.getCreated(); return !event.getCreated();
case API_USAGE_STATE, EDGE, AI_MODEL: case API_USAGE_STATE, EDGE:
return false; return false;
case DOMAIN: case DOMAIN:
if (entity instanceof Domain domain) { if (entity instanceof Domain domain) {

16
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.domain.DomainInfo;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -86,6 +88,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -654,4 +657,17 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build(); .setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build();
} }
public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) {
return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel))
.setIdMSB(aiModel.getId().getId().getMostSignificantBits())
.setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build();
}
public static AiModelUpdateMsg constructAiModelDeleteMsg(AiModelId aiModelId) {
return AiModelUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(aiModelId.getId().getMostSignificantBits())
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
}
} }

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

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -934,6 +935,11 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg));
} }
} }
if (uplinkMsg.getAiModelUpdateMsgCount() > 0) {
for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) {
result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg));
}
}
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

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

@ -139,7 +139,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
UPDATED_COMMENT, DELETED -> true; UPDATED_COMMENT, DELETED -> true;
default -> switch (type) { default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE, WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false; default -> false;
}; };

130
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java

@ -0,0 +1,130 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.ai;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@Component
@TbCoreComponent
public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiModelProcessor {
@Override
public ListenableFuture<Void> processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg) {
AiModelId aiModelId = new AiModelId(new UUID(aiModelUpdateMsg.getIdMSB(), aiModelUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (aiModelUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge);
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AiModelId aiModelId = new AiModelId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(edgeEvent.getTenantId(), aiModelId);
if (aiModel.isPresent()) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelUpdatedMsg(msgType, aiModel.get());
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAiModelUpdateMsg(aiModelUpdateMsg)
.build();
}
}
case DELETED -> {
AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelDeleteMsg(aiModelId);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAiModelUpdateMsg(aiModelUpdateMsg)
.build();
}
}
return null;
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.AI_MODEL;
}
private void processAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg, Edge edge) {
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateAiModel(tenantId, aiModelId, aiModelUpdateMsg);
Boolean wasCreated = resultPair.getFirst();
if (wasCreated) {
pushAiModelCreatedEventToRuleEngine(tenantId, edge, aiModelId);
}
Boolean nameWasUpdated = resultPair.getSecond();
if (nameWasUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, EdgeEventActionType.UPDATED, aiModelId, null);
}
}
private void pushAiModelCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AiModelId aiModelId) {
try {
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
if (aiModel.isPresent()) {
String aiModelAsString = JacksonUtil.toString(aiModel.get());
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId());
pushEntityEventToRuleEngine(tenantId, aiModelId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, aiModelAsString, msgMetaData);
} else {
log.warn("[{}][{}] Failed to find aiModel", tenantId, aiModelId);
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to push aiModel action to rule engine: {}", tenantId, aiModelId, TbMsgType.ENTITY_CREATED.name(), e);
}
}
}

21
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java → application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java

@ -13,23 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.actors.calculatedField; package org.thingsboard.server.service.edge.rpc.processor.ai;
import lombok.Data; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
@Data public interface AiModelProcessor extends EdgeProcessor {
public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId; ListenableFuture<Void> processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg);
private final CalculatedFieldId cfId;
@Override
public MsgType getMsgType() {
return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG;
}
} }

81
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java

@ -0,0 +1,81 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.ai;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.Optional;
@Slf4j
public abstract class BaseAiModelProcessor extends BaseEdgeProcessor {
@Autowired
private DataValidator<AiModel> aiModelValidator;
protected Pair<Boolean, Boolean> saveOrUpdateAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg) {
boolean isCreated = false;
boolean isNameUpdated = false;
try {
AiModel aiModel = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
if (aiModel == null) {
throw new RuntimeException("[{" + tenantId + "}] aiModelUpdateMsg {" + aiModelUpdateMsg + " } cannot be converted to aiModel");
}
Optional<AiModel> aiModelById = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
if (aiModelById.isEmpty()) {
aiModel.setCreatedTime(Uuids.unixTimestamp(aiModelId.getId()));
isCreated = true;
aiModel.setId(null);
} else {
aiModel.setId(aiModelId);
}
String aiModelName = aiModel.getName();
Optional<AiModel> aiModelByName = edgeCtx.getAiModelService().findAiModelByTenantIdAndName(aiModel.getTenantId(), aiModelName);
if (aiModelByName.isPresent() && !aiModelByName.get().getId().equals(aiModelId)) {
aiModelName = aiModelName + "_" + StringUtils.randomAlphabetic(15);
log.warn("[{}] aiModel with name {} already exists. Renaming aiModel name to {}",
tenantId, aiModel.getName(), aiModelByName.get().getName());
isNameUpdated = true;
}
aiModel.setName(aiModelName);
aiModelValidator.validate(aiModel, AiModel::getTenantId);
if (isCreated) {
aiModel.setId(aiModelId);
}
edgeCtx.getAiModelService().save(aiModel, false);
} catch (Exception e) {
log.error("[{}] Failed to process aiModel update msg [{}]", tenantId, aiModelUpdateMsg, e);
throw e;
}
return Pair.of(isCreated, isNameUpdated);
}
}

8
application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java

@ -20,6 +20,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
@ -40,10 +41,15 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
@Override @Override
public Asset save(Asset asset, User user) throws Exception { public Asset save(Asset asset, User user) throws Exception {
return save(asset, NameConflictStrategy.DEFAULT, user);
}
@Override
public Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = asset.getTenantId(); TenantId tenantId = asset.getTenantId();
try { try {
Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); Asset savedAsset = checkNotNull(assetService.saveAsset(asset, nameConflictStrategy));
autoCommit(user, savedAsset.getId()); autoCommit(user, savedAsset.getId());
logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(), logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(),
actionType, user); actionType, user);

3
application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.asset; package org.thingsboard.server.service.entitiy.asset;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -27,6 +28,8 @@ public interface TbAssetService {
Asset save(Asset asset, User user) throws Exception; Asset save(Asset asset, User user) throws Exception;
Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
void delete(Asset asset, User user); void delete(Asset asset, User user);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException; Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException;

8
application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -32,10 +33,15 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements
@Override @Override
public Customer save(Customer customer, SecurityUser user) throws Exception { public Customer save(Customer customer, SecurityUser user) throws Exception {
return save(customer, NameConflictStrategy.DEFAULT, user);
}
@Override
public Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception {
ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = customer.getTenantId(); TenantId tenantId = customer.getTenantId();
try { try {
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer)); Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer, nameConflictStrategy));
autoCommit(user, savedCustomer.getId()); autoCommit(user, savedCustomer.getId());
logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user); logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user);
return savedCustomer; return savedCustomer;

4
application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java

@ -16,8 +16,12 @@
package org.thingsboard.server.service.entitiy.customer; package org.thingsboard.server.service.entitiy.customer;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TbCustomerService extends SimpleTbEntityService<Customer> { public interface TbCustomerService extends SimpleTbEntityService<Customer> {
Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception;
} }

15
application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java

@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
@ -56,10 +57,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
@Override @Override
public Device save(Device device, String accessToken, User user) throws Exception { public Device save(Device device, String accessToken, User user) throws Exception {
return save(device, accessToken, NameConflictStrategy.DEFAULT, user);
}
@Override
public Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = device.getTenantId(); TenantId tenantId = device.getTenantId();
try { try {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken, nameConflictStrategy));
autoCommit(user, savedDevice.getId()); autoCommit(user, savedDevice.getId());
logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(),
actionType, user); actionType, user);
@ -73,10 +79,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
@Override @Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException { public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException {
return saveDeviceWithCredentials(device, credentials, NameConflictStrategy.DEFAULT, user);
}
@Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException {
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = device.getTenantId(); TenantId tenantId = device.getTenantId();
try { try {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials, nameConflictStrategy));
logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(),
actionType, user); actionType, user);

5
application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.device;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -33,8 +34,12 @@ public interface TbDeviceService {
Device save(Device device, String accessToken, User user) throws Exception; Device save(Device device, String accessToken, User user) throws Exception;
Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException; Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException;
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException;
void delete(Device device, User user); void delete(Device device, User user);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException; Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException;

8
application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -80,10 +81,15 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
@Override @Override
public EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception { public EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception {
return save(entityView, existingEntityView, NameConflictStrategy.DEFAULT, user);
}
@Override
public EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = entityView.getTenantId(); TenantId tenantId = entityView.getTenantId();
try { try {
EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView, nameConflictStrategy));
this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user); this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user);
autoCommit(user, savedEntityView.getId()); autoCommit(user, savedEntityView.getId());
logEntityActionService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView, logEntityActionService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,

3
application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.entityview;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -33,6 +34,8 @@ public interface TbEntityViewService extends ComponentLifecycleListener {
EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception; EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception;
EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
void updateEntityViewAttributes(TenantId tenantId, EntityView savedEntityView, EntityView oldEntityView, User user) throws ThingsboardException; void updateEntityViewAttributes(TenantId tenantId, EntityView savedEntityView, EntityView oldEntityView, User user) throws ThingsboardException;
void delete(EntityView entity, User user) throws ThingsboardException; void delete(EntityView entity, User user) throws ThingsboardException;

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

@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// This list should include all versions which are compatible for the upgrade. // This list should include all versions which are compatible for the upgrade.
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release.
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0"); private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1");
private final ProjectInfo projectInfo; private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
@ -83,6 +84,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
ActorSystemContext actorContext, ActorSystemContext actorContext,
TbDeviceProfileCache deviceProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
TbTenantProfileCache tenantProfileCache, TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, PartitionService partitionService,
@ -90,7 +92,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
JwtSettingsService jwtSettingsService, JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache, CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) { CalculatedFieldStateService stateService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService); eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory; this.queueFactory = tbQueueFactory;
this.stateService = stateService; this.stateService = stateService;

9
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -435,8 +436,9 @@ public class DefaultTbClusterService implements TbClusterService {
@Override @Override
public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) { public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) {
TenantId tenantId = resource.getTenantId();
TbResourceId resourceId = resource.getId();
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
TenantId tenantId = resource.getTenantId();
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder() ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
@ -447,6 +449,7 @@ public class DefaultTbClusterService implements TbClusterService {
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
} }
broadcastEntityStateChangeEvent(tenantId, resourceId, ComponentLifecycleEvent.UPDATED);
} }
@Override @Override
@ -462,6 +465,7 @@ public class DefaultTbClusterService implements TbClusterService {
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build(); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
} }
broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED);
} }
private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
@ -592,7 +596,8 @@ public class DefaultTbClusterService implements TbClusterService {
EntityType.TENANT_PROFILE, EntityType.TENANT_PROFILE,
EntityType.DEVICE_PROFILE, EntityType.DEVICE_PROFILE,
EntityType.ASSET_PROFILE, EntityType.ASSET_PROFILE,
EntityType.JOB) EntityType.JOB,
EntityType.TB_RESOURCE)
|| (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
) { ) {

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

@ -55,6 +55,7 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
@ -176,10 +177,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
NotificationSchedulerService notificationSchedulerService, NotificationSchedulerService notificationSchedulerService,
NotificationRuleProcessor notificationRuleProcessor, NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService, TbImageService imageService,
TbResourceDataCache tbResourceDataCache,
RuleEngineCallService ruleEngineCallService, RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache, CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) { EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService); eventPublisher, jwtSettingsService);
this.stateService = stateService; this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService; this.localSubscriptionService = localSubscriptionService;

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

@ -87,7 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext, public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) { StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
super(actorContext, null, null, null, null, null, null, super(actorContext, null, null, null, null, null, null, null,
null, null); null, null);
this.edgeCtx = edgeCtx; this.edgeCtx = edgeCtx;
this.stats = new EdgeConsumerStats(statsFactory); this.stats = new EdgeConsumerStats(statsFactory);

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

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg; import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg;
@ -78,13 +79,14 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
QueueService queueService, QueueService queueService,
TbDeviceProfileCache deviceProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
TbTenantProfileCache tenantProfileCache, TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, PartitionService partitionService,
ApplicationEventPublisher eventPublisher, ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService, JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache) { CalculatedFieldCache calculatedFieldCache) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx; this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService; this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService; this.queueService = queueService;

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

@ -30,12 +30,14 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -72,6 +74,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbTenantProfileCache tenantProfileCache; protected final TbTenantProfileCache tenantProfileCache;
protected final TbDeviceProfileCache deviceProfileCache; protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache; protected final TbAssetProfileCache assetProfileCache;
protected final TbResourceDataCache tbResourceDataCache;
protected final CalculatedFieldCache calculatedFieldCache; protected final CalculatedFieldCache calculatedFieldCache;
protected final TbApiUsageStateService apiUsageStateService; protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService; protected final PartitionService partitionService;
@ -202,6 +205,8 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
} else { } else {
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId()); calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
} }
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));
} }
eventPublisher.publishEvent(componentLifecycleMsg); eventPublisher.publishEvent(componentLifecycleMsg);

4
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.queue.processing;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
@ -43,12 +44,13 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
TbTenantProfileCache tenantProfileCache, TbTenantProfileCache tenantProfileCache,
TbDeviceProfileCache deviceProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
CalculatedFieldCache calculatedFieldCache, CalculatedFieldCache calculatedFieldCache,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, PartitionService partitionService,
ApplicationEventPublisher eventPublisher, ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) { JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
} }
@PostConstruct @PostConstruct

1
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -102,7 +102,6 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
if (result.isSuccess()) { if (result.isSuccess()) {
logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString()); logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString());
} }
return result; return result;
} catch (Exception e) { } catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),

13
application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java

@ -18,8 +18,10 @@ package org.thingsboard.server.service.security.auth;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@ -32,6 +34,10 @@ public class AuthExceptionHandler extends OncePerRequestFilter {
private final ThingsboardErrorResponseHandler errorResponseHandler; private final ThingsboardErrorResponseHandler errorResponseHandler;
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
try { try {
@ -39,8 +45,15 @@ public class AuthExceptionHandler extends OncePerRequestFilter {
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log(e);
errorResponseHandler.handle(e, response); errorResponseHandler.handle(e, response);
} }
} }
private void log(Exception e) {
if (logControllerErrorStackTrace) {
log.error("Auth error", e);
}
}
} }

24
application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java

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

22
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
@ -61,12 +62,25 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
private static final ThingsboardException TOO_MANY_REQUESTS_ERROR = new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); private static final ThingsboardException TOO_MANY_REQUESTS_ERROR = new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
@Override @Override
public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) { public boolean isTwoFaEnabled(TenantId tenantId, User user) {
return configManager.getAccountTwoFaSettings(tenantId, userId) return configManager.getAccountTwoFaSettings(tenantId, user)
.map(settings -> !settings.getConfigs().isEmpty()) .map(settings -> !settings.getConfigs().isEmpty())
.orElse(false); .orElse(false);
} }
@Override
public boolean isEnforceTwoFaEnabled(TenantId tenantId, User user) {
SystemLevelUsersFilter enforcedUsersFilter = configManager.getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, true)
.filter(PlatformTwoFaSettings::isEnforceTwoFa)
.map(PlatformTwoFaSettings::getEnforcedUsersFilter)
.orElse(null);
if (enforcedUsersFilter == null) {
return false;
}
return userService.matchesFilter(tenantId, enforcedUsersFilter, user);
}
@Override @Override
public void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException { public void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException {
getTwoFaProvider(providerType).check(tenantId); getTwoFaProvider(providerType).check(tenantId);
@ -75,7 +89,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
@Override @Override
public void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception { public void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception {
TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType)
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR);
prepareVerificationCode(user, accountConfig, checkLimits); prepareVerificationCode(user, accountConfig, checkLimits);
} }
@ -104,7 +118,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
@Override @Override
public boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException { public boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException {
TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType)
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR);
return checkVerificationCode(user, verificationCode, accountConfig, checkLimits); return checkVerificationCode(user, verificationCode, accountConfig, checkLimits);
} }

6
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java

@ -18,17 +18,17 @@ package org.thingsboard.server.service.security.auth.mfa;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
public interface TwoFactorAuthService { public interface TwoFactorAuthService {
boolean isTwoFaEnabled(TenantId tenantId, UserId userId); boolean isTwoFaEnabled(TenantId tenantId, User user);
void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException; boolean isEnforceTwoFaEnabled(TenantId tenantId, User user);
void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException;
void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception; void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception;

49
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java

@ -21,15 +21,16 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserAuthSettings; import org.thingsboard.server.common.data.security.UserAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.settings.AdminSettingsDao;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
@ -55,9 +56,9 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
@Override @Override
public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) { public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, User user) {
PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null); PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null);
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) return Optional.ofNullable(userAuthSettingsDao.findByUserId(user.getId()))
.map(userAuthSettings -> { .map(userAuthSettings -> {
AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings(); AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings();
if (twoFaSettings == null) return null; if (twoFaSettings == null) return null;
@ -79,17 +80,22 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
} }
if (updateNeeded) { if (updateNeeded) {
twoFaSettings = saveAccountTwoFaSettings(tenantId, userId, twoFaSettings); twoFaSettings = saveAccountTwoFaSettings(tenantId, user, twoFaSettings);
} }
return twoFaSettings; return twoFaSettings;
}); });
} }
protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) { protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, User user, AccountTwoFaSettings settings) {
UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) if (settings.getConfigs().isEmpty()) {
if (twoFactorAuthService.isEnforceTwoFaEnabled(tenantId, user)) {
throw new DataValidationException("At least one 2FA provider is required");
}
}
UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(user.getId()))
.orElseGet(() -> { .orElseGet(() -> {
UserAuthSettings newUserAuthSettings = new UserAuthSettings(); UserAuthSettings newUserAuthSettings = new UserAuthSettings();
newUserAuthSettings.setUserId(userId); newUserAuthSettings.setUserId(user.getId());
return newUserAuthSettings; return newUserAuthSettings;
}); });
userAuthSettings.setTwoFaSettings(settings); userAuthSettings.setTwoFaSettings(settings);
@ -101,18 +107,18 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
@Override @Override
public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) { public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType) {
return getAccountTwoFaSettings(tenantId, userId) return getAccountTwoFaSettings(tenantId, user)
.map(AccountTwoFaSettings::getConfigs) .map(AccountTwoFaSettings::getConfigs)
.flatMap(configs -> Optional.ofNullable(configs.get(providerType))); .flatMap(configs -> Optional.ofNullable(configs.get(providerType)));
} }
@Override @Override
public AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig) { public AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, User user, TwoFaAccountConfig accountConfig) {
getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
.orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured")); .orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured"));
AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId).orElseGet(() -> { AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, user).orElseGet(() -> {
AccountTwoFaSettings newSettings = new AccountTwoFaSettings(); AccountTwoFaSettings newSettings = new AccountTwoFaSettings();
newSettings.setConfigs(new LinkedHashMap<>()); newSettings.setConfigs(new LinkedHashMap<>());
return newSettings; return newSettings;
@ -128,12 +134,12 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
if (configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) { if (configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) {
configs.values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true)); configs.values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true));
} }
return saveAccountTwoFaSettings(tenantId, userId, settings); return saveAccountTwoFaSettings(tenantId, user, settings);
} }
@Override @Override
public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) { public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType) {
AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId) AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, user)
.orElseThrow(() -> new IllegalArgumentException("2FA not configured")); .orElseThrow(() -> new IllegalArgumentException("2FA not configured"));
settings.getConfigs().remove(providerType); settings.getConfigs().remove(providerType);
if (settings.getConfigs().size() == 1) { if (settings.getConfigs().size() == 1) {
@ -145,7 +151,7 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
.min(Comparator.comparing(TwoFaAccountConfig::getProviderType)) .min(Comparator.comparing(TwoFaAccountConfig::getProviderType))
.ifPresent(config -> config.setUseByDefault(true)); .ifPresent(config -> config.setUseByDefault(true));
} }
return saveAccountTwoFaSettings(tenantId, userId, settings); return saveAccountTwoFaSettings(tenantId, user, settings);
} }
@ -166,6 +172,19 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
for (TwoFaProviderConfig providerConfig : twoFactorAuthSettings.getProviders()) { for (TwoFaProviderConfig providerConfig : twoFactorAuthSettings.getProviders()) {
twoFactorAuthService.checkProvider(tenantId, providerConfig.getProviderType()); twoFactorAuthService.checkProvider(tenantId, providerConfig.getProviderType());
} }
if (tenantId.isSysTenantId()) {
if (twoFactorAuthSettings.isEnforceTwoFa()) {
if (twoFactorAuthSettings.getProviders().isEmpty()) {
throw new DataValidationException("At least one 2FA provider is required if enforcing is enabled");
}
if (twoFactorAuthSettings.getEnforcedUsersFilter() == null) {
throw new DataValidationException("Users filter to enforce 2FA for is required");
}
}
} else {
twoFactorAuthSettings.setEnforceTwoFa(false);
twoFactorAuthSettings.setEnforcedUsersFilter(null);
}
AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.orElseGet(() -> { .orElseGet(() -> {

10
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java

@ -15,9 +15,9 @@
*/ */
package org.thingsboard.server.service.security.auth.mfa.config; package org.thingsboard.server.service.security.auth.mfa.config;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
@ -27,14 +27,14 @@ import java.util.Optional;
public interface TwoFaConfigManager { public interface TwoFaConfigManager {
Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId); Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, User user);
Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType);
AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig); AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, User user, TwoFaAccountConfig accountConfig);
AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType);
Optional<PlatformTwoFaSettings> getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault); Optional<PlatformTwoFaSettings> getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault);

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java

@ -58,7 +58,7 @@ public class BackupCodeTwoFaProvider implements TwoFaProvider<BackupCodeTwoFaPro
public boolean checkVerificationCode(SecurityUser user, String code, BackupCodeTwoFaProviderConfig providerConfig, BackupCodeTwoFaAccountConfig accountConfig) { public boolean checkVerificationCode(SecurityUser user, String code, BackupCodeTwoFaProviderConfig providerConfig, BackupCodeTwoFaAccountConfig accountConfig) {
if (CollectionsUtil.contains(accountConfig.getCodes(), code)) { if (CollectionsUtil.contains(accountConfig.getCodes(), code)) {
accountConfig.getCodes().remove(code); accountConfig.getCodes().remove(code);
twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user, accountConfig);
return true; return true;
} else { } else {
return false; return false;

8
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java

@ -44,6 +44,7 @@ import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.MfaConfigurationToken;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.exception.UserPasswordNotValidException; import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -82,11 +83,10 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
Assert.notNull(authentication, "No authentication data provided"); Assert.notNull(authentication, "No authentication data provided");
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
if (!(principal instanceof UserPrincipal)) { if (!(principal instanceof UserPrincipal userPrincipal)) {
throw new BadCredentialsException("Authentication Failed. Bad user principal."); throw new BadCredentialsException("Authentication Failed. Bad user principal.");
} }
UserPrincipal userPrincipal = (UserPrincipal) principal;
SecurityUser securityUser; SecurityUser securityUser;
if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
String username = userPrincipal.getValue(); String username = userPrincipal.getValue();
@ -103,8 +103,10 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
} }
securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password); securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) { if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser)) {
return new MfaAuthenticationToken(securityUser); return new MfaAuthenticationToken(securityUser);
} else if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId(), securityUser)) {
return new MfaConfigurationToken(securityUser);
} else { } else {
systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null); systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null);
} }

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

@ -15,11 +15,11 @@
*/ */
package org.thingsboard.server.service.security.auth.rest; package org.thingsboard.server.service.security.auth.rest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -30,6 +30,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.MfaConfigurationToken;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -38,7 +39,7 @@ import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Component(value = "defaultAuthenticationSuccessHandler") @Slf4j @Component(value = "defaultAuthenticationSuccessHandler")
@RequiredArgsConstructor @RequiredArgsConstructor
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
@ -46,18 +47,14 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
@Override @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException { Authentication authentication) throws IOException {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
JwtPair tokenPair = new JwtPair(); JwtPair tokenPair;
if (authentication instanceof MfaAuthenticationToken) { if (authentication instanceof MfaAuthenticationToken) {
int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true) tokenPair = createMfaTokenPair(securityUser, Authority.PRE_VERIFICATION_TOKEN);
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification()) } else if (authentication instanceof MfaConfigurationToken) {
.filter(time -> time > 0)) tokenPair = createMfaTokenPair(securityUser, Authority.MFA_CONFIGURATION_TOKEN);
.orElse((int) TimeUnit.MINUTES.toSeconds(30));
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
tokenPair.setRefreshToken(null);
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
} else { } else {
tokenPair = tokenFactory.createTokenPair(securityUser); tokenPair = tokenFactory.createTokenPair(securityUser);
} }
@ -69,6 +66,19 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
clearAuthenticationAttributes(request); clearAuthenticationAttributes(request);
} }
public JwtPair createMfaTokenPair(SecurityUser securityUser, Authority scope) {
log.debug("[{}][{}] Creating {} token", securityUser.getTenantId(), securityUser.getId(), scope);
JwtPair tokenPair = new JwtPair();
int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())
.filter(time -> time > 0))
.orElse((int) TimeUnit.MINUTES.toSeconds(30));
tokenPair.setToken(tokenFactory.createMfaToken(securityUser, scope, preVerificationTokenLifetime).getToken());
tokenPair.setRefreshToken(null);
tokenPair.setScope(scope);
return tokenPair;
}
/** /**
* Removes temporary authentication-related data which may have been stored * Removes temporary authentication-related data which may have been stored
* in the session during the authentication process.. * in the session during the authentication process..

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

@ -115,13 +115,16 @@ public class JwtTokenFactory {
throw new IllegalArgumentException("JWT Token doesn't have any scopes"); throw new IllegalArgumentException("JWT Token doesn't have any scopes");
} }
Authority authority = Authority.parse(scopes.get(0));
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
securityUser.setEmail(subject); securityUser.setEmail(subject);
securityUser.setAuthority(Authority.parse(scopes.get(0))); securityUser.setAuthority(authority);
String tenantId = claims.get(TENANT_ID, String.class); String tenantId = claims.get(TENANT_ID, String.class);
if (tenantId != null) { if (tenantId != null) {
securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId))); securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId)));
} else if (securityUser.getAuthority() == Authority.SYS_ADMIN) { } else if (authority == Authority.SYS_ADMIN) {
securityUser.setTenantId(TenantId.SYS_TENANT_ID); securityUser.setTenantId(TenantId.SYS_TENANT_ID);
} }
String customerId = claims.get(CUSTOMER_ID, String.class); String customerId = claims.get(CUSTOMER_ID, String.class);
@ -132,18 +135,15 @@ public class JwtTokenFactory {
securityUser.setSessionId(claims.get(SESSION_ID, String.class)); securityUser.setSessionId(claims.get(SESSION_ID, String.class));
} }
UserPrincipal principal; boolean isPublic = false;
if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { if (authority != Authority.PRE_VERIFICATION_TOKEN && authority != Authority.MFA_CONFIGURATION_TOKEN) {
securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
securityUser.setLastName(claims.get(LAST_NAME, String.class)); securityUser.setLastName(claims.get(LAST_NAME, String.class));
securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); isPublic = claims.get(IS_PUBLIC, Boolean.class);
principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
} else {
principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, subject);
} }
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
securityUser.setUserPrincipal(principal); securityUser.setUserPrincipal(principal);
return securityUser; return securityUser;
} }
@ -179,8 +179,8 @@ public class JwtTokenFactory {
return securityUser; return securityUser;
} }
public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) { public JwtToken createMfaToken(SecurityUser user, Authority scope, Integer expirationTime) {
JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime) JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(scope.name()), expirationTime)
.claim(TENANT_ID, user.getTenantId().toString()); .claim(TENANT_ID, user.getTenantId().toString());
if (user.getCustomerId() != null) { if (user.getCustomerId() != null) {
jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString()); jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString());

2
application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java

@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@Component(value = "customerUserPermissions") @Component
public class CustomerUserPermissions extends AbstractPermissions { public class CustomerUserPermissions extends AbstractPermissions {
public CustomerUserPermissions() { public CustomerUserPermissions() {

13
application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.service.security.permission; package org.thingsboard.server.service.security.permission;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -33,18 +32,18 @@ import java.util.Optional;
@Slf4j @Slf4j
public class DefaultAccessControlService implements AccessControlService { public class DefaultAccessControlService implements AccessControlService {
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; private static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
private final Map<Authority, Permissions> authorityPermissions = new HashMap<>(); private final Map<Authority, Permissions> authorityPermissions = new HashMap<>();
public DefaultAccessControlService( public DefaultAccessControlService(SysAdminPermissions sysAdminPermissions,
@Qualifier("sysAdminPermissions") Permissions sysAdminPermissions, TenantAdminPermissions tenantAdminPermissions,
@Qualifier("tenantAdminPermissions") Permissions tenantAdminPermissions, CustomerUserPermissions customerUserPermissions,
@Qualifier("customerUserPermissions") Permissions customerUserPermissions) { MfaConfigurationPermissions mfaConfigurationPermissions) {
authorityPermissions.put(Authority.SYS_ADMIN, sysAdminPermissions); authorityPermissions.put(Authority.SYS_ADMIN, sysAdminPermissions);
authorityPermissions.put(Authority.TENANT_ADMIN, tenantAdminPermissions); authorityPermissions.put(Authority.TENANT_ADMIN, tenantAdminPermissions);
authorityPermissions.put(Authority.CUSTOMER_USER, customerUserPermissions); authorityPermissions.put(Authority.CUSTOMER_USER, customerUserPermissions);
authorityPermissions.put(Authority.MFA_CONFIGURATION_TOKEN, mfaConfigurationPermissions);
} }
@Override @Override
@ -58,7 +57,7 @@ public class DefaultAccessControlService implements AccessControlService {
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <I extends EntityId, T extends HasTenantId> void checkPermission(SecurityUser user, Resource resource, public <I extends EntityId, T extends HasTenantId> void checkPermission(SecurityUser user, Resource resource,
Operation operation, I entityId, T entity) throws ThingsboardException { Operation operation, I entityId, T entity) throws ThingsboardException {
PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource); PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource);
if (!permissionChecker.hasPermission(user, operation, entityId, entity)) { if (!permissionChecker.hasPermission(user, operation, entityId, entity)) {
permissionDenied(); permissionDenied();

28
application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.permission;
import org.springframework.stereotype.Component;
@Component
public class MfaConfigurationPermissions extends AbstractPermissions {
public MfaConfigurationPermissions() {
super();
// for compatibility with PE
}
}

2
application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java

@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@Component(value = "sysAdminPermissions") @Component
public class SysAdminPermissions extends AbstractPermissions { public class SysAdminPermissions extends AbstractPermissions {
public SysAdminPermissions() { public SysAdminPermissions() {

4
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@Component(value = "tenantAdminPermissions") @Component
public class TenantAdminPermissions extends AbstractPermissions { public class TenantAdminPermissions extends AbstractPermissions {
public TenantAdminPermissions() { public TenantAdminPermissions() {
@ -73,7 +73,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
}; };
private static final PermissionChecker tenantPermissionChecker = private static final PermissionChecker tenantPermissionChecker =
new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) { new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY, Operation.DELETE) {
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

9
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.sync.ie.importing.csv;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
@ -183,7 +184,13 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
data.entrySet().stream() data.entrySet().stream()
.filter(dataEntry -> dataEntry.getKey().getType() == kvType && .filter(dataEntry -> dataEntry.getKey().getType() == kvType &&
StringUtils.isNotEmpty(dataEntry.getKey().getKey())) StringUtils.isNotEmpty(dataEntry.getKey().getKey()))
.forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive())); .forEach(dataEntry -> {
ParsedValue value = dataEntry.getValue();
JsonElement kvValue = (value.getDataType() == DataType.JSON)
? (JsonElement) value.getValue()
: value.toJsonPrimitive();
kvs.add(dataEntry.getKey().getKey(), kvValue);
});
return Map.entry(kvType, kvs); return Map.entry(kvType, kvs);
}) })
.filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0) .filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0)

19
application/src/main/java/org/thingsboard/server/utils/CsvUtils.java

@ -17,10 +17,15 @@ package org.thingsboard.server.utils;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord; import org.apache.commons.csv.CSVRecord;
import org.apache.commons.io.input.CharSequenceReader; import org.apache.commons.io.input.CharSequenceReader;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -43,4 +48,18 @@ public class CsvUtils {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@SneakyThrows
public static byte[] generateCsv(List<List<String>> rows) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) {
for (List<String> row : rows) {
csvPrinter.printRecord(row);
}
csvPrinter.flush();
}
return out.toByteArray();
}
} }

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

@ -306,8 +306,11 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}" default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE # Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}" ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
# Enable/Disable timestamp key-value partioning on read queries # Enable/Disable timestamp key-value partitioning on read queries
use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}" use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}"
# Safety trigger to fall back to use_ts_key_value_partitioning_on_read as true if estimated partitions count is greater than safety trigger value.
# It helps to prevent building huge partition list (OOM) for corner cases (like from 0 to infinity) and prefer fewer reads strategy from NoSQL database
use_ts_key_value_partitioning_on_read_max_estimated_partition_count: "${USE_TS_KV_PARTITIONING_ON_READ_MAX_ESTIMATED_PARTITION_COUNT:40}"
# The number of partitions that are cached in memory of each service. It is useful to decrease the load of re-inserting the same partitions again # The number of partitions that are cached in memory of each service. It is useful to decrease the load of re-inserting the same partitions again
ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}" ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}"
# Timeseries Time To Live (in seconds) for Cassandra Record. 0 - record has never expired # Timeseries Time To Live (in seconds) for Cassandra Record. 0 - record has never expired
@ -679,6 +682,9 @@ cache:
maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled
systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled
tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled
tbResourceData:
timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_DATA_TTL:10080}" # TB resource data cache TTL
maxSize: "${CACHE_SPECS_RESOURCE_DATA_MAX_SIZE:100000}" # 0 means the cache is disabled
# Spring data parameters # Spring data parameters
spring.data.redis.repositories.enabled: false # Disable this because it is not required. spring.data.redis.repositories.enabled: false # Disable this because it is not required.

88
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
@ -47,10 +47,12 @@ import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.controller.CalculatedFieldControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -621,6 +623,59 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
}); });
} }
@Test
public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("M + 1");
calculatedField.setDebugSettings(DebugSettings.all());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("m", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("m", argument));
config.setExpression("m + 1");
Output output = new Output();
output.setName("m1");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0);
config.setOutput(output);
calculatedField.setConfiguration(config);
calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":1}"));
await().alias("create CF -> ctx is initialized -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1");
assertThat(m1).isNotNull();
assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2");
});
config.setExpression("m m");
calculatedField.setConfiguration(config);
calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":2}"));
await().alias("update CF -> ctx is not initialized -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1");
assertThat(m1).isNotNull();
assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2");
});
}
@Test @Test
public void testGeofencingCalculatedField_withZonesCreatedOnDevice() throws Exception { public void testGeofencingCalculatedField_withZonesCreatedOnDevice() throws Exception {
// --- Arrange entities --- // --- Arrange entities ---
@ -760,19 +815,13 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Zone groups: ATTRIBUTE on specific assets (one zone per group) // Zone groups: ATTRIBUTE on specific assets (one zone per group)
ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone")));
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone")));
restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone");
restrictedZoneDynamicSourceConfiguration.setMaxLevel(1);
restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup));
@ -831,10 +880,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class); TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class);
assertThat(foundTenantProfile).isNotNull(); assertThat(foundTenantProfile).isNotNull();
assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull(); assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull();
foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10); int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10;
foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF);
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class); TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class);
assertThat(savedTenantProfile).isNotNull(); assertThat(savedTenantProfile).isNotNull();
assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10); assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF);
loginTenantAdmin(); loginTenantAdmin();
// --- Arrange entities --- // --- Arrange entities ---
@ -869,11 +919,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY));
var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone")));
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup)); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup));
@ -884,7 +931,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setOutput(out); cfg.setOutput(out);
// Enable scheduled refresh with a 6-second interval // Enable scheduled refresh with a 6-second interval
cfg.setScheduledUpdateInterval(6); cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF);
cfg.setScheduledUpdateEnabled(true);
cf.setConfiguration(cfg); cf.setConfiguration(cfg);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class);
@ -935,7 +983,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
relAllowedB.setType("AllowedZone"); relAllowedB.setType("AllowedZone");
doPost("/api/relation", relAllowedB).andExpect(status().isOk()); doPost("/api/relation", relAllowedB).andExpect(status().isOk());
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId()); awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF);
// --- Same coordinates as before, but now we expect ENTERED since a new zone is registered --- // --- Same coordinates as before, but now we expect ENTERED since a new zone is registered ---
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",

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

@ -142,6 +142,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; 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.TenantProfileData;
import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.FeatureType;
@ -155,6 +156,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryStorage;
import org.thingsboard.server.service.cf.CfRocksDb; import org.thingsboard.server.service.cf.CfRocksDb;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; 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.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
@ -208,7 +210,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected static final String TENANT_ADMIN_PASSWORD = "tenant"; protected static final String TENANT_ADMIN_PASSWORD = "tenant";
protected static final String DIFFERENT_TENANT_ADMIN_EMAIL = "testdifftenant@thingsboard.org"; protected static final String DIFFERENT_TENANT_ADMIN_EMAIL = "testdifftenant@thingsboard.org";
private static final String DIFFERENT_TENANT_ADMIN_PASSWORD = "difftenant"; protected static final String DIFFERENT_TENANT_ADMIN_PASSWORD = "difftenant";
protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org"; protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org";
private static final String CUSTOMER_USER_PASSWORD = "customer"; private static final String CUSTOMER_USER_PASSWORD = "customer";
@ -601,8 +603,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Assert.assertNotNull(tokenInfo); Assert.assertNotNull(tokenInfo);
Assert.assertTrue(tokenInfo.has("token")); Assert.assertTrue(tokenInfo.has("token"));
Assert.assertTrue(tokenInfo.has("refreshToken")); Assert.assertTrue(tokenInfo.has("refreshToken"));
String token = tokenInfo.get("token").asText(); validateAndSetJwtToken(JacksonUtil.treeToValue(tokenInfo, JwtPair.class), username);
String refreshToken = tokenInfo.get("refreshToken").asText(); }
protected void validateAndSetJwtToken(JwtPair jwtPair, String username) {
Assert.assertNotNull(jwtPair);
String token = jwtPair.getToken();
String refreshToken = jwtPair.getRefreshToken();
validateJwtToken(token, username); validateJwtToken(token, username);
validateJwtToken(refreshToken, username); validateJwtToken(refreshToken, username);
this.token = token; this.token = token;
@ -1104,14 +1111,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}); });
} }
protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) { protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) {
CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId); CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId);
Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states"); Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states");
Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> { Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId); CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty(); boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs()
log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty); < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
return stateDirty; log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady;
}); });
} }

12
application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java

@ -104,7 +104,10 @@ public class AiModelControllerTest extends AbstractControllerTest {
var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class); var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class);
var newModelConfig = OpenAiChatModelConfig.builder() var newModelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key-updated")) .providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key-updated")
.build())
.modelId("o4-mini") .modelId("o4-mini")
.temperature(0.2) .temperature(0.2)
.topP(0.4) .topP(0.4)
@ -270,7 +273,7 @@ public class AiModelControllerTest extends AbstractControllerTest {
.tenantId(tenantId) .tenantId(tenantId)
.name("Test model 1") .name("Test model 1")
.configuration(OpenAiChatModelConfig.builder() .configuration(OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key")) .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build())
.modelId("o3-pro") .modelId("o3-pro")
.build()) .build())
.build(), AiModel.class); .build(), AiModel.class);
@ -594,7 +597,10 @@ public class AiModelControllerTest extends AbstractControllerTest {
private AiModel constructValidOpenAiModel(String name) { private AiModel constructValidOpenAiModel(String name) {
var modelConfig = OpenAiChatModelConfig.builder() var modelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key")) .providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key")
.build())
.modelId("gpt-4o") .modelId("gpt-4o")
.temperature(0.5) .temperature(0.5)
.topP(0.3) .topP(0.3)

24
application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java

@ -1080,6 +1080,30 @@ public class AssetControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId); testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId);
} }
@Test
public void testSaveAssetWithUniquifyStrategy() throws Exception {
Asset asset = new Asset();
asset.setName("My unique asset");
asset.setType("default");
doPost("/api/asset", asset, Asset.class);
doPost("/api/asset", asset).andExpect(status().isBadRequest());
doPost("/api/asset?nameConflictPolicy=FAIL", asset).andExpect(status().isBadRequest());
Asset secondAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY", asset, Asset.class);
assertThat(secondAsset.getName()).startsWith("My unique asset_");
Asset thirdAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", asset, Asset.class);
assertThat(thirdAsset.getName()).startsWith("My unique asset-");
Asset fourthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class);
assertThat(fourthAsset.getName()).isEqualTo("My unique asset_1");
Asset fifthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class);
assertThat(fifthAsset.getName()).isEqualTo("My unique asset_2");
}
private Asset createAsset(String name) { private Asset createAsset(String name) {
Asset asset = new Asset(); Asset asset = new Asset();
asset.setName(name); asset.setName(name);

66
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -29,15 +29,23 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import java.util.Map; import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS;
@DaoSqlTest @DaoSqlTest
public class CalculatedFieldControllerTest extends AbstractControllerTest { public class CalculatedFieldControllerTest extends AbstractControllerTest {
@ -84,7 +92,35 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
.andExpect(status().isOk());
}
@Test
public void testSaveGeofencingCalculatedField() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = getCalculatedField(testDevice.getId(), getGeofencingCalculatedFieldConfig());
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
assertThat(savedCalculatedField).isNotNull();
assertThat(savedCalculatedField.getId()).isNotNull();
assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0);
assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId());
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getGeofencingCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF"); savedCalculatedField.setName("Test CF");
@ -128,17 +164,41 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
} }
private CalculatedField getCalculatedField(DeviceId deviceId) { private CalculatedField getCalculatedField(DeviceId deviceId) {
return getCalculatedField(deviceId, getSimpleCalculatedFieldConfig());
}
private CalculatedField getCalculatedField(DeviceId deviceId, CalculatedFieldConfiguration configuration) {
CalculatedField calculatedField = new CalculatedField(); CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(deviceId); calculatedField.setEntityId(deviceId);
calculatedField.setType(CalculatedFieldType.SIMPLE); calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("Test Calculated Field"); calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1); calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig()); calculatedField.setConfiguration(configuration);
calculatedField.setVersion(1L); calculatedField.setVersion(1L);
return calculatedField; return calculatedField;
} }
private CalculatedFieldConfiguration getCalculatedFieldConfig() { private CalculatedFieldConfiguration getGeofencingCalculatedFieldConfig() {
var config = new GeofencingCalculatedFieldConfiguration();
var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "FromSafeArea")));
var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration);
Output output = new Output();
output.setType(OutputType.TIME_SERIES);
config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude"));
config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration));
config.setScheduledUpdateEnabled(false);
config.setOutput(output);
return config;
}
private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument(); Argument argument = new Argument();

22
application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java

@ -33,6 +33,7 @@ import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
@ -462,6 +463,27 @@ public class CustomerControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId); testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId);
} }
@Test
public void testSaveCustomerWithUniquifyStrategy() throws Exception {
Customer customer = new Customer();
customer.setTitle("My unique customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
doPost("/api/customer?nameConflictPolicy=FAIL", customer).andExpect(status().isBadRequest());
Customer secondCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY", customer, Customer.class);
assertThat(secondCustomer.getName()).startsWith("My unique customer_");
Customer thirdCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", customer, Customer.class);
assertThat(thirdCustomer.getName()).startsWith("My unique customer-");
Customer fourthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class);
assertThat(fourthCustomer.getName()).isEqualTo("My unique customer_1");
Customer fifthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class);
assertThat(fifthCustomer.getName()).isEqualTo("My unique customer_2");
}
private Customer createCustomer(String title) { private Customer createCustomer(String title) {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTitle(title); customer.setTitle(title);

81
application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java

@ -38,6 +38,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.testcontainers.shaded.org.awaitility.Awaitility; import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
@ -59,6 +60,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceCredentialsId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
@ -77,10 +79,15 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.utils.CsvUtils;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -1586,6 +1593,56 @@ public class DeviceControllerTest extends AbstractControllerTest {
Assert.assertEquals(newAttributeValue, actualAttribute.get("value")); Assert.assertEquals(newAttributeValue, actualAttribute.get("value"));
} }
@Test
public void testBulkImportDeviceWithJsonAttr() throws Exception {
String deviceName = "some_device";
String deviceType = "some_type";
String deviceAttr = "{\"threshold\":45}";
List<List<String>> content = new LinkedList<>();
content.add(Arrays.asList("NAME", "TYPE", "ATTR"));
content.add(Arrays.asList(deviceName, deviceType, deviceAttr));
byte[] bytes = CsvUtils.generateCsv(content);
BulkImportRequest request = new BulkImportRequest();
request.setFile(new String(bytes, StandardCharsets.UTF_8));
BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping();
BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping();
name.setType(BulkImportColumnType.NAME);
BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping();
type.setType(BulkImportColumnType.TYPE);
BulkImportRequest.ColumnMapping attr = new BulkImportRequest.ColumnMapping();
attr.setType(BulkImportColumnType.SERVER_ATTRIBUTE);
attr.setKey("attr");
List<BulkImportRequest.ColumnMapping> columns = new ArrayList<>();
columns.add(name);
columns.add(type);
columns.add(attr);
mapping.setColumns(columns);
mapping.setDelimiter(',');
mapping.setUpdate(true);
mapping.setHeader(true);
request.setMapping(mapping);
BulkImportResult<Device> deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {});
Assert.assertEquals(1, deviceBulkImportResult.getCreated().get());
Assert.assertEquals(0, deviceBulkImportResult.getErrors().get());
Assert.assertEquals(0, deviceBulkImportResult.getUpdated().get());
Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty());
Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
Assert.assertNotNull(savedDevice);
Assert.assertEquals(deviceName, savedDevice.getName());
Assert.assertEquals(deviceType, savedDevice.getType());
Optional<AttributeKvEntry> retrieved = attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get();
assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr);
assertThat(retrieved.get().getStrValue()).isNotPresent();
}
@Test @Test
public void testSaveDeviceWithOutdatedVersion() throws Exception { public void testSaveDeviceWithOutdatedVersion() throws Exception {
Device device = createDevice("Device v1.0"); Device device = createDevice("Device v1.0");
@ -1608,6 +1665,30 @@ public class DeviceControllerTest extends AbstractControllerTest {
assertThat(device.getVersion()).isEqualTo(3); assertThat(device.getVersion()).isEqualTo(3);
} }
@Test
public void testSaveDeviceWithUniquifyStrategy() throws Exception {
Device device = new Device();
device.setName("My unique device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
doPost("/api/device", device).andExpect(status().isBadRequest());
doPost("/api/device?nameConflictPolicy=FAIL", device).andExpect(status().isBadRequest());
Device secondDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY", device, Device.class);
assertThat(secondDevice.getName()).startsWith("My unique device_");
Device thirdDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", device, Device.class);
assertThat(thirdDevice.getName()).startsWith("My unique device-");
Device fourthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class);
assertThat(fourthDevice.getName()).isEqualTo("My unique device_1");
Device fifthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class);
assertThat(fifthDevice.getName()).isEqualTo("My unique device_2");
}
private Device createDevice(String name) { private Device createDevice(String name) {
Device device = new Device(); Device device = new Device();
device.setName(name); device.setName(name);

160
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -77,6 +77,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -431,14 +432,17 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List<EntityKey> alarmFields = new ArrayList<>(); List<EntityKey> alarmFields = new ArrayList<>();
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "originatorDisplayName"));
EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET); assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10);
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList();
assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes);
List<String> retrievedAlarmDisplayName = alarmPageData.getData().stream().map(AlarmData::getOriginatorDisplayName).toList();
assertThat(retrievedAlarmDisplayName).containsExactlyInAnyOrderElementsOf(assets.stream().map(Asset::getLabel).toList());
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter); List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
@ -1068,6 +1072,119 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
countByQueryAndCheck(customerEntitiesQuery, 0); countByQueryAndCheck(customerEntitiesQuery, 0);
} }
@Test
public void testFindDevicesByDisplayName() throws Exception {
loginTenantAdmin();
int numOfDevices = 3;
for (int i = 0; i < numOfDevices; i++) {
Device device = new Device();
String name = "Device" + i;
device.setName(name);
device.setLabel("Device Label " + i);
device.setType("testFindDevicesByDisplayName");
Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class);
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("testFindDevicesByDisplayName"));
filter.setDeviceNameFilter("");
KeyFilter displayNameFilter = getEntityFieldEqualFilter("displayName", "Device Label " + 0);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"));
// all devices with ownerName = TEST TENANT
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), Collections.emptyList());
checkEntitiesByQuery(query, numOfDevices, (i, entity) -> {
String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("Device" + i, name);
Assert.assertEquals("Device Label " + i, displayName);
});
// all devices with ownerName = TEST TENANT
EntityDataQuery displayNameFilterQuery = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(displayNameFilter));
checkEntitiesByQuery(displayNameFilterQuery, 1, (i, entity) -> {
String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("Device" + i, name);
Assert.assertEquals("Device Label " + i, displayName);
});
}
@Test
public void testFindUsersByDisplayName() throws Exception {
loginTenantAdmin();
User userA = new User();
userA.setAuthority(Authority.TENANT_ADMIN);
userA.setFirstName("John");
userA.setLastName("Doe");
userA.setEmail("john.doe@tb.org");
userA = doPost("/api/user", userA, User.class);
var aId = userA.getId();
User userB = new User();
userB.setAuthority(Authority.TENANT_ADMIN);
userB.setFirstName("John");
userB.setEmail("john@tb.org");
userB = doPost("/api/user", userB, User.class);
var bId = userB.getId();
User userC = new User();
userC.setAuthority(Authority.TENANT_ADMIN);
userC.setLastName("Doe");
userC.setEmail("doe@tb.org");
userC = doPost("/api/user", userC, User.class);
var cId = userC.getId();
User userD = new User();
userD.setAuthority(Authority.TENANT_ADMIN);
userD.setEmail("noname@tb.org");
userD = doPost("/api/user", userD, User.class);
var dId = userD.getId();
EntityTypeFilter filter = new EntityTypeFilter();
filter.setEntityType(EntityType.USER);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John Doe")));
checkEntitiesByQuery(query, 1, (i, entity) -> {
Assert.assertEquals(aId, entity.getEntityId());
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("John Doe", displayName);
});
query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John")));
checkEntitiesByQuery(query, 1, (i, entity) -> {
Assert.assertEquals(bId, entity.getEntityId());
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("John", displayName);
});
query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "Doe")));
checkEntitiesByQuery(query, 1, (i, entity) -> {
Assert.assertEquals(cId, entity.getEntityId());
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("Doe", displayName);
});
query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "noname@tb.org")));
checkEntitiesByQuery(query, 1, (i, entity) -> {
Assert.assertEquals(dId, entity.getEntityId());
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("noname@tb.org", displayName);
});
}
@Test @Test
public void testFindDevicesByOwnerNameAndOwnerType() throws Exception { public void testFindDevicesByOwnerNameAndOwnerType() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
@ -1105,19 +1222,30 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
// all devices with ownerName = TEST TENANT // all devices with ownerName = TEST TENANT
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter));
checkEntitiesByQuery(query, numOfDevices, TEST_TENANT_NAME, "TENANT"); BiConsumer<Integer, EntityData> checkFunction = (i, entity) -> {
String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
String ownerName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerName", new TsValue(0, "Invalid")).getValue();
String ownerType = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerType", new TsValue(0, "Invalid")).getValue();
String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue();
Assert.assertEquals("Device" + i, name);
Assert.assertEquals(TEST_TENANT_NAME, ownerName);
Assert.assertEquals("TENANT", ownerType);
Assert.assertEquals("1" + i, alarmActiveTime);
};
checkEntitiesByQuery(query, numOfDevices, checkFunction);
// all devices with wrong ownerName // all devices with wrong ownerName
EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter));
checkEntitiesByQuery(wrongTenantNameQuery, 0, null, null); checkEntitiesByQuery(wrongTenantNameQuery, 0, null);
// all devices with owner type = TENANT // all devices with owner type = TENANT
EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter));
checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, TEST_TENANT_NAME, "TENANT"); checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, checkFunction);
// all devices with owner type = CUSTOMER // all devices with owner type = CUSTOMER
EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter));
checkEntitiesByQuery(customerEntitiesQuery, 0, null, null); checkEntitiesByQuery(customerEntitiesQuery, 0, null);
} }
@Test @Test
@ -1163,6 +1291,28 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
findByQueryAndCheck(query, 0); findByQueryAndCheck(query, 0);
} }
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer<Integer,EntityData> checkFunction) throws Exception {
await()
.alias("data by query")
.atMost(30, TimeUnit.SECONDS)
.until(() -> {
var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData());
return loadedEntities.size() == expectedNumOfDevices;
});
if (expectedNumOfDevices == 0) {
return;
}
var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData());
Assert.assertEquals(expectedNumOfDevices, loadedEntities.size());
for (int i = 0; i < expectedNumOfDevices; i++) {
checkFunction.accept(i, loadedEntities.get(i));
}
}
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception {
await() await()
.alias("data by query") .alias("data by query")

25
application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java

@ -853,4 +853,29 @@ public class EntityViewControllerTest extends AbstractControllerTest {
EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId(); EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId();
testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId); testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId);
} }
@Test
public void testSaveEntityViewWithUniquifyStrategy() throws Exception {
EntityView view = new EntityView();
view.setEntityId(testDevice.getId());
view.setTenantId(tenantId);
view.setType("default");
view.setName("My unique view");
EntityView savedView = doPost("/api/entityView", view, EntityView.class);
doPost("/api/entityView?nameConflictPolicy=FAIL", view).andExpect(status().isBadRequest());
EntityView secondView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY", view, EntityView.class);
assertThat(secondView.getName()).startsWith("My unique view_");
EntityView thirdView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", view, EntityView.class);
assertThat(thirdView.getName()).startsWith("My unique view-");
EntityView fourthView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class);
assertThat(fourthView.getName()).isEqualTo("My unique view_1");
EntityView fifthEntityView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class);
assertThat(fifthEntityView.getName()).isEqualTo("My unique view_2");
}
} }

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

@ -31,6 +31,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -321,18 +322,15 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
Assert.assertNotNull(referenceValues); Assert.assertNotNull(referenceValues);
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<WidgetTypeInfo>>>() { var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
}); });
Assert.assertNotNull(widgetTypeInfos); Assert.assertNotNull(widgetTypeInfos);
Assert.assertFalse(widgetTypeInfos.isEmpty()); Assert.assertFalse(widgetTypeInfos.isEmpty());
Assert.assertEquals(1, widgetTypeInfos.size()); Assert.assertEquals(1, widgetTypeInfos.size());
var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); var widgetTypeInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0);
Assert.assertNotNull(dashboardInfo); Assert.assertNotNull(widgetTypeInfo);
Assert.assertEquals(new EntityInfo(savedWidgetType.getId(), savedWidgetType.getName()), widgetTypeInfo);
WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class);
Assert.assertNotNull(foundedWidgetType);
Assert.assertEquals(foundedWidgetType, dashboardInfo);
} }
@Test @Test
@ -372,7 +370,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(isSuccess); Assert.assertTrue(isSuccess);
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<WidgetTypeInfo>>>() { var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
}); });
Assert.assertNull(widgetTypeInfos); Assert.assertNull(widgetTypeInfos);
} }
@ -417,7 +415,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
Assert.assertNotNull(referenceValues); Assert.assertNotNull(referenceValues);
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<DashboardInfo>>>() { var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
}); });
Assert.assertNotNull(dashboardInfos); Assert.assertNotNull(dashboardInfos);
Assert.assertFalse(dashboardInfos.isEmpty()); Assert.assertFalse(dashboardInfos.isEmpty());
@ -425,10 +423,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0); var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0);
Assert.assertNotNull(dashboardInfo); Assert.assertNotNull(dashboardInfo);
Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class);
Assert.assertNotNull(foundDashboard);
Assert.assertEquals(foundDashboard, dashboardInfo);
} }
@Test @Test
@ -469,7 +464,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(isSuccess); Assert.assertTrue(isSuccess);
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<DashboardInfo>>>() { var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
}); });
Assert.assertNull(dashboardInfos); Assert.assertNull(dashboardInfos);
} }

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

@ -243,6 +243,29 @@ public class TenantControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorNoFound("Tenant", tenantIdStr)))); .andExpect(statusReason(containsString(msgErrorNoFound("Tenant", tenantIdStr))));
} }
@Test
public void testDeleteTenantByTenantAdmin() throws Exception {
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
Tenant savedTenant = saveTenant(tenant);
//login as tenant admin
User tenantAdminUser = new User();
tenantAdminUser.setAuthority(Authority.TENANT_ADMIN);
tenantAdminUser.setTenantId(savedTenant.getId());
tenantAdminUser.setEmail("tenantToDelete@thingsboard.io");
createUserAndLogin(tenantAdminUser, TENANT_ADMIN_PASSWORD);
String tenantIdStr = savedTenant.getId().getId().toString();
deleteTenant(savedTenant.getId());
loginSysAdmin();
doGet("/api/tenant/" + tenantIdStr)
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Tenant", tenantIdStr))));
}
@Test @Test
public void testFindTenants() throws Exception { public void testFindTenants() throws Exception {
loginSysAdmin(); loginSysAdmin();

152
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java

@ -30,6 +30,8 @@ import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
@ -48,6 +50,7 @@ import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFaP
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -85,7 +88,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
twoFaConfigManager.deletePlatformTwoFaSettings(tenantId); twoFaConfigManager.deletePlatformTwoFaSettings(tenantId);
} }
@Test @Test
public void testSavePlatformTwoFaSettings() throws Exception { public void testSavePlatformTwoFaSettings() throws Exception {
loginSysAdmin(); loginSysAdmin();
@ -102,15 +104,32 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
twoFaSettings.setTotalAllowedTimeForVerification(3600); twoFaSettings.setTotalAllowedTimeForVerification(3600);
twoFaSettings.setEnforceTwoFa(true);
twoFaSettings.setEnforcedUsersFilter(new AllUsersFilter());
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); saveTwoFaSettings(twoFaSettings);
PlatformTwoFaSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), PlatformTwoFaSettings.class); PlatformTwoFaSettings savedTwoFaSettings = findTwoFaSettings();
assertThat(savedTwoFaSettings.getProviders()).hasSize(2); assertThat(savedTwoFaSettings.getProviders()).hasSize(2);
assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig); assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig);
} }
@Test
public void testSavePlatformTwoFaSettingsWithEnforceTwoFaWithoutProviders() throws Exception {
loginSysAdmin();
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(List.of());
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
twoFaSettings.setTotalAllowedTimeForVerification(3600);
twoFaSettings.setEnforceTwoFa(true);
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isBadRequest());
}
@Test @Test
public void testSavePlatformTwoFaSettings_validationError() throws Exception { public void testSavePlatformTwoFaSettings_validationError() throws Exception {
loginSysAdmin(); loginSysAdmin();
@ -157,17 +176,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
assertThat(errorResponse).containsIgnoringCase("verificationCodeLifetime is required"); assertThat(errorResponse).containsIgnoringCase("verificationCodeLifetime is required");
} }
private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
.andExpect(status().isBadRequest()));
}
@Test @Test
public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception {
configureSmsTwoFaProvider("${code}"); configureSmsTwoFaProvider("${code}");
@ -268,24 +276,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
} }
private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception {
TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP")
.andExpect(status().isOk()), TwoFaAccountConfig.class);
assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class);
assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> {
UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build();
assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth");
assertThat(otpAuthUrl.getHost()).isEqualTo("totp");
assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName());
assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL);
assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> {
assertDoesNotThrow(() -> Base32.decode(secretKey));
});
});
return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig;
}
@Test @Test
public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception {
testVerifyAndSaveTotpTwoFaAccountConfig(); testVerifyAndSaveTotpTwoFaAccountConfig();
@ -419,6 +409,56 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig); assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig);
} }
@Test
public void testIsTwoFaEnabled() throws Exception {
configureSmsTwoFaProvider("${code}");
SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig();
accountConfig.setPhoneNumber("+38050505050");
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUser, accountConfig);
assertThat(twoFactorAuthService.isTwoFaEnabled(tenantId, tenantAdminUser)).isTrue();
}
@Test
public void testDeleteTwoFaAccountConfig() throws Exception {
configureSmsTwoFaProvider("${code}");
loginTenantAdmin();
SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig();
accountConfig.setPhoneNumber("+38050505050");
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUser, accountConfig);
AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class);
TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS);
assertThat(savedAccountConfig).isEqualTo(accountConfig);
PlatformTwoFaSettings twoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, true).get();
twoFaSettings.setEnforceTwoFa(true);
TenantAdministratorsFilter enforcedUsersFilter = new TenantAdministratorsFilter();
enforcedUsersFilter.setTenantsIds(Set.of(tenantId.getId()));
twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter);
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
String errorMessage = getErrorMessage(doDelete("/api/2fa/account/config?providerType=SMS")
.andExpect(status().isBadRequest()));
assertThat(errorMessage).isEqualTo("At least one 2FA provider is required");
twoFaSettings.setEnforceTwoFa(false);
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk());
assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs())
.doesNotContainKey(TwoFaProviderType.SMS);
}
private PlatformTwoFaSettings findTwoFaSettings() throws Exception {
return doGet("/api/2fa/settings", PlatformTwoFaSettings.class);
}
private void saveTwoFaSettings(PlatformTwoFaSettings twoFaSettings) throws Exception {
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
}
private TotpTwoFaProviderConfig configureTotpTwoFaProvider() throws Exception { private TotpTwoFaProviderConfig configureTotpTwoFaProvider() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb"); totpTwoFaProviderConfig.setIssuerName("tb");
@ -441,37 +481,35 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
twoFaSettings.setMinVerificationCodeSendPeriod(5); twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100); twoFaSettings.setTotalAllowedTimeForVerification(100);
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); saveTwoFaSettings(twoFaSettings);
} }
@Test private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception {
public void testIsTwoFaEnabled() throws Exception { TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP")
configureSmsTwoFaProvider("${code}"); .andExpect(status().isOk()), TwoFaAccountConfig.class);
SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class);
accountConfig.setPhoneNumber("+38050505050");
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
assertThat(twoFactorAuthService.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue(); assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> {
UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build();
assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth");
assertThat(otpAuthUrl.getHost()).isEqualTo("totp");
assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName());
assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL);
assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> {
assertDoesNotThrow(() -> Base32.decode(secretKey));
});
});
return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig;
} }
@Test private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
public void testDeleteTwoFaAccountConfig() throws Exception { PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
configureSmsTwoFaProvider("${code}"); twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); twoFaSettings.setMinVerificationCodeSendPeriod(5);
accountConfig.setPhoneNumber("+38050505050"); twoFaSettings.setTotalAllowedTimeForVerification(100);
loginTenantAdmin();
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class);
TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS);
assertThat(savedAccountConfig).isEqualTo(accountConfig);
doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk());
assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
.doesNotContainKey(TwoFaProviderType.SMS); .andExpect(status().isBadRequest()));
} }
} }

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

@ -25,6 +25,7 @@ import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
@ -58,6 +60,7 @@ import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -116,7 +119,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
public void testTwoFa_totp() throws Exception { public void testTwoFa_totp() throws Exception {
TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa();
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
doPost("/api/auth/2fa/verification/send?providerType=TOTP") doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isOk()); .andExpect(status().isOk());
@ -136,7 +139,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
public void testTwoFa_sms() throws Exception { public void testTwoFa_sms() throws Exception {
configureSmsTwoFa(); configureSmsTwoFa();
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
doPost("/api/auth/2fa/verification/send?providerType=SMS") doPost("/api/auth/2fa/verification/send?providerType=SMS")
.andExpect(status().isOk()); .andExpect(status().isOk());
@ -160,7 +163,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setTotalAllowedTimeForVerification(65); twoFaSettings.setTotalAllowedTimeForVerification(65);
}); });
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
await("expiration of the pre-verification token") await("expiration of the pre-verification token")
.atLeast(Duration.ofSeconds(30).plusMillis(500)) .atLeast(Duration.ofSeconds(30).plusMillis(500))
@ -177,7 +180,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
}); });
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
Stream.generate(() -> StringUtils.randomNumeric(6)) Stream.generate(() -> StringUtils.randomNumeric(6))
.limit(9) .limit(9)
@ -206,7 +209,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setMinVerificationCodeSendPeriod(10); twoFaSettings.setMinVerificationCodeSendPeriod(10);
}); });
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
doPost("/api/auth/2fa/verification/send?providerType=TOTP") doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isOk()); .andExpect(status().isOk());
@ -230,7 +233,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); twoFaSettings.setVerificationCodeCheckRateLimit("3:10");
}); });
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
@ -258,7 +261,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
@Test @Test
public void testCheckVerificationCode_invalidVerificationCode() throws Exception { public void testCheckVerificationCode_invalidVerificationCode() throws Exception {
configureTotpTwoFa(); configureTotpTwoFa();
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) {
String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + invalidVerificationCode) String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + invalidVerificationCode)
@ -273,7 +276,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
smsTwoFaProviderConfig.setVerificationCodeLifetime(10); smsTwoFaProviderConfig.setVerificationCodeLifetime(10);
}); });
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
doPost("/api/auth/2fa/verification/send?providerType=SMS").andExpect(status().isOk()); doPost("/api/auth/2fa/verification/send?providerType=SMS").andExpect(status().isOk());
@ -296,7 +299,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
public void testTwoFa_logLoginAction() throws Exception { public void testTwoFa_logLoginAction() throws Exception {
TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa();
logInWithPreVerificationToken(username, password); logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
await("async audit log saving").during(1, TimeUnit.SECONDS); await("async audit log saving").during(1, TimeUnit.SECONDS);
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
@ -334,7 +337,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
@Test @Test
public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException {
configureTotpTwoFa(); configureTotpTwoFa();
twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), TwoFaProviderType.TOTP); twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user, TwoFaProviderType.TOTP);
assertDoesNotThrow(() -> { assertDoesNotThrow(() -> {
login(username, password); login(username, password);
@ -368,17 +371,17 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(twoFaUser, TwoFaProviderType.TOTP); TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(twoFaUser, TwoFaProviderType.TOTP);
totpTwoFaAccountConfig.setUseByDefault(true); totpTwoFaAccountConfig.setUseByDefault(true);
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), totpTwoFaAccountConfig); twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, totpTwoFaAccountConfig);
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38012312322"); smsTwoFaAccountConfig.setPhoneNumber("+38012312322");
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), smsTwoFaAccountConfig); twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, smsTwoFaAccountConfig);
EmailTwoFaAccountConfig emailTwoFaAccountConfig = new EmailTwoFaAccountConfig(); EmailTwoFaAccountConfig emailTwoFaAccountConfig = new EmailTwoFaAccountConfig();
emailTwoFaAccountConfig.setEmail(twoFaUser.getEmail()); emailTwoFaAccountConfig.setEmail(twoFaUser.getEmail());
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), emailTwoFaAccountConfig); twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, emailTwoFaAccountConfig);
logInWithPreVerificationToken(twoFaUser.getEmail(), "12345678"); logInWithMfaToken(twoFaUser.getEmail(), "12345678", Authority.PRE_VERIFICATION_TOKEN);
Map<TwoFaProviderType, TwoFactorAuthController.TwoFaProviderInfo> providersInfos = readResponse(doGet("/api/auth/2fa/providers").andExpect(status().isOk()), new TypeReference<List<TwoFactorAuthController.TwoFaProviderInfo>>() {}).stream() Map<TwoFaProviderType, TwoFactorAuthController.TwoFaProviderInfo> providersInfos = readResponse(doGet("/api/auth/2fa/providers").andExpect(status().isOk()), new TypeReference<List<TwoFactorAuthController.TwoFaProviderInfo>>() {}).stream()
.collect(Collectors.toMap(TwoFactorAuthController.TwoFaProviderInfo::getType, v -> v)); .collect(Collectors.toMap(TwoFactorAuthController.TwoFaProviderInfo::getType, v -> v));
@ -395,13 +398,48 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
assertThat(providersInfos.get(TwoFaProviderType.EMAIL).isDefault()).isFalse(); assertThat(providersInfos.get(TwoFaProviderType.EMAIL).isDefault()).isFalse();
} }
private void logInWithPreVerificationToken(String username, String password) throws Exception { @Test
public void testEnforceTwoFa() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
twoFaSettings.setEnforceTwoFa(true);
TenantAdministratorsFilter enforcedUsersFilter = new TenantAdministratorsFilter();
enforcedUsersFilter.setTenantsIds(Set.of(tenantId.getId()));
twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter);
twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
logInWithMfaToken(username, password, Authority.MFA_CONFIGURATION_TOKEN);
TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, totpTwoFaProviderConfig.getProviderType());
String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret");
String verificationCode = new Totp(secret).now();
readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class);
JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class);
assertThat(tokenPair.getToken()).isNotEmpty();
assertThat(tokenPair.getRefreshToken()).isNotEmpty();
validateAndSetJwtToken(tokenPair, username);
doGet("/api/user/" + user.getId()).andExpect(status().isOk());
// verifying enforced users filter
createDifferentTenant();
doGet("/api/user/" + savedDifferentTenantUser.getId()).andExpect(status().isOk());
}
private void logInWithMfaToken(String username, String password, Authority expectedScope) throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password); LoginRequest loginRequest = new LoginRequest(username, password);
JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class); JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class);
assertThat(response.getToken()).isNotNull(); assertThat(response.getToken()).isNotNull();
assertThat(response.getRefreshToken()).isNull(); assertThat(response.getRefreshToken()).isNull();
assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); assertThat(response.getScope()).isEqualTo(expectedScope);
this.token = response.getToken(); this.token = response.getToken();
} }
@ -418,7 +456,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP); TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP);
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user, totpTwoFaAccountConfig);
return totpTwoFaAccountConfig; return totpTwoFaAccountConfig;
} }
@ -436,7 +474,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); smsTwoFaAccountConfig.setPhoneNumber("+38050505050");
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user, smsTwoFaAccountConfig);
return smsTwoFaAccountConfig; return smsTwoFaAccountConfig;
} }

196
application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java

@ -0,0 +1,196 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class AiModelEdgeTest extends AbstractEdgeTest {
private static final String DEFAULT_AI_MODEL_NAME = "Edge Test AiModel";
private static final String UPDATED_AI_MODEL_NAME = "Updated Edge Test AiModel";
@Test
public void testAiModel_create_update_delete() throws Exception {
// create AiModel
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
edgeImitator.expectMessageAmount(1);
AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
AiModelUpdateMsg aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
AiModel aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
Assert.assertEquals(savedAiModel.getTenantId(), aiModelFromMsg.getTenantId());
// update AiModel
edgeImitator.expectMessageAmount(1);
savedAiModel.setName(UPDATED_AI_MODEL_NAME);
savedAiModel = doPost("/api/ai/model", savedAiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(UPDATED_AI_MODEL_NAME, aiModelFromMsg.getName());
// delete AiModel
edgeImitator.expectMessageAmount(1);
doDelete("/api/ai/model/" + savedAiModel.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
}
@Test
public void testSendAiModelToCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
}
@Test
public void testUpdateAiModelNameOnCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
aiModel.setName(UPDATED_AI_MODEL_NAME);
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
checkAiModelOnCloud(updatedUplinkMsg, uuid, aiModel.getName());
}
@Test
public void testAiModelToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
edgeImitator.expectMessageAmount(1);
AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<AiModelUpdateMsg> aiModelUpdateMsgOpt = edgeImitator.findMessageByType(AiModelUpdateMsg.class);
Assert.assertTrue(aiModelUpdateMsgOpt.isPresent());
AiModelUpdateMsg latestAiModelUpdateMsg = aiModelUpdateMsgOpt.get();
AiModel aiModelFromMsg = JacksonUtil.fromString(latestAiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
Assert.assertNotEquals(savedAiModel.getUuidId(), uuid);
AiModel aiModelFromCloud = doGet("/api/ai/model/" + uuid, AiModel.class);
Assert.assertNotNull(aiModelFromCloud);
Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromCloud.getName());
}
private AiModel createSimpleAiModel(String name) {
AiModel aiModel = new AiModel();
aiModel.setTenantId(tenantId);
aiModel.setName(name);
aiModel.setConfiguration(OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig(null, "test-api-key"))
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build());
return aiModel;
}
private UplinkMsg getUplinkMsg(UUID uuid, AiModel aiModel, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
AiModelUpdateMsg.Builder aiModelUpdateMsgBuilder = AiModelUpdateMsg.newBuilder();
aiModelUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
aiModelUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
aiModelUpdateMsgBuilder.setEntity(JacksonUtil.toString(aiModel));
aiModelUpdateMsgBuilder.setMsgType(updateMsgType);
testAutoGeneratedCodeByProtobuf(aiModelUpdateMsgBuilder);
uplinkMsgBuilder.addAiModelUpdateMsg(aiModelUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
return uplinkMsgBuilder.build();
}
private void checkAiModelOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
AiModel aiModel = doGet("/api/ai/model/" + uuid, AiModel.class);
Assert.assertNotNull(aiModel);
Assert.assertEquals(resourceTitle, aiModel.getName());
}
}

6
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -29,6 +29,7 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient;
import org.thingsboard.edge.rpc.EdgeRpcClient; import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest; import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -358,6 +359,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(calculatedFieldUpdateMsg)); result.add(saveDownlinkMsg(calculatedFieldUpdateMsg));
} }
} }
if (downlinkMsg.getAiModelUpdateMsgCount() > 0) {
for (AiModelUpdateMsg aiModelUpdateMsg : downlinkMsg.getAiModelUpdateMsgList()) {
result.add(saveDownlinkMsg(aiModelUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) { if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
} }

23
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java

@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
@ -257,7 +258,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull(); assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope()); assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo( assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode() JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "LEFT") .put("allowedZonesEvent", "LEFT")
.put("allowedZonesStatus", "OUTSIDE") .put("allowedZonesStatus", "OUTSIDE")
@ -329,7 +330,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull(); assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope()); assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo( assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode() JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "LEFT") .put("allowedZonesEvent", "LEFT")
.put("restrictedZonesEvent", "ENTERED") .put("restrictedZonesEvent", "ENTERED")
@ -401,7 +402,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull(); assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope()); assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo( assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode() JacksonUtil.newObjectNode()
.put("allowedZonesStatus", "OUTSIDE") .put("allowedZonesStatus", "OUTSIDE")
.put("restrictedZonesStatus", "INSIDE") .put("restrictedZonesStatus", "INSIDE")
@ -453,21 +454,15 @@ public class GeofencingCalculatedFieldStateTest {
config.setEntityCoordinates(entityCoordinates); config.setEntityCoordinates(entityCoordinates);
ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "AllowedZone")));
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
allowedZonesGroup.setRelationType("CurrentZone"); allowedZonesGroup.setRelationType("CurrentZone");
allowedZonesGroup.setDirection(EntitySearchDirection.TO); allowedZonesGroup.setDirection(EntitySearchDirection.TO);
ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true);
var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "RestrictedZone")));
restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone");
restrictedZoneDynamicSourceConfiguration.setMaxLevel(1);
restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration);
restrictedZonesGroup.setRelationType("CurrentZone"); restrictedZonesGroup.setRelationType("CurrentZone");
restrictedZonesGroup.setDirection(EntitySearchDirection.TO); restrictedZonesGroup.setDirection(EntitySearchDirection.TO);

3
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -123,7 +123,8 @@ public class SimpleCalculatedFieldStateTest {
Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L));
assertThatThrownBy(() -> state.updateState(ctx, newArgs)) assertThatThrownBy(() -> state.updateState(ctx, newArgs))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Rolling argument entry is not supported for simple calculated fields."); .hasMessage("Unsupported argument type detected for argument: key3. " +
"Rolling argument entry is not supported for simple calculated fields.");
} }
@Test @Test

2
application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java

@ -253,7 +253,7 @@ class DefaultTbAiModelServiceTest {
private static AiModelConfig constructValidOpenAiModelConfig() { private static AiModelConfig constructValidOpenAiModelConfig() {
return OpenAiChatModelConfig.builder() return OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key")) .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build())
.modelId("gpt-4o") .modelId("gpt-4o")
.temperature(0.5) .temperature(0.5)
.topP(0.3) .topP(0.3)

83
application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java

@ -0,0 +1,83 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.resource;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.GeneralFileDescriptor;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@DaoSqlTest
public class DefaultResourceDataCacheTest extends AbstractControllerTest {
@MockitoSpyBean
private ResourceService resourceService;
@Autowired
private TbResourceService tbResourceService;
@MockitoSpyBean
private TbResourceDataCache resourceDataCache;
@Test
public void testGetCachedResourceData() throws Exception {
loginTenantAdmin();
TbResource resource = new TbResource();
resource.setTenantId(tenantId);
resource.setTitle("File for AI request");
resource.setResourceType(ResourceType.GENERAL);
resource.setFileName("myTestJson.json");
GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json");
resource.setDescriptorValue(descriptor);
byte[] data = "This is a test prompt for AI request.".getBytes();
resource.setData(data);
TbResourceInfo savedResource = tbResourceService.save(resource);
verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId());
TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedData.getData()).isEqualTo(data);
assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor);
verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId());
// retrieve resource data second time
clearInvocations(resourceService);
TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedData2.getData()).isEqualTo(data);
verifyNoMoreInteractions(resourceService);
// delete resource, check cache
TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId());
tbResourceService.delete(resourceById, true, null);
verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId());
TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedDataAfterDeletion).isEqualTo(null);
}
}

125
application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.resource.sql;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -24,8 +25,10 @@ import org.junit.Test;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.ai.TbAiNode;
import org.thingsboard.rule.engine.ai.TbAiNodeConfiguration;
import org.thingsboard.rule.engine.ai.TbResponseFormat;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -37,26 +40,40 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.resource.TbResourceService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -135,6 +152,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
private WidgetTypeService widgetTypeService; private WidgetTypeService widgetTypeService;
@Autowired @Autowired
private DashboardService dashboardService; private DashboardService dashboardService;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private AiModelService aiModelService;
private Tenant savedTenant; private Tenant savedTenant;
private User tenantAdmin; private User tenantAdmin;
@ -453,11 +474,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertFalse(result.getReferences().isEmpty()); Assert.assertFalse(result.getReferences().isEmpty());
Assert.assertEquals(1, result.getReferences().size()); Assert.assertEquals(1, result.getReferences().size());
WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); EntityInfo widgetTypeInfo = (EntityInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0);
WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType);
Assert.assertNotNull(widgetTypeInfo); Assert.assertNotNull(widgetTypeInfo);
Assert.assertNotNull(foundWidgetTypeInfo); Assert.assertEquals(widgetTypeInfo, new EntityInfo(foundWidgetType.getId(), foundWidgetType.getName()));
Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo);
TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId()); TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId());
Assert.assertNotNull(foundResource); Assert.assertNotNull(foundResource);
@ -546,11 +565,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNotNull(result.getReferences()); Assert.assertNotNull(result.getReferences());
Assert.assertEquals(1, result.getReferences().size()); Assert.assertEquals(1, result.getReferences().size());
DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); EntityInfo dashboardInfo = (EntityInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0);
DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId());
Assert.assertNotNull(dashboardInfo); Assert.assertNotNull(dashboardInfo);
Assert.assertNotNull(foundDashboardInfo); Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
Assert.assertEquals(foundDashboardInfo, dashboardInfo);
foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
Assert.assertNotNull(foundResource); Assert.assertNotNull(foundResource);
@ -598,6 +615,94 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNull(foundResource); Assert.assertNull(foundResource);
} }
@Test
public void testShouldNotDeleteResourceIfUsedInAiNode() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.GENERAL);
resource.setTitle("My resource");
resource.setFileName("test.json");
resource.setTenantId(savedTenant.getId());
resource.setData("<test></test>".getBytes());
TbResourceInfo savedResource = tbResourceService.save(resource);
RuleChainMetaData ruleChain = createRuleChainReferringResource(savedResource.getId());
TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null);
assertThat(result).isNotNull();
assertThat(result.isSuccess()).isFalse();
assertThat(result.getReferences()).isNotEmpty().hasSize(1);
EntityInfo entityInfo = (EntityInfo) result.getReferences().get(EntityType.RULE_CHAIN.name()).get(0);
assertThat(entityInfo).isEqualTo(new EntityInfo(ruleChain.getRuleChainId(), "Test"));
TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
assertThat(foundResource).isNotNull();
// force delete
TbResourceDeleteResult deleteResult = tbResourceService.delete(savedResource, true, null);
assertThat(deleteResult).isNotNull();
assertThat(deleteResult.isSuccess()).isTrue();
TbResource resourceAfterDeletion = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
assertThat(resourceAfterDeletion).isNull();
}
private RuleChainMetaData createRuleChainReferringResource(TbResourceId resourceId) {
AiModel model = constructValidOpenAiModel("Test model");
AiModel saved = aiModelService.save(model);
RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId);
ruleChain.setName("Test");
ruleChain.setType(RuleChainType.CORE);
ruleChain.setDebugMode(true);
ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
ruleChain = ruleChainService.saveRuleChain(ruleChain);
RuleChainId ruleChainId = ruleChain.getId();
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChainId);
RuleNode aiNode = new RuleNode();
aiNode.setName("Ai request");
aiNode.setType(org.thingsboard.rule.engine.ai.TbAiNode.class.getName());
aiNode.setConfigurationVersion(TbAiNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
aiNode.setDebugSettings(DebugSettings.all());
TbAiNodeConfiguration configuration = new TbAiNodeConfiguration();
configuration.setResourceIds(Set.of(resourceId.getId()));
configuration.setModelId(saved.getId());
configuration.setResponseFormat(new TbResponseFormat.TbJsonResponseFormat());
configuration.setTimeoutSeconds(1);
configuration.setUserPrompt("What is temp");
aiNode.setConfiguration(JacksonUtil.valueToTree(configuration));
metaData.setNodes(Arrays.asList(aiNode));
metaData.setFirstNodeIndex(0);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId);
}
private AiModel constructValidOpenAiModel(String name) {
var modelConfig = OpenAiChatModelConfig.builder()
.providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key")
.build())
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build();
return AiModel.builder()
.tenantId(tenantId)
.name(name)
.configuration(modelConfig)
.build();
}
@Test @Test
public void testFindTenantResourcesByTenantId() throws Exception { public void testFindTenantResourcesByTenantId() throws Exception {
loginSysAdmin(); loginSysAdmin();

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

@ -125,7 +125,7 @@ public class JwtTokenFactoryTest {
public void testCreateAndParsePreVerificationJwtToken() { public void testCreateAndParsePreVerificationJwtToken() {
SecurityUser securityUser = createSecurityUser(); SecurityUser securityUser = createSecurityUser();
int tokenLifetime = (int) TimeUnit.MINUTES.toSeconds(30); int tokenLifetime = (int) TimeUnit.MINUTES.toSeconds(30);
JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime); JwtToken preVerificationToken = tokenFactory.createMfaToken(securityUser, Authority.PRE_VERIFICATION_TOKEN, tokenLifetime);
checkExpirationTime(preVerificationToken, tokenLifetime); checkExpirationTime(preVerificationToken, tokenLifetime);
SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.getToken()); SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.getToken());

61
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java

@ -114,6 +114,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
initRpc(0); initRpc(0);
} else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationReadCollectedValueTest")) { } else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationReadCollectedValueTest")) {
initRpc(3303); initRpc(3303);
} else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationInitReadCompositeAllTest")) {
initRpc(2);
}else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationInitReadCompositeByObjectTest")) {
initRpc(3);
} else { } else {
initRpc(1); initRpc(1);
} }
@ -220,10 +224,67 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
" ],\n" + " ],\n" +
" \"attributeLwm2m\": {}\n" + " \"attributeLwm2m\": {}\n" +
" }"; " }";
String INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_ALL =
" {\n" +
" \"keyName\": {\n" +
" \"/3_1.2/0/9\": \"batteryLevel\",\n" +
" \"/3_1.2/0/20\": \"batteryStatus\",\n" +
" \"/19_1.1/0/2\": \"dataCreationTime\",\n" +
" \"/5_1.2/0/6\": \"pkgname\",\n" +
" \"/5_1.2/0/7\": \"pkgversion\",\n" +
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/3_1.2/0/20\"\n" +
" ],\n" +
" \"attribute\": [\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\"\n" +
" ],\n" +
" \"telemetry\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/3_1.2/0/20\",\n" +
" \"/5_1.2/0/9\",\n" +
" \"/19_1.1/0/2\"\n" +
" ],\n" +
" \"attributeLwm2m\": {},\n" +
" \"initAttrTelAsObsStrategy\": true,\n" +
" \"observeStrategy\": 1\n" +
" }";
String INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_BY_OBJECT =
" {\n" +
" \"keyName\": {\n" +
" \"/3_1.2/0/9\": \"batteryLevel\",\n" +
" \"/3_1.2/0/20\": \"batteryStatus\",\n" +
" \"/19_1.1/0/2\": \"dataCreationTime\",\n" +
" \"/5_1.2/0/6\": \"pkgname\",\n" +
" \"/5_1.2/0/7\": \"pkgversion\",\n" +
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/3_1.2/0/9\"\n" +
" ],\n" +
" \"attribute\": [\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\"\n" +
" ],\n" +
" \"telemetry\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/3_1.2/0/20\",\n" +
" \"/5_1.2/0/9\",\n" +
" \"/19_1.1/0/2\"\n" +
" ],\n" +
" \"attributeLwm2m\": {},\n" +
" \"initAttrTelAsObsStrategy\": true,\n" +
" \"observeStrategy\": 2\n" +
" }";
CONFIG_PROFILE_WITH_PARAMS_RPC = CONFIG_PROFILE_WITH_PARAMS_RPC =
switch (typeConfigProfile) { switch (typeConfigProfile) {
case 0 -> ATTRIBUTES_TELEMETRY_WITH_PARAMS_RPC_WITH_OBSERVE; case 0 -> ATTRIBUTES_TELEMETRY_WITH_PARAMS_RPC_WITH_OBSERVE;
case 1 -> TELEMETRY_WITH_PARAMS_RPC_WITHOUT_OBSERVE; case 1 -> TELEMETRY_WITH_PARAMS_RPC_WITHOUT_OBSERVE;
case 2 -> INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_ALL;
case 3 -> INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_BY_OBJECT;
case 3303 -> TELEMETRY_WITH_PARAMS_RPC_COLLECTED_VALUE; case 3303 -> TELEMETRY_WITH_PARAMS_RPC_COLLECTED_VALUE;
default -> throw new IllegalStateException("Unexpected value: " + typeConfigProfile); default -> throw new IllegalStateException("Unexpected value: " + typeConfigProfile);
}; };

92
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java

@ -0,0 +1,92 @@
/**
* Copyright © 2016-2025 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.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
@Slf4j
public class RpcLwm2mIntegrationInitReadCompositeAllTest extends AbstractRpcLwM2MIntegrationTest {
/**
" \"/3_1.2/0/9\": \"batteryLevel\", - Telemetry
" \"/3_1.2/0/20\": \"batteryStatus\" - Observe, Telemetry
" \"/5_1.2/0/6\": \"pkgname\" - Attributes
" \"/5_1.2/0/7\": \"pkgversion\" - Attributes
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\ - Telemetry
" \"/19_1.1/0/2\": \"dataCreationTime\" - Telemetry
* "observeStrategy": 1
*/
@Test
public void testInitReadCompositeAsObserveStrategyCompositeAll() throws Exception {
// init test
String RESOURCE_3_9 = "batteryLevel";
String RESOURCE_3_20 = "batteryStatus";
String RESOURCE_5_6 = "pkgname";
String RESOURCE_5_7 = "pkgversion";
String RESOURCE_5_9 = "firmwareUpdateDeliveryMethod";
String RESOURCE_19_2 = "dataCreationTime";
String idVwr_3_0_20 = idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + 20;
String IdVer5_0_6 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_6;
String IdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String IdVer5_0_9 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_9;
String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
countUpdateAttrTelemetryResource(idVer_3_0_9);
countUpdateAttrTelemetryResource(idVwr_3_0_20);
countUpdateAttrTelemetryResource(IdVer5_0_6);
countUpdateAttrTelemetryResource(IdVer5_0_7);
countUpdateAttrTelemetryResource(IdVer5_0_9);
countUpdateAttrTelemetryResource(idVer_19_0_2);
AtomicReference<ObjectNode> actualValues = new AtomicReference<>();
await().atMost(40, SECONDS).until(() -> {
actualValues.set(doGetAsync(
"/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys="
+ RESOURCE_3_9 + "," + RESOURCE_3_20 + "," + RESOURCE_5_9 + "," + RESOURCE_19_2, ObjectNode.class));
return actualValues.get() != null && !actualValues.get().isEmpty()
&& !actualValues.get().get(RESOURCE_3_9).isEmpty()
&& !actualValues.get().get(RESOURCE_3_20).isEmpty()
&& !actualValues.get().get(RESOURCE_5_9).isEmpty()
&& !actualValues.get().get(RESOURCE_19_2).isEmpty();
});
AtomicReference<List<String>> actualKeys =new AtomicReference<>();
await().atMost(40, SECONDS).until(() -> {
actualKeys.set(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {
}));
return actualKeys.get() != null && !actualKeys.get().isEmpty() && !actualKeys.get().isEmpty()
&& actualKeys.get().contains(RESOURCE_5_6)&& actualKeys.get().contains(RESOURCE_5_7);
});
}
}

92
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java

@ -0,0 +1,92 @@
/**
* Copyright © 2016-2025 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.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
@Slf4j
public class RpcLwm2mIntegrationInitReadCompositeByObjectTest extends AbstractRpcLwM2MIntegrationTest {
/**
" \"/3_1.2/0/9\": \"batteryLevel\", - Telemetry
" \"/3_1.2/0/20\": \"batteryStatus\" - Observe, Telemetry
" \"/5_1.2/0/6\": \"pkgname\" - Attributes
" \"/5_1.2/0/7\": \"pkgversion\" - Attributes
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\ - Telemetry
" \"/19_1.1/0/2\": \"dataCreationTime\" - Telemetry
* "observeStrategy": 2
*/
@Test
public void testInitReadCompositeAsObserveStrategyCompositeByObject() throws Exception {
// init test
String RESOURCE_3_9 = "batteryLevel";
String RESOURCE_3_20 = "batteryStatus";
String RESOURCE_5_6 = "pkgname";
String RESOURCE_5_7 = "pkgversion";
String RESOURCE_5_9 = "firmwareUpdateDeliveryMethod";
String RESOURCE_19_2 = "dataCreationTime";
String idVwr_3_0_20 = idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + 20;
String IdVer5_0_6 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_6;
String IdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String IdVer5_0_9 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_9;
String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
countUpdateAttrTelemetryResource(idVer_3_0_9);
countUpdateAttrTelemetryResource(idVwr_3_0_20);
countUpdateAttrTelemetryResource(IdVer5_0_6);
countUpdateAttrTelemetryResource(IdVer5_0_7);
countUpdateAttrTelemetryResource(IdVer5_0_9);
countUpdateAttrTelemetryResource(idVer_19_0_2);
AtomicReference<ObjectNode> actualValues = new AtomicReference<>();
await().atMost(40, SECONDS).until(() -> {
actualValues.set(doGetAsync(
"/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys="
+ RESOURCE_3_9 + "," + RESOURCE_3_20 + "," + RESOURCE_5_9 + "," + RESOURCE_19_2, ObjectNode.class));
return actualValues.get() != null && !actualValues.get().isEmpty()
&& !actualValues.get().get(RESOURCE_3_9).isEmpty()
&& !actualValues.get().get(RESOURCE_3_20).isEmpty()
&& !actualValues.get().get(RESOURCE_5_9).isEmpty()
&& !actualValues.get().get(RESOURCE_19_2).isEmpty();
});
AtomicReference<List<String>> actualKeys =new AtomicReference<>();
await().atMost(40, SECONDS).until(() -> {
actualKeys.set(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {
}));
return actualKeys.get() != null && !actualKeys.get().isEmpty() && !actualKeys.get().isEmpty()
&& actualKeys.get().contains(RESOURCE_5_6)&& actualKeys.get().contains(RESOURCE_5_7);
});
}
}

39
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java

@ -25,10 +25,10 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
import org.thingsboard.server.transport.mqtt.gateway.GatewayMetricsService; import org.thingsboard.server.transport.mqtt.gateway.GatewayMetricsService;
import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata;
import org.thingsboard.server.transport.mqtt.gateway.metrics.GatewayMetricsState; import org.thingsboard.server.transport.mqtt.gateway.metrics.GatewayMetricsState;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
@ -43,6 +43,7 @@ import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
@ -112,6 +113,42 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt
processGatewayTelemetryTest(GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); processGatewayTelemetryTest(GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2);
} }
@Test
public void testAckIsReceivedOnFailedPublishMessage() throws Exception {
String devicePayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]";
String payloadA = "{\"Device A\": " + devicePayload + "}";
String deviceBPayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]";
String payloadB = "{\"Device B\": " + deviceBPayload + "}";
testAckIsReceivedOnFailedPublishMessage("Device A", payloadA.getBytes(), "Device B", payloadB.getBytes());
}
protected void testAckIsReceivedOnFailedPublishMessage(String deviceName1, byte[] payload1, String deviceName2, byte[] payload2) throws Exception {
updateDefaultTenantProfileConfig(profileConfiguration -> {
profileConfiguration.setMaxDevices(3);
});
MqttTestClient client = new MqttTestClient();
client.connectAndWait(gatewayAccessToken);
client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload1);
// check device is created
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertNotNull(doGet("/api/tenant/devices?deviceName=" + deviceName1, Device.class));
});
client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload2);
client.disconnectAndWait();
// check device was not created due to limit
doGet("/api/tenant/devices?deviceName=" + deviceName2).andExpect(status().isNotFound());
updateDefaultTenantProfileConfig(profileConfiguration -> {
profileConfiguration.setMaxDevices(0);
});
}
@Test @Test
public void testGatewayConnect() throws Exception { public void testGatewayConnect() throws Exception {
String payload = "{\"device\":\"Device A\"}"; String payload = "{\"device\":\"Device A\"}";

11
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java

@ -186,4 +186,15 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract
assertFalse(callback.isPubAckReceived()); assertFalse(callback.isPubAckReceived());
} }
@Override
public void testAckIsReceivedOnFailedPublishMessage() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Post Telemetry device json payload")
.gatewayName("Test Post Telemetry gateway json payload")
.transportPayloadType(TransportPayloadType.JSON)
.telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC)
.build();
processBeforeTest(configProperties);
super.testAckIsReceivedOnFailedPublishMessage();
}
} }

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

Loading…
Cancel
Save