Browse Source

Merge remote-tracking branch 'upstream/master' into api-key

pull/14074/head
Andrii Landiak 11 months ago
parent
commit
a2cce16dc1
  1. 2
      application/src/main/data/resources/dashboards/gateways_dashboard.json
  2. 66
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  3. 11
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  4. 2
      application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java
  5. 14
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  6. 2
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  7. 47
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  8. 50
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  9. 41
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
  10. 19
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  12. 11
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java
  14. 13
      application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java
  15. 24
      application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java
  16. 22
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  17. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java
  18. 49
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  19. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java
  20. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java
  21. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  22. 32
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  23. 22
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  24. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  25. 10
      application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java
  26. 28
      application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java
  27. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  28. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  29. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  30. 2
      application/src/main/resources/thingsboard.yml
  31. 50
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  32. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  33. 45
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  34. 23
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  35. 152
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java
  36. 74
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  37. 2
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  38. 1
      application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java
  39. 43
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  40. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java
  41. 174
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  42. 72
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  43. 92
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java
  44. 92
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java
  45. 32
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java
  46. 37
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java
  47. 29
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java
  48. 37
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java
  49. 39
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java
  50. 29
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java
  51. 31
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java
  52. 50
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java
  53. 13
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java
  54. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java
  55. 8
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java
  56. 6
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java
  57. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java
  58. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  59. 112
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java
  60. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java
  61. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java
  62. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java
  63. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java
  64. 19
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java
  65. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java
  66. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java
  67. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
  68. 3
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java
  69. 13
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  70. 6
      common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java
  71. 4
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java
  72. 17
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java
  73. 326
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
  74. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java
  75. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  76. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  77. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java
  78. 54
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  79. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  80. 7
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java
  81. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
  82. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  83. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
  84. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  85. 6
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
  86. 4
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java
  87. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java
  88. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
  89. 11
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
  90. 49
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java
  91. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java
  92. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
  93. 5
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java
  94. 12
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java
  95. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java
  96. 2
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  97. 2
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  98. 4
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java
  99. 27
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  100. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

2
application/src/main/data/resources/dashboards/gateways_dashboard.json

@ -650,7 +650,7 @@
"settings": { "settings": {
"useMarkdownTextFunction": true, "useMarkdownTextFunction": true,
"markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.", "markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.",
"markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Logs\");\nfunction generateMatHeader(index) {\n if (index !== undefined && index > -1) {\n return `<mat-card-header class='tb-home-widget-link' (click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[${index}], ctx.datasources[0].entity.id)\">`\n } else {\n return \"<mat-card-header>\"\n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n <mat-card style=\"flex-grow: 1; width: ${mobile ? '100%' : 'auto'}; min-height: ${mobile ? 'auto' : '57px'}\" class=\"${dividerStyle}\">\n <div class=\"divider\"></div>\n <mat-divider vertical style=\"height:100%\"></mat-divider>\n ${generateMatHeader(index)}\n <mat-card-subtitle>${label}</mat-card-subtitle>\n </mat-card-header>\n <mat-card-content> ${value}</mat-card-content>\n </mat-card>`;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\" ? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\nif (data[0].Version) {\n createDataBlock(data[0].Version, \"Gateway Version\", '');\n}\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `<span style=\"color:rgb(25,128,56)\">${(data[1] ? data[1].count : 0)} </span>`\n + \" | \" +\n `<span style=\"color:rgb(203,37,48)\">${(data[2] ? data[2][\"count 2\"] : 0)} </span>`\n , \"Devices <span class='tb-hint' style='padding-left: 0'>(Active | Inactive)</span>\", '');\ncreateDataBlock(\n `<span style=\"color:rgb(25,128,56)\">${(data[0].active_connectors ? JSON.parse(data[0].active_connectors).length : 0)} </span>`\n + \" | \" +\n `<span style=\"color:rgb(203,37,48)\">${(data[0].inactive_connectors ? JSON.parse(data[0].inactive_connectors).length : 0)} </span>`\n , \"Connectors <span class='tb-hint' style='padding-left: 0'>(Enabled | Disabled)</span>\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `<div class=\"flex flex-row flex-wrap gap-2 cards-container\">${blockData}</div>`;", "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Logs\");\nfunction generateMatHeader(index) {\n if (index !== undefined && index > -1) {\n return `<mat-card-header class='tb-home-widget-link' (click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[${index}], ctx.datasources[0].entity.id)\">`\n } else {\n return \"<mat-card-header>\"\n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n <mat-card style=\"flex-grow: 1; width: ${mobile ? '100%' : 'auto'}; min-height: ${mobile ? 'auto' : '57px'}\" class=\"${dividerStyle}\">\n <div class=\"divider\"></div>\n <mat-divider vertical style=\"height:100%\"></mat-divider>\n ${generateMatHeader(index)}\n <mat-card-subtitle>${label}</mat-card-subtitle>\n </mat-card-header>\n <mat-card-content> ${ctx.sanitizer.sanitize(1, value)}</mat-card-content>\n </mat-card>`;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\" ? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\nif (data[0].Version) {\n createDataBlock(data[0].Version, \"Gateway Version\", '');\n}\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `<span style=\"color:rgb(25,128,56)\">${(data[1] ? data[1].count : 0)} </span>`\n + \" | \" +\n `<span style=\"color:rgb(203,37,48)\">${(data[2] ? data[2][\"count 2\"] : 0)} </span>`\n , \"Devices <span class='tb-hint' style='padding-left: 0'>(Active | Inactive)</span>\", '');\ncreateDataBlock(\n `<span style=\"color:rgb(25,128,56)\">${(data[0].active_connectors ? JSON.parse(data[0].active_connectors).length : 0)} </span>`\n + \" | \" +\n `<span style=\"color:rgb(203,37,48)\">${(data[0].inactive_connectors ? JSON.parse(data[0].inactive_connectors).length : 0)} </span>`\n , \"Connectors <span class='tb-hint' style='padding-left: 0'>(Enabled | Disabled)</span>\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `<div class=\"flex flex-row flex-wrap gap-2 cards-container\">${blockData}</div>`;",
"applyDefaultMarkdownStyle": false, "applyDefaultMarkdownStyle": false,
"markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}" "markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}"
}, },

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

@ -360,21 +360,26 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArguments(argNames, data); return mapToArguments(argNames, data);
} }
private Map<String, ArgumentEntry> mapToArguments(Map<ReferencedEntityKey, String> argNames, List<TsKvProto> data) { private Map<String, ArgumentEntry> mapToArguments(Map<ReferencedEntityKey, Set<String>> args, List<TsKvProto> data) {
if (argNames.isEmpty()) { if (args.isEmpty()) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
Map<String, ArgumentEntry> arguments = new HashMap<>(); Map<String, ArgumentEntry> arguments = new HashMap<>();
for (TsKvProto item : data) { for (TsKvProto item : data) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null);
String argName = argNames.get(key); Set<String> argNames = args.get(key);
if (argName != null) { if (argNames != null) {
arguments.put(argName, new SingleValueArgumentEntry(item)); argNames.forEach(argName -> {
arguments.put(argName, new SingleValueArgumentEntry(item));
});
} }
key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null);
argName = argNames.get(key); argNames = args.get(key);
if (argName != null) { if (argNames != null) {
arguments.put(argName, new SingleValueArgumentEntry(item)); argNames.forEach(argName -> {
arguments.put(argName, new SingleValueArgumentEntry(item));
});
} }
} }
return arguments; return arguments;
@ -393,19 +398,21 @@ 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> geofencingArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) { private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, Set<String>> args, 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()));
String argName = argNames.get(key); Set<String> argNames = args.get(key);
if (argName == null) { if (argNames == null) {
continue;
}
if (geofencingArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry(entityId, item));
continue; continue;
} }
arguments.put(argName, new SingleValueArgumentEntry(item)); argNames.forEach(argName -> {
if (geofencingArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry(entityId, item));
} else {
arguments.put(argName, new SingleValueArgumentEntry(item));
}
});
} }
return arguments; return arguments;
} }
@ -423,24 +430,25 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), 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, List<String> geofencingArgNames, AttributeScopeProto scope, List<String> removedAttrKeys) { private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, Set<String>> args, 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); Set<String> argNames = args.get(key);
if (argName == null) { if (argNames == null) {
continue; continue;
} }
if (geofencingArgNames.contains(argName)) { argNames.forEach(argName -> {
arguments.put(argName, new GeofencingArgumentEntry()); if (geofencingArgNames.contains(argName)) {
continue; arguments.put(argName, new GeofencingArgumentEntry());
} } else {
Argument argument = configArguments.get(argName); Argument argument = configArguments.get(argName);
String defaultValue = (argument != null) ? argument.getDefaultValue() : null; String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
arguments.put(argName, StringUtils.isNotEmpty(defaultValue) arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
: new SingleValueArgumentEntry()); : new SingleValueArgumentEntry());
}
});
} }
return arguments; return arguments;
} }

11
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -115,14 +115,9 @@ 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;
/**
* @author Andrew Shvayka
*/
@Slf4j @Slf4j
public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
static final String SESSION_TIMEOUT_MESSAGE = "session timeout!";
final TenantId tenantId; final TenantId tenantId;
final DeviceId deviceId; final DeviceId deviceId;
final LinkedHashMapRemoveEldest<UUID, SessionInfoMetaData> sessions; final LinkedHashMapRemoveEldest<UUID, SessionInfoMetaData> sessions;
@ -178,7 +173,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private EdgeId findRelatedEdgeId() { private EdgeId findRelatedEdgeId() {
List<EntityRelation> result = List<EntityRelation> result =
systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE);
if (result != null && result.size() > 0) { if (result != null && !result.isEmpty()) {
EntityRelation relationToEdge = result.get(0); EntityRelation relationToEdge = result.get(0);
if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) {
log.trace("[{}][{}] found edge [{}] for device", tenantId, deviceId, relationToEdge.getFrom().getId()); log.trace("[{}][{}] found edge [{}] for device", tenantId, deviceId, relationToEdge.getFrom().getId());
@ -501,7 +496,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
UUID sessionId = getSessionId(sessionInfo); UUID sessionId = getSessionId(sessionInfo);
DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB()));
ListenableFuture<Void> registrationFuture = systemContext.getClaimDevicesService() ListenableFuture<Void> registrationFuture = systemContext.getClaimDevicesService()
.registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); .registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs());
Futures.addCallback(registrationFuture, new FutureCallback<>() { Futures.addCallback(registrationFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(Void result) { public void onSuccess(Void result) {
@ -723,7 +718,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
toDeviceRpcPendingMap.remove(requestId); toDeviceRpcPendingMap.remove(requestId);
status = RpcStatus.FAILED; status = RpcStatus.FAILED;
response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry " + response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry " +
"attempts have been exhausted. Retry attempts set: " + maxRpcRetries); "attempts have been exhausted. Retry attempts set: " + maxRpcRetries);
} }
} else { } else {
md.setRetries(md.getRetries() + 1); md.setRetries(md.getRetries() + 1);

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

@ -63,7 +63,7 @@ public class ApiKeyController extends BaseController {
private final ApiKeyService apiKeyService; private final ApiKeyService apiKeyService;
@ApiOperation(value = "Save API key for user (saveApiKey)", @ApiOperation(value = "Save API key for user (saveApiKey)",
notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey <hash>'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey <value>'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping(value = "/apiKey") @PostMapping(value = "/apiKey")
public ApiKey saveApiKey( public ApiKey saveApiKey(

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

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

41
application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java

@ -15,7 +15,13 @@
*/ */
package org.thingsboard.server.service.ai; package org.thingsboard.server.service.ai;
import com.fasterxml.jackson.core.io.JsonStringEncoder;
import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FluentFuture;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.Content;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.ModelProvider;
import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.chat.response.ChatResponse;
@ -24,6 +30,9 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
import java.util.List;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
class AiChatModelServiceImpl implements AiChatModelService { class AiChatModelServiceImpl implements AiChatModelService {
@ -34,7 +43,39 @@ class AiChatModelServiceImpl implements AiChatModelService {
@Override @Override
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) { public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer); ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
if (langChainChatModel.provider() == ModelProvider.GITHUB_MODELS) {
chatRequest = prepareGithubChatRequest(chatRequest);
}
return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest); return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest);
} }
private ChatRequest prepareGithubChatRequest(ChatRequest chatRequest) {
List<ChatMessage> messages = chatRequest.messages().stream()
.map(this::prepareUserMessage)
.collect(Collectors.toList());
return ChatRequest.builder()
.messages(messages)
.responseFormat(chatRequest.responseFormat())
.build();
}
private ChatMessage prepareUserMessage(ChatMessage message) {
if (message instanceof UserMessage userMessage) {
List<Content> newContents = userMessage.contents().stream()
.map(this::prepareContent)
.collect(Collectors.toList());
return UserMessage.from(newContents);
}
return message;
}
private Content prepareContent(Content content) {
if (content instanceof TextContent txt) {
return new TextContent(new String(JsonStringEncoder.getInstance().quoteAsString(txt.text())));
}
return content;
}
} }

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

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
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;
@ -49,6 +50,7 @@ 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.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
@ -63,8 +65,8 @@ public class CalculatedFieldCtx {
private EntityId entityId; private EntityId entityId;
private CalculatedFieldType cfType; private CalculatedFieldType cfType;
private final Map<String, Argument> arguments; private final Map<String, Argument> arguments;
private final Map<ReferencedEntityKey, String> mainEntityArguments; private final Map<ReferencedEntityKey, Set<String>> mainEntityArguments;
private final Map<EntityId, Map<ReferencedEntityKey, String>> linkedEntityArguments; private final Map<EntityId, Map<ReferencedEntityKey, Set<String>>> linkedEntityArguments;
private final List<String> argNames; private final List<String> argNames;
private Output output; private Output output;
private String expression; private String expression;
@ -110,9 +112,10 @@ public class CalculatedFieldCtx {
continue; continue;
} }
if (refId == null || refId.equals(calculatedField.getEntityId())) { if (refId == null || refId.equals(calculatedField.getEntityId())) {
mainEntityArguments.put(refKey, entry.getKey()); mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey()));
} else { } else {
linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>())
.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey()));
} }
} }
this.argNames.addAll(arguments.keySet()); this.argNames.addAll(arguments.keySet());
@ -225,7 +228,7 @@ public class CalculatedFieldCtx {
return map != null && matchesTimeSeries(map, values); return map != null && matchesTimeSeries(map, values);
} }
private boolean matchesAttributes(Map<ReferencedEntityKey, String> argMap, List<AttributeKvEntry> values, AttributeScope scope) { private boolean matchesAttributes(Map<ReferencedEntityKey, Set<String>> argMap, List<AttributeKvEntry> values, AttributeScope scope) {
if (argMap.isEmpty() || values.isEmpty()) { if (argMap.isEmpty() || values.isEmpty()) {
return false; return false;
} }
@ -239,7 +242,7 @@ public class CalculatedFieldCtx {
return false; return false;
} }
private boolean matchesTimeSeries(Map<ReferencedEntityKey, String> argMap, List<TsKvEntry> values) { private boolean matchesTimeSeries(Map<ReferencedEntityKey, Set<String>> argMap, List<TsKvEntry> values) {
if (argMap.isEmpty() || values.isEmpty()) { if (argMap.isEmpty() || values.isEmpty()) {
return false; return false;
} }
@ -268,7 +271,7 @@ public class CalculatedFieldCtx {
return matchesTimeSeriesKeys(mainEntityArguments, keys); return matchesTimeSeriesKeys(mainEntityArguments, keys);
} }
private boolean matchesAttributesKeys(Map<ReferencedEntityKey, String> argMap, List<String> keys, AttributeScope scope) { private boolean matchesAttributesKeys(Map<ReferencedEntityKey, Set<String>> argMap, List<String> keys, AttributeScope scope) {
if (argMap.isEmpty() || keys.isEmpty()) { if (argMap.isEmpty() || keys.isEmpty()) {
return false; return false;
} }
@ -283,7 +286,7 @@ public class CalculatedFieldCtx {
return false; return false;
} }
private boolean matchesTimeSeriesKeys(Map<ReferencedEntityKey, String> argMap, List<String> keys) { private boolean matchesTimeSeriesKeys(Map<ReferencedEntityKey, Set<String>> argMap, List<String> keys) {
if (argMap.isEmpty() || keys.isEmpty()) { if (argMap.isEmpty() || keys.isEmpty()) {
return false; return false;
} }

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

11
application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java

@ -186,9 +186,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
try { try {
Optional<AttributeKvEntry> provisionState = attributesService.find(device.getTenantId(), device.getId(), Optional<AttributeKvEntry> provisionState = attributesService.find(device.getTenantId(), device.getId(),
AttributeScope.SERVER_SCOPE, DEVICE_PROVISION_STATE).get(); AttributeScope.SERVER_SCOPE, DEVICE_PROVISION_STATE).get();
if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) { if (provisionState != null && provisionState.isPresent()) {
notify(device, provisionRequest, TbMsgType.PROVISION_FAILURE, false); if (provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) {
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); notify(device, provisionRequest, TbMsgType.PROVISION_FAILURE, false);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
} else {
log.error("[{}][{}] Unknown provision state: {}!", device.getName(), DEVICE_PROVISION_STATE, provisionState.get().getValueAsString());
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
} else { } else {
saveProvisionStateAttribute(device).get(); saveProvisionStateAttribute(device).get();
notify(device, provisionRequest, TbMsgType.PROVISION_SUCCESS, true); notify(device, provisionRequest, TbMsgType.PROVISION_SUCCESS, true);

2
application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java

@ -328,7 +328,7 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService {
attributes.add(new BaseAttributeKvEntry(ts, new LongDataEntry(getAttributeKey(otaPackageType, SIZE), otaPackage.getDataSize()))); attributes.add(new BaseAttributeKvEntry(ts, new LongDataEntry(getAttributeKey(otaPackageType, SIZE), otaPackage.getDataSize())));
} }
if (otaPackage.getChecksumAlgorithm() != null) { if (otaPackage.getChecksumAlgorithm() == null) {
attrToRemove.add(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM)); attrToRemove.add(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM));
} else { } else {
attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM), otaPackage.getChecksumAlgorithm().name()))); attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM), otaPackage.getChecksumAlgorithm().name())));

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;

5
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;
@ -102,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).token());
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).token());
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() {

10
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;
@ -37,13 +36,14 @@ public class DefaultAccessControlService implements AccessControlService {
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

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

@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.pat.ApiKeyInfo;
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() {
@ -77,7 +77,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")

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

@ -193,7 +193,7 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
}); });
return Map.entry(kvType, kvs); return Map.entry(kvType, kvs);
}) })
.filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0) .filter(kvsEntry -> !kvsEntry.getValue().entrySet().isEmpty())
.forEach(kvsEntry -> { .forEach(kvsEntry -> {
BulkImportColumnType kvType = kvsEntry.getKey(); BulkImportColumnType kvType = kvsEntry.getKey();
if (kvType == BulkImportColumnType.SHARED_ATTRIBUTE || kvType == BulkImportColumnType.SERVER_ATTRIBUTE) { if (kvType == BulkImportColumnType.SHARED_ATTRIBUTE || kvType == BulkImportColumnType.SERVER_ATTRIBUTE) {

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

@ -1214,7 +1214,7 @@ transport:
# Enable/disable Bootstrap Server # Enable/disable Bootstrap Server
enabled: "${LWM2M_ENABLED_BS:true}" enabled: "${LWM2M_ENABLED_BS:true}"
# Default value in LwM2M client after start in mode Bootstrap for the object : name "LWM2M Security" field: "Short Server ID" (deviceProfile: Bootstrap.BOOTSTRAP SERVER.Short ID) # Default value in LwM2M client after start in mode Bootstrap for the object : name "LWM2M Security" field: "Short Server ID" (deviceProfile: Bootstrap.BOOTSTRAP SERVER.Short ID)
id: "${LWM2M_SERVER_ID_BS:111}" id: "${LWM2M_SERVER_ID_BS:0}"
# LwM2M bootstrap server bind address. Bind to all interfaces by default # LwM2M bootstrap server bind address. Bind to all interfaces by default
bind_address: "${LWM2M_BS_BIND_ADDRESS:0.0.0.0}" bind_address: "${LWM2M_BS_BIND_ADDRESS:0.0.0.0}"
# LwM2M bootstrap server bind port # LwM2M bootstrap server bind port

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

@ -1002,6 +1002,56 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
}); });
} }
@Test
public void testCalculatedFieldWhenTheSameTelemetryKeysUsed() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":5}"));
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("a + b");
calculatedField.setDebugSettings(DebugSettings.all());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null);
Argument argumentA = new Argument();
argumentA.setRefEntityKey(refEntityKey);
Argument argumentB = new Argument();
argumentB.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("a", argumentA, "b", argumentB));
config.setExpression("a + b");
Output output = new Output();
output.setName("c");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0);
config.setOutput(output);
calculatedField.setConfiguration(config);
doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode c = getLatestTelemetry(testDevice.getId(), "c");
assertThat(c).isNotNull();
assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("10");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}"));
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode c = getLatestTelemetry(testDevice.getId(), "c");
assertThat(c).isNotNull();
assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("20");
});
}
private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception {
return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class);
} }

12
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;
@ -211,7 +212,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";
@ -606,8 +607,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;

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

@ -394,6 +394,48 @@ public class DeviceControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString("Device can`t be referencing to device profile from different tenant!"))); .andExpect(statusReason(containsString("Device can`t be referencing to device profile from different tenant!")));
} }
@Test
public void testSaveDeviceWithFirmware() throws Exception {
loginTenantAdmin();
DeviceProfile profile = createDeviceProfile("Profile to test ota updates");
profile = doPost("/api/deviceProfile", profile, DeviceProfile.class);
SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest();
firmwareInfo.setDeviceProfileId(profile.getId());
firmwareInfo.setType(FIRMWARE);
String title = "title";
firmwareInfo.setTitle(title);
String fwVersion = "1.0";
firmwareInfo.setVersion(fwVersion);
String url = "test.url";
firmwareInfo.setUrl(url);
firmwareInfo.setUsesUrl(true);
OtaPackageInfo savedFw = doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class);
Device device = new Device();
device.setName("My ota device");
device.setDeviceProfileId(profile.getId());
device.setFirmwareId(savedFw.getId());
device = doPost("/api/device", device, Device.class);
//check shared attributes
Device finalDevice = device;
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
List<Map<String, Object>> attributes = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + finalDevice.getId() +
"/values/attributes/SHARED_SCOPE", new TypeReference<List<Map<String, Object>>>() {
});
return findAttrValue("fw_version", attributes).equals(fwVersion) &&
findAttrValue("fw_title", attributes).equals(title) &&
findAttrValue("fw_url", attributes).equals(url);
});
}
private static Object findAttrValue(String key, List<Map<String, Object>> attributes) {
Optional<Map<String, Object>> attr = attributes.stream()
.filter(att -> att.get("key").equals(key)).findFirst();
return attr.isPresent() ? attr.get().get("value") : "";
}
@Test @Test
public void testSaveDeviceWithFirmwareFromDifferentTenant() throws Exception { public void testSaveDeviceWithFirmwareFromDifferentTenant() throws Exception {
loginDifferentTenant(); loginDifferentTenant();
@ -1638,7 +1680,8 @@ public class DeviceControllerTest extends AbstractControllerTest {
Assert.assertEquals(deviceName, savedDevice.getName()); Assert.assertEquals(deviceName, savedDevice.getName());
Assert.assertEquals(deviceType, savedDevice.getType()); Assert.assertEquals(deviceType, savedDevice.getType());
Optional<AttributeKvEntry> retrieved = attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get(); Optional<AttributeKvEntry> retrieved = await().atMost(5, TimeUnit.SECONDS)
.until(() -> attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get(), Optional::isPresent);
assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr); assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr);
assertThat(retrieved.get().getStrValue()).isNotPresent(); assertThat(retrieved.get().getStrValue()).isNotPresent();
} }

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

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.token()); SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.token());

1
application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java

@ -64,6 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
"coap.server.enabled=true", "coap.server.enabled=true",
"coap.dtls.enabled=true", "coap.dtls.enabled=true",
"coap.dtls.credentials.pem.cert_file=coap/credentials/server/cert.pem", "coap.dtls.credentials.pem.cert_file=coap/credentials/server/cert.pem",
"coap.dtls.x509.skip_validity_check_for_client_cert=true",
"device.connectivity.coaps.enabled=true", "device.connectivity.coaps.enabled=true",
"service.integrations.supported=ALL", "service.integrations.supported=ALL",
"transport.coap.enabled=true", "transport.coap.enabled=true",

43
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -84,6 +84,7 @@ import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -120,11 +121,11 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
import static org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS_19; import static org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS_19;
@TestPropertySource(properties = {
"transport.lwm2m.enabled=true",
})
@Slf4j @Slf4j
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = {
"transport.lwm2m.enabled=true"
})
public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest { public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest {
@SpyBean @SpyBean
@ -145,9 +146,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public static final String host = "localhost"; public static final String host = "localhost";
public static final String hostBs = "localhost"; public static final String hostBs = "localhost";
public static final Integer shortServerId = 123; public static final Integer shortServerId = 123;
public static final Integer shortServerIdBs0 = 0;
public static final int serverId = 1;
public static final int serverIdBs = 0;
public static final String COAP = "coap://"; public static final String COAP = "coap://";
public static final String COAPS = "coaps://"; public static final String COAPS = "coaps://";
@ -317,7 +315,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
@After @After
public void after() throws Exception { public void after() throws Exception {
this.clientDestroy(true); this.clientDestroy();
if (executor != null && !executor.isShutdown()) { if (executor != null && !executor.isShutdown()) {
executor.shutdownNow(); executor.shutdownNow();
} }
@ -564,7 +562,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public void createNewClient(Security security, Security securityBs, boolean isRpc, public void createNewClient(Security security, Security securityBs, boolean isRpc,
String endpoint, Integer clientDtlsCidLength, boolean queueMode, String endpoint, Integer clientDtlsCidLength, boolean queueMode,
String deviceIdStr, Integer value3_0_9) throws Exception { String deviceIdStr, Integer value3_0_9) throws Exception {
this.clientDestroy(false); this.clientDestroy();
lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources); lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources);
try (ServerSocket socket = new ServerSocket(0)) { try (ServerSocket socket = new ServerSocket(0)) {
@ -651,13 +649,30 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
} }
private void clientDestroy(boolean isAfter) { private void clientDestroy() {
try { try {
if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) { if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) {
if (isAfter) { boolean serverAlive = false;
sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); for (int port = AbstractLwM2MIntegrationTest.port; port <= securityPortBs; port++) {
awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr()); try (ServerSocket socket = new ServerSocket(port)) {
log.info("Port {} is free.", port);
} catch (IOException e) {
log.debug("Port {} is busy — CoAP server still active.", port);
serverAlive = true;
break;
}
}
if (serverAlive) {
try {
sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr());
} catch (Exception e) {
log.warn("Failed to cleanup LwM2M observations before destroy: {}", e.getMessage());
}
} else {
log.info("No active CoAP server found on ports 5685–5688. Skipping observe cleanup.");
} }
lwM2MTestClient.destroy(); lwM2MTestClient.destroy();
} }
} catch (Exception e) { } catch (Exception e) {
@ -705,10 +720,10 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return bootstrap; return bootstrap;
} }
private AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) { protected AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) {
AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential(); AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential();
bootstrapServerCredential.setServerPublicKey(""); bootstrapServerCredential.setServerPublicKey("");
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId); bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap); bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host); bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? portBs : port); bootstrapServerCredential.setPort(isBootstrap ? portBs : port);

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java

@ -24,8 +24,6 @@ public class Lwm2mTestHelper {
public static final int TEMPERATURE_SENSOR = 3303; public static final int TEMPERATURE_SENSOR = 3303;
// Ids in Client // Ids in Client
public static final int OBJECT_ID_0 = 0;
public static final int OBJECT_ID_1 = 1;
public static final int OBJECT_INSTANCE_ID_0 = 0; public static final int OBJECT_INSTANCE_ID_0 = 0;
public static final int OBJECT_INSTANCE_ID_1 = 1; public static final int OBJECT_INSTANCE_ID_1 = 1;
public static final int OBJECT_INSTANCE_ID_2 = 2; public static final int OBJECT_INSTANCE_ID_2 = 2;

174
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java

@ -68,6 +68,9 @@ import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandle
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -77,6 +80,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
@ -88,10 +92,7 @@ import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT;
import static org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder.getDefaultPathEncoder; import static org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder.getDefaultPathEncoder;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerIdBs0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE;
@ -141,7 +142,7 @@ public class LwM2MTestClient {
private LwM2mTemperatureSensor lwM2mTemperatureSensor12; private LwM2mTemperatureSensor lwM2mTemperatureSensor12;
private String deviceIdStr; private String deviceIdStr;
public void init(Security security, Security securityBs, int port, boolean isRpc, public void init(Security securityLwm2m, Security securityBs, int port, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode, LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode,
boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException { boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException {
@ -149,55 +150,33 @@ public class LwM2MTestClient {
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
this.clientContext = clientContext; this.clientContext = clientContext;
List<ObjectModel> models = ObjectLoader.loadAllDefault(); ObjectsInitializer initializer = createFreshInitializer();
for (String resourceName : lwm2mClientResources) {
models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); // SECURITY
if (securityLwm2m != null && securityLwm2m.getId() != null) {
forceNullSecurityId(securityLwm2m);
} }
if (this.modelResources != null) { if (securityBs!= null && securityBs.getId() != null) {
List<ObjectModel> modelsRes = new ArrayList<>(); forceNullSecurityId(securityBs);
for (String resourceName : this.modelResources) {
modelsRes.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName));
}
Set<Integer> idsToRemove = new HashSet<>();
for (ObjectModel model : modelsRes) {
idsToRemove.add(model.id);
}
models.removeIf(model -> idsToRemove.contains(model.id));
models.addAll(modelsRes);
} }
if (securityBs != null && securityLwm2m != null) {
LwM2mModel model = new StaticModel(models); log.warn("Security Both: securityBs: [{}] and security Lwm2m [{}]", securityBs.getId(), securityLwm2m.getId());
ObjectsInitializer initializer = new ObjectsInitializer(model); initializer.setInstancesForObject(SECURITY, securityBs, securityLwm2m);
if (securityBs != null && security != null) {
// SECURITY
security.setId(serverId);
securityBs.setId(serverIdBs);
LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{securityBs, security};
initializer.setClassForObject(SECURITY, Security.class);
initializer.setInstancesForObject(SECURITY, instances);
// SERVER
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60));
serverBs.setId(serverIdBs);
instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer};
initializer.setClassForObject(SERVER, Server.class);
initializer.setInstancesForObject(SERVER, instances);
} else if (securityBs != null) { } else if (securityBs != null) {
// SECURITY log.warn("Security BS only: securityBs: [{}] ", securityBs.getId());
initializer.setInstancesForObject(SECURITY, securityBs); initializer.setInstancesForObject(SECURITY, securityBs);
// SERVER } else if (securityLwm2m != null){
initializer.setClassForObject(SERVER, Server.class);
} else {
// SECURITY // SECURITY
initializer.setInstancesForObject(SECURITY, security); log.warn("Security Lwm2m only: security Lwm2m [{}]", securityLwm2m.getId());
// SERVER initializer.setInstancesForObject(SECURITY, securityLwm2m);
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
initializer.setInstancesForObject(SERVER, lwm2mServer);
} }
// SERVER
Server serverLwm2m = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
initializer.setInstancesForObject(SERVER, serverLwm2m);
// DEVICE
initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9)); initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9));
// OTHER t
initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice());
initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice());
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
@ -444,25 +423,43 @@ public class LwM2MTestClient {
public void destroy() { public void destroy() {
if (leshanClient != null) { if (leshanClient != null) {
leshanClient.destroy(true); try {
} leshanClient.destroy(true);
if (lwM2MDevice != null) { } catch (Exception e) {
lwM2MDevice.destroy(); log.warn("Failed to destroy Leshan client", e);
} } finally {
if (fwLwM2MDevice != null) { leshanClient = null;
fwLwM2MDevice.destroy(); }
}
if (swLwM2MDevice != null) {
swLwM2MDevice.destroy();
}
if (lwM2MBinaryAppDataContainer != null) {
lwM2MBinaryAppDataContainer.destroy();
} }
if (lwM2MTemperatureSensor != null) {
lwM2MTemperatureSensor.destroy(); // ThingsBoard custom LwM2M objects
destroySafe(lwM2MDevice);
destroySafe(fwLwM2MDevice);
destroySafe(swLwM2MDevice);
destroySafe(lwM2MBinaryAppDataContainer);
destroySafe(lwM2MTemperatureSensor);
lwM2MDevice = null;
fwLwM2MDevice = null;
swLwM2MDevice = null;
lwM2MBinaryAppDataContainer = null;
lwM2MTemperatureSensor = null;
}
private void destroySafe(Object obj) {
if (obj == null) return;
try {
Method destroy = obj.getClass().getMethod("destroy");
destroy.invoke(obj);
} catch (NoSuchMethodException e) {
// не має destroy() — ігноруємо
} catch (Exception e) {
log.warn("Failed to destroy {}", obj.getClass().getSimpleName(), e);
} }
} }
public void start(boolean isStartLw) { public void start(boolean isStartLw) {
if (leshanClient != null) { if (leshanClient != null) {
leshanClient.start(); leshanClient.start();
@ -483,4 +480,59 @@ public class LwM2MTestClient {
LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(endpoint); LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(endpoint);
Mockito.doAnswer(invocationOnMock -> null).when(defaultLwM2mUplinkMsgHandlerTest).initAttributes(lwM2MClient, true); Mockito.doAnswer(invocationOnMock -> null).when(defaultLwM2mUplinkMsgHandlerTest).initAttributes(lwM2MClient, true);
} }
private ObjectsInitializer createFreshInitializer() {
List<ObjectModel> models = new ArrayList<>(ObjectLoader.loadAllDefault());
for (String resourceName : lwm2mClientResources) {
try (InputStream in = LwM2MTestClient.class.getClassLoader()
.getResourceAsStream("lwm2m/" + resourceName)) {
models.addAll(ObjectLoader.loadDdfFile(in, resourceName));
} catch (IOException | InvalidDDFFileException e) {
log.warn("Failed to load resource {}", resourceName, e);
}
}
if (this.modelResources != null) {
List<ObjectModel> modelsRes = new ArrayList<>();
for (String resourceName : this.modelResources) {
try (InputStream in = LwM2MTestClient.class.getClassLoader()
.getResourceAsStream("lwm2m/" + resourceName)) {
modelsRes.addAll(ObjectLoader.loadDdfFile(in, resourceName));
} catch (IOException | InvalidDDFFileException e) {
log.warn("Failed to load resource {}", resourceName, e);
}
}
Set<Integer> idsToRemove = modelsRes.stream()
.map(m -> m.id)
.collect(Collectors.toSet());
models.removeIf(m -> idsToRemove.contains(m.id));
models.addAll(modelsRes);
}
LwM2mModel model = new StaticModel(models);
return new ObjectsInitializer(model);
}
private void forceNullSecurityId(Security security) {
if (security == null) {
return;
}
try {
Field field = security.getClass().getDeclaredField("id");
field.setAccessible(true);
field.set(security, null);
log.info("[forceNullSecurityId] Set id=null for {}", security);
} catch (NoSuchFieldException e) {
try {
//(SecurityObjectInstance)
Field field = security.getClass().getSuperclass().getDeclaredField("id");
field.setAccessible(true);
field.set(security, null);
log.info("[forceNullSecurityId] Set id=null for {} (via superclass)", security);
} catch (Exception ex) {
log.error("[forceNullSecurityId] Field 'id' not found for {}", security.getClass(), ex);
}
} catch (Exception e) {
log.error("[forceNullSecurityId] Failed to set id=null for {}", security.getClass(), e);
}
}
} }

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

@ -43,12 +43,11 @@ import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.eclipse.leshan.core.LwM2mId.DEVICE; import static org.eclipse.leshan.core.LwM2mId.DEVICE;
import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; import static org.eclipse.leshan.core.LwM2mId.FIRMWARE;
import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12;
@ -115,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);
} }
@ -140,10 +143,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
}); });
} }
}); });
String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_0).version; String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SECURITY).version;
String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version; String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SERVER).version;
objectIdVer_0 = "/" + OBJECT_ID_0 + "_" + ver_Id_0; objectIdVer_0 = "/" + SECURITY + "_" + ver_Id_0;
objectIdVer_1 = "/" + OBJECT_ID_1 + "_" + ver_Id_1; objectIdVer_1 = "/" + SERVER + "_" + ver_Id_1;
objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + ACCESS_CONTROL)).findFirst().get(); objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + ACCESS_CONTROL)).findFirst().get();
objectIdVer_3 = (String) expectedObjectIdVers.stream().filter(PREDICATE_3).findFirst().get(); objectIdVer_3 = (String) expectedObjectIdVers.stream().filter(PREDICATE_3).findFirst().get();
objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get();
@ -221,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);
});
}
}

32
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java

@ -21,8 +21,10 @@ import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -53,18 +55,18 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
try { try {
expectedObjectIdVers.forEach(expected -> { expectedObjectIdVers.forEach(expected -> {
try { try {
String actualResult = sendRPCById((String) expected);
String expectedObjectId = pathIdVerToObjectId((String) expected); String expectedObjectId = pathIdVerToObjectId((String) expected);
LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); LwM2mPath expectedPath = new LwM2mPath(expectedObjectId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); if (expectedPath.getObjectId() > ACCESS_CONTROL) {
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String actualResult = sendRPCByIdSync((String) expected);
String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources="; if (StringUtils.isNoneBlank(actualResult)) {
if (expectedPath.getObjectId() == 1) { log.warn(" expectedPath: [{}]", expectedPath);
expectedObjectInstances = "LwM2mObject [id=1, instances={1="; ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
} else if (expectedPath.getObjectId() == 2) { assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expectedObjectInstances = "LwM2mObject [id=2, instances={}]"; String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources=";
assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances));
}
} }
assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances));
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -83,7 +85,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception { public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception {
expectedObjectIdVerInstances.forEach(expected -> { expectedObjectIdVerInstances.forEach(expected -> {
try { try {
String actualResult = sendRPCById((String) expected); String actualResult = sendRPCByIdAsync((String) expected);
String expectedObjectId = pathIdVerToObjectId((String) expected); String expectedObjectId = pathIdVerToObjectId((String) expected);
LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); LwM2mPath expectedPath = new LwM2mPath(expectedObjectId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@ -104,7 +106,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
@Test @Test
public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception { public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception {
String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11; String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11;
String actualResult = sendRPCById(expectedIdVer); String actualResult = sendRPCByIdAsync(expectedIdVer);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={"; String expected = "LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={";
@ -117,7 +119,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
@Test @Test
public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception { public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception {
String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String actualResult = sendRPCById(expectedIdVer); String actualResult = sendRPCByIdAsync(expectedIdVer);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=";
@ -228,10 +230,14 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
assertEquals(actualValue, expectedValue); assertEquals(actualValue, expectedValue);
} }
private String sendRPCById(String path) throws Exception { private String sendRPCByIdAsync(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
private String sendRPCByIdSync(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPost("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
}
private String sendRPCByKey(String key) throws Exception { private String sendRPCByKey(String key) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"key\": \"" + key + "\"}}"; String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"key\": \"" + key + "\"}}";

37
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java

@ -69,6 +69,7 @@ import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.client.object.Security.noSecBootstrap; import static org.eclipse.leshan.client.object.Security.noSecBootstrap;
import static org.eclipse.leshan.client.object.Security.psk; import static org.eclipse.leshan.client.object.Security.psk;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
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.device.credentials.lwm2m.LwM2MSecurityMode.PSK; import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK;
@ -77,7 +78,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClient
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
@DaoSqlTest @DaoSqlTest
@ -95,7 +96,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
protected static final String SERVER_STORE_PWD = "server_ks_password"; protected static final String SERVER_STORE_PWD = "server_ks_password";
protected static final String SERVER_CERT_ALIAS = "server"; protected static final String SERVER_CERT_ALIAS = "server";
protected static final String SERVER_CERT_ALIAS_BS = "bootstrap"; protected static final String SERVER_CERT_ALIAS_BS = "bootstrap";
protected static final Security SECURITY_NO_SEC_BS = noSecBootstrap(URI_BS);;
protected final X509Certificate serverX509Cert; // server certificate signed by rootCA protected final X509Certificate serverX509Cert; // server certificate signed by rootCA
protected final X509Certificate serverX509CertBs; // serverBs certificate signed by rootCA protected final X509Certificate serverX509CertBs; // serverBs certificate signed by rootCA
protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK
@ -178,20 +178,20 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
defaultBootstrapCredentials.setLwm2mServer(serverCredentials); defaultBootstrapCredentials.setLwm2mServer(serverCredentials);
} }
public void basicTestConnectionBefore(String clientEndpoint, public void basicTestConnectionStartBS(String clientEndpoint,
String awaitAlias, String awaitAlias,
LwM2MProfileBootstrapConfigType type, LwM2MProfileBootstrapConfigType type,
Set<LwM2MClientState> expectedStatuses, Set<LwM2MClientState> expectedStatuses,
LwM2MClientState finishState) throws Exception { LwM2MClientState finishState) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
this.basicTestConnection(null , SECURITY_NO_SEC_BS, this.basicTestConnection(null , noSecBootstrap(URI_BS),
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,
awaitAlias, awaitAlias,
expectedStatuses, expectedStatuses,
true, false,
finishState, finishState,
false); false);
} }
@ -231,12 +231,19 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
} }
public void basicTestConnectionBootstrapRequestTriggerBefore(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type) throws Exception { public void basicTestConnectionBootstrapRequestTriggerBefore(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type, int cnt) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type)); List<LwM2MBootstrapServerCredential> bootstrapServerCredentialsNoSec = getBootstrapServerCredentialsNoSec(type);
for (int i = 2; i <= cnt; i++) {
AbstractLwM2MBootstrapServerCredential bsCredential = getBootstrapServerCredentialNoSec(false);
bsCredential.setHost("0.0.0." + i);
bsCredential.setShortServerId(bsCredential.getShortServerId() + i);
bootstrapServerCredentialsNoSec.add(bsCredential);
}
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, bootstrapServerCredentialsNoSec);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
this.basicTestConnectionBootstrapRequestTrigger( this.basicTestConnectionBootstrapRequestTrigger(
SECURITY_NO_SEC, SECURITY_NO_SEC,
SECURITY_NO_SEC_BS, noSecBootstrap(URI_BS),
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,
@ -275,8 +282,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
}); });
Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesLwm2m)); Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesLwm2m));
String executedPath = "/" + OBJECT_ID_1 + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version String executedPath = "/" + SERVER + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SERVER).version
+ "/" +serverId + "/" + RESOURCE_ID_9; + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9;
lwM2MTestClient.setClientStates(new HashSet<>()); lwM2MTestClient.setClientStates(new HashSet<>());
String actualResult = sendRPCSecurityExecuteById(executedPath, deviceIdStr, endpoint); String actualResult = sendRPCSecurityExecuteById(executedPath, deviceIdStr, endpoint);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@ -352,7 +359,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
default: default:
throw new IllegalStateException("Unexpected value: " + mode); throw new IllegalStateException("Unexpected value: " + mode);
} }
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId); bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap); bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host); bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? securityPortBs : securityPort); bootstrapServerCredential.setPort(isBootstrap ? securityPortBs : securityPort);

29
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java

@ -0,0 +1,29 @@
/**
* 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.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY;
public class NoSecLwM2MIntegrationBS3SectionTriggerTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTrigger_3_ConnectBsSuccess_UpdateLwm2mSection_3_AndLm2m_1_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger_3" + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 3);
}
}

37
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java

@ -0,0 +1,37 @@
/**
* 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.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
public class NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 1);
}
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name();
String awaitAlias = "await on client state (NoSecBS Trigger None section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE, 1);
}
}

39
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java

@ -0,0 +1,39 @@
/**
* 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.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY;
public class NoSecLwM2MIntegrationBSNoTriggerTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + BOTH.name();
String awaitAlias = "await on client state (NoSecBS two section)";
basicTestConnectionStartBS(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
@Test
public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Lwm2m section)";
basicTestConnectionStartBS(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
}

29
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java

@ -0,0 +1,29 @@
/**
* 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.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY;
public class NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY, 1);
}
}

31
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java

@ -0,0 +1,31 @@
/**
* 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.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
public class NoSecLwM2MIntegrationBSTriggerTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name();
String awaitAlias = "await on client state (NoSecBS Trigger Two section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH, 1);
}
}

50
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java

@ -19,12 +19,6 @@ import org.junit.Test;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest {
//Lwm2m only //Lwm2m only
@ -34,48 +28,4 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false, false); super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false, false);
} }
// Bootstrap + Lwm2m
@Test
public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + BOTH.name();
String awaitAlias = "await on client state (NoSecBS two section)";
basicTestConnectionBefore(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
@Test
public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Lwm2m section)";
basicTestConnectionBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
// Bs trigger
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name();
String awaitAlias = "await on client state (NoSecBS Trigger Two section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH);
}
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY);
}
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY);
}
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name();
String awaitAlias = "await on client state (NoSecBS Trigger None section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE);
}
} }

13
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java

@ -58,8 +58,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Hex.decodeHex(keyPsk.toCharArray())); Hex.decodeHex(keyPsk.toCharArray()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
this.basicTestConnection(security, this.basicTestConnection(security, null,
null,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,
@ -85,8 +84,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
String awaitAlias = "await on client state (Psk_Lwm2m)"; String awaitAlias = "await on client state (Psk_Lwm2m)";
Device lwm2mDevice = this.basicTestConnection(security, Device lwm2mDevice = this.basicTestConnection(security, null,
null,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,
@ -121,8 +119,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
String awaitAlias = "await on client state (Psk_Lwm2m)"; String awaitAlias = "await on client state (Psk_Lwm2m)";
Device lwm2mDevice = this.basicTestConnection(security, Device lwm2mDevice = this.basicTestConnection(security, null,
null,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,
@ -175,12 +172,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setIdentity(identity); clientCredentials.setIdentity(identity);
clientCredentials.setKey(keyPsk); clientCredentials.setKey(keyPsk);
Security securityBs = pskBootstrap(SECURE_URI_BS, Security securityPskBs = pskBootstrap(SECURE_URI_BS,
identity.getBytes(StandardCharsets.UTF_8), identity.getBytes(StandardCharsets.UTF_8),
Hex.decodeHex(keyPsk.toCharArray())); Hex.decodeHex(keyPsk.toCharArray()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, BOTH)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
this.basicTestConnection(null, securityBs, this.basicTestConnection(null, securityPskBs,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java

@ -113,13 +113,13 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
RPKClientCredential clientCredentials = new RPKClientCredential(); RPKClientCredential clientCredentials = new RPKClientCredential();
clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setKey(Base64.encodeBase64String(certificate.getPublicKey().getEncoded())); clientCredentials.setKey(Base64.encodeBase64String(certificate.getPublicKey().getEncoded()));
Security securityBs = rpkBootstrap(SECURE_URI_BS, Security securityRpkBs = rpkBootstrap(SECURE_URI_BS,
certificate.getPublicKey().getEncoded(), certificate.getPublicKey().getEncoded(),
privateKey.getEncoded(), privateKey.getEncoded(),
serverX509CertBs.getPublicKey().getEncoded()); serverX509CertBs.getPublicKey().getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, BOTH)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, clientPrivateKeyFromCertTrust, certificate, RPK, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, clientPrivateKeyFromCertTrust, certificate, RPK, false);
this.basicTestConnection(null, securityBs, this.basicTestConnection(null, securityRpkBs,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,

8
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java

@ -51,15 +51,14 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg
X509ClientCredential clientCredentials = new X509ClientCredential(); X509ClientCredential clientCredentials = new X509ClientCredential();
clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setCert(Base64.getEncoder().encodeToString(certificate.getEncoded())); clientCredentials.setCert(Base64.getEncoder().encodeToString(certificate.getEncoded()));
Security security = x509(SECURE_URI, Security securityX509 = x509(SECURE_URI,
shortServerId, shortServerId,
certificate.getEncoded(), certificate.getEncoded(),
privateKey.getEncoded(), privateKey.getEncoded(),
serverX509Cert.getEncoded()); serverX509Cert.getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security, this.basicTestConnection(securityX509, null,
null,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,
@ -119,8 +118,7 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg
serverX509CertBs.getEncoded()); serverX509CertBs.getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security, this.basicTestConnection(security, null,
null,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,

6
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java

@ -50,8 +50,7 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra
serverX509Cert.getEncoded()); serverX509Cert.getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security, this.basicTestConnection(security, null,
null,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,
@ -77,8 +76,7 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra
serverX509CertBs.getEncoded()); serverX509CertBs.getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security, this.basicTestConnection(security,null,
null,
deviceCredentials, deviceCredentials,
clientEndpoint, clientEndpoint,
transportConfiguration, transportConfiguration,

1
common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.dao.tenant; package org.thingsboard.server.dao.tenant;
import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
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;

6
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo; import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
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.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
@ -110,6 +112,10 @@ public interface UserService extends EntityDaoService {
void removeMobileSession(TenantId tenantId, String mobileToken); void removeMobileSession(TenantId tenantId, String mobileToken);
PageData<User> findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink);
boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user);
UserAuthDetails findUserAuthDetailsByUserId(TenantId tenantId, UserId userId); UserAuthDetails findUserAuthDetailsByUserId(TenantId tenantId, UserId userId);
} }

112
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java

@ -0,0 +1,112 @@
/**
* 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.common.data.device.credentials.lwm2m;
/**
* Enum representing predefined LwM2M Short Server Identifiers.
* <p>
* See OMA Lightweight M2M Specification for details about the server identifier space.
*/
public enum Lwm2mServerIdentifier {
/**
* Not used for identifying an LwM2M Server (0).
*/
NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN(0, "Bootstrap Short Server ID", false),
/**
* Primary LwM2M Server Short Server ID (1).
* Upper boundary for valid LwM2M Server Identifiers (1–65534).
*/
PRIMARY_LWM2M_SERVER(1, "LwM2M Server Short Server ID", true),
/**
* Maximum valid LwM2M Server ID (65534).
* Upper boundary for valid LwM2M Server Identifiers (1–65534).
*/
LWM2M_SERVER_MAX(65534, "LwM2M Server Short Server ID", true),
/**
* Not used for identifying an LwM2M Server (65535).
* Reserved sentinel value representing "no server associated" or "invalid ID".
* MUST NOT be assigned to any LwM2M Server according to OMA-TS-LightweightM2M-Core, §6.2.1.
* OMA LwM2M Core / v1.2: Server / Short Server ID): «MAX_ID 65535 is a reserved value and MUST NOT be used for identifying an Object»
*/
NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX(65535, "Reserved sentinel value (no active server)", false);
private final Integer id;
private final String description;
private final boolean isLwm2mServer;
Lwm2mServerIdentifier(Integer id, String description, boolean isLwm2mServer) {
this.id = id;
this.description = description;
this.isLwm2mServer = isLwm2mServer;
}
/**
* @return the integer value of this Short Server ID.
*/
public Integer getId() {
return id;
}
/**
* @return a human-readable description of this Server ID.
*/
public String getDescription() {
return description;
}
/**
* @return true if this ID represents a Lwm2m Server.
*/
public boolean isLwm2mServer() {
return isLwm2mServer;
}
/**
* Checks whether a given ID represents a valid LwM2M Server (1–65534).
* @param id Short Server ID value.
* @return true if the ID belongs to a standard LwM2M Server.
*/
public static boolean isLwm2mServer(Integer id) {
return id != null && id >= PRIMARY_LWM2M_SERVER.id && id <= LWM2M_SERVER_MAX.id;
}
public static boolean isNotLwm2mServer(Integer id) {
return id == null || id < PRIMARY_LWM2M_SERVER.id || id > LWM2M_SERVER_MAX.id;
}
/**
* Returns a {@link Lwm2mServerIdentifier} instance matching the given ID.
* @param id numeric ID.
* @return corresponding enum constant.
* @throws IllegalArgumentException if no constant matches the given ID.
*/
public static Lwm2mServerIdentifier fromId(Integer id) {
for (Lwm2mServerIdentifier s : values()) {
if (s.id == id) {
return s;
}
}
throw new IllegalArgumentException("Unknown Lwm2mServerIdentifier: " + id);
}
@Override
public String toString() {
return name() + "(" + id + ") - " + description;
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java

@ -36,6 +36,7 @@ public class TelemetryMappingConfiguration implements Serializable {
private Set<String> attribute; private Set<String> attribute;
private Set<String> telemetry; private Set<String> telemetry;
private Map<String, ObjectAttributes> attributeLwm2m; private Map<String, ObjectAttributes> attributeLwm2m;
private Boolean initAttrTelAsObsStrategy;
private TelemetryObserveStrategy observeStrategy; private TelemetryObserveStrategy observeStrategy;
@JsonCreator @JsonCreator
@ -45,6 +46,7 @@ public class TelemetryMappingConfiguration implements Serializable {
@JsonProperty("attribute") Set<String> attribute, @JsonProperty("attribute") Set<String> attribute,
@JsonProperty("telemetry") Set<String> telemetry, @JsonProperty("telemetry") Set<String> telemetry,
@JsonProperty("attributeLwm2m") Map<String, ObjectAttributes> attributeLwm2m, @JsonProperty("attributeLwm2m") Map<String, ObjectAttributes> attributeLwm2m,
@JsonProperty("initAttrTelAsObsStrategy") Boolean initAttrTelAsObsStrategy,
@JsonProperty("observeStrategy") TelemetryObserveStrategy observeStrategy) { @JsonProperty("observeStrategy") TelemetryObserveStrategy observeStrategy) {
this.keyName = keyName != null ? keyName : Collections.emptyMap(); this.keyName = keyName != null ? keyName : Collections.emptyMap();
@ -52,6 +54,7 @@ public class TelemetryMappingConfiguration implements Serializable {
this.attribute = attribute != null ? attribute : Collections.emptySet(); this.attribute = attribute != null ? attribute : Collections.emptySet();
this.telemetry = telemetry != null ? telemetry : Collections.emptySet(); this.telemetry = telemetry != null ? telemetry : Collections.emptySet();
this.attributeLwm2m = attributeLwm2m != null ? attributeLwm2m : Collections.emptyMap(); this.attributeLwm2m = attributeLwm2m != null ? attributeLwm2m : Collections.emptyMap();
this.initAttrTelAsObsStrategy = initAttrTelAsObsStrategy != null ? initAttrTelAsObsStrategy : false;
this.observeStrategy = observeStrategy != null ? observeStrategy : TelemetryObserveStrategy.SINGLE; this.observeStrategy = observeStrategy != null ? observeStrategy : TelemetryObserveStrategy.SINGLE;
} }
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java

@ -26,7 +26,7 @@ public class LwM2MServerSecurityConfig implements Serializable {
@Schema(description = "Server short Id. Used as link to associate server Object Instance. This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. " + @Schema(description = "Server short Id. Used as link to associate server Object Instance. This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. " +
"This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. " + "This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. " +
"The values ID:1 and ID:65534 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY) "The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY)
protected Integer shortServerId = 123; protected Integer shortServerId = 123;
/** Security -> ObjectId = 0 'LWM2M Security' */ /** Security -> ObjectId = 0 'LWM2M Security' */
@Schema(description = "Is Bootstrap Server or Lwm2m Server. " + @Schema(description = "Is Bootstrap Server or Lwm2m Server. " +

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java

@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.notification.targets.platform;
import lombok.Data; import lombok.Data;
@Data @Data
public class AllUsersFilter implements UsersFilter { public class AllUsersFilter implements SystemLevelUsersFilter {
@Override @Override
public UsersFilterType getType() { public UsersFilterType getType() {

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java

@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.notification.targets.platform;
import lombok.Data; import lombok.Data;
@Data @Data
public class SystemAdministratorsFilter implements UsersFilter { public class SystemAdministratorsFilter implements SystemLevelUsersFilter {
@Override @Override
public UsersFilterType getType() { public UsersFilterType getType() {

19
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java

@ -0,0 +1,19 @@
/**
* 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.common.data.notification.targets.platform;
public interface SystemLevelUsersFilter extends UsersFilter {
}

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java

@ -21,7 +21,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Data @Data
public class TenantAdministratorsFilter implements UsersFilter { public class TenantAdministratorsFilter implements SystemLevelUsersFilter {
private Set<UUID> tenantsIds; private Set<UUID> tenantsIds;
private Set<UUID> tenantProfilesIds; private Set<UUID> tenantProfilesIds;

4
common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java

@ -20,8 +20,10 @@ public enum Authority {
SYS_ADMIN(0), SYS_ADMIN(0),
TENANT_ADMIN(1), TENANT_ADMIN(1),
CUSTOMER_USER(2), CUSTOMER_USER(2),
REFRESH_TOKEN(10), REFRESH_TOKEN(10),
PRE_VERIFICATION_TOKEN(11); PRE_VERIFICATION_TOKEN(11),
MFA_CONFIGURATION_TOKEN(12);
private int code; private int code;

4
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java

@ -24,11 +24,15 @@ import org.thingsboard.server.common.data.HasVersion;
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 java.io.Serial;
@Schema @Schema
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class DeviceCredentials extends BaseData<DeviceCredentialsId> implements DeviceCredentialsFilter, HasVersion { public class DeviceCredentials extends BaseData<DeviceCredentialsId> implements DeviceCredentialsFilter, HasVersion {
@Serial
private static final long serialVersionUID = -7869261127032877765L; private static final long serialVersionUID = -7869261127032877765L;
private DeviceId deviceId; private DeviceId deviceId;
private DeviceCredentialsType credentialsType; private DeviceCredentialsType credentialsType;
private String credentialsId; private String credentialsId;

3
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java

@ -21,6 +21,7 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter;
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;
@ -46,6 +47,8 @@ public class PlatformTwoFaSettings {
@Min(value = 60) @Min(value = 60)
private Integer totalAllowedTimeForVerification; private Integer totalAllowedTimeForVerification;
private boolean enforceTwoFa;
private SystemLevelUsersFilter enforcedUsersFilter;
public Optional<TwoFaProviderConfig> getProviderConfig(TwoFaProviderType providerType) { public Optional<TwoFaProviderConfig> getProviderConfig(TwoFaProviderType providerType) {
return Optional.ofNullable(providers) return Optional.ofNullable(providers)

13
common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java

@ -95,4 +95,17 @@ public class CollectionsUtil {
return false; return false;
} }
public static <T> Set<T> addToSet(Set<T> existing, T value) {
if (existing == null || existing.isEmpty()) {
return Set.of(value);
}
if (existing.contains(value)) {
return existing;
}
Set<T> newSet = new HashSet<>(existing.size() + 1);
newSet.addAll(existing);
newSet.add(value);
return (Set<T>) Set.of(newSet.toArray());
}
} }

6
common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java

@ -23,16 +23,15 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg;
import java.io.Serial;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/**
* @author Andrew Shvayka
*/
@Data @Data
public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg { public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg {
@Serial
private static final long serialVersionUID = 2422071590415277039L; private static final long serialVersionUID = 2422071590415277039L;
private final TenantId tenantId; private final TenantId tenantId;
@ -56,4 +55,5 @@ public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotifi
public MsgType getMsgType() { public MsgType getMsgType() {
return MsgType.DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG; return MsgType.DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG;
} }
} }

4
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java

@ -127,7 +127,9 @@ public class LwM2MBootstrapConfig implements Serializable {
private BootstrapConfig.ServerConfig setServerConfig (AbstractLwM2MBootstrapServerCredential serverCredential) { private BootstrapConfig.ServerConfig setServerConfig (AbstractLwM2MBootstrapServerCredential serverCredential) {
BootstrapConfig.ServerConfig serverConfig = new BootstrapConfig.ServerConfig(); BootstrapConfig.ServerConfig serverConfig = new BootstrapConfig.ServerConfig();
serverConfig.shortId = serverCredential.getShortServerId(); if (serverCredential.getShortServerId() != null) {
serverConfig.shortId = serverCredential.getShortServerId();
}
serverConfig.lifetime = serverCredential.getLifetime(); serverConfig.lifetime = serverCredential.getLifetime();
serverConfig.defaultMinPeriod = serverCredential.getDefaultMinPeriod(); serverConfig.defaultMinPeriod = serverCredential.getDefaultMinPeriod();
serverConfig.notifIfDisabled = serverCredential.isNotifIfDisabled(); serverConfig.notifIfDisabled = serverCredential.isNotifIfDisabled();

17
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.bootstrap.secure; package org.thingsboard.server.transport.lwm2m.bootstrap.secure;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.leshan.core.peer.IpPeer; import org.eclipse.leshan.core.peer.IpPeer;
import org.eclipse.leshan.core.peer.LwM2mPeer; import org.eclipse.leshan.core.peer.LwM2mPeer;
import org.eclipse.leshan.core.peer.PskIdentity; import org.eclipse.leshan.core.peer.PskIdentity;
@ -112,8 +113,10 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
} catch (InvalidConfigurationException e){ } catch (InvalidConfigurationException e){
log.error("Failed put to lwM2MBootstrapSessionClients by endpoint [{}]", request.getEndpointName(), e); log.error("Failed put to lwM2MBootstrapSessionClients by endpoint [{}]", request.getEndpointName(), e);
} }
String msg = String.format("Bootstrap session started... %s", ((Request) request.getCoapRequest()).getLocalAddress().toString());
log.warn(String.format("%s: %s", request.getEndpointName(), msg));
this.sendLogs(request.getEndpointName(), this.sendLogs(request.getEndpointName(),
String.format("%s: Bootstrap session started...", LOG_LWM2M_INFO, request.getEndpointName())); String.format("%s: %s", LOG_LWM2M_INFO, msg));
} }
return session; return session;
} }
@ -135,7 +138,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
session.setModel(modelProvider.getObjectModel(session, tasks.supportedObjects)); session.setModel(modelProvider.getObjectModel(session, tasks.supportedObjects));
// set Requests to Send // set Requests to Send
log.info("tasks.requestsToSend = [{}]", tasks.requestsToSend); log.warn("tasks.requestsToSend = [{}]", tasks.requestsToSend);
session.setRequests(tasks.requestsToSend); session.setRequests(tasks.requestsToSend);
// prepare list where we will store Responses // prepare list where we will store Responses
@ -182,14 +185,16 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
session.getResponses().add(response); session.getResponses().add(response);
String msg = String.format("%s: receives success response for: %s %s %s", LOG_LWM2M_INFO, String msg = String.format("%s: receives success response for: %s %s %s", LOG_LWM2M_INFO,
request.getClass().getSimpleName(), request.getPath().toString(), response.toString()); request.getClass().getSimpleName(), request.getPath().toString(), response.toString());
log.warn(msg);
this.sendLogs(bsSession.getEndpoint(), msg); this.sendLogs(bsSession.getEndpoint(), msg);
// on success for NOT bootstrap finish request we send next request // on success for NOT bootstrap finish request we send next request
return BootstrapPolicy.continueWith(nextRequest(bsSession)); return BootstrapPolicy.continueWith(nextRequest(bsSession));
} else { } else {
// on success for bootstrap finish request we stop the session // on success for bootstrap finish request we stop the session
this.sendLogs(bsSession.getEndpoint(), String msg = String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO);
String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO)); log.info(msg);
this.sendLogs(bsSession.getEndpoint(), msg);
this.tasksProvider.remove(bsSession.getEndpoint()); this.tasksProvider.remove(bsSession.getEndpoint());
return BootstrapPolicy.finished(); return BootstrapPolicy.finished();
} }
@ -228,7 +233,9 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
@Override @Override
public void end(BootstrapSession bsSession) { public void end(BootstrapSession bsSession) {
this.sendLogs(bsSession.getEndpoint(), String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO)); String msg = String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO);
log.warn(msg);
this.sendLogs(bsSession.getEndpoint(), msg);
this.tasksProvider.remove(bsSession.getEndpoint()); this.tasksProvider.remove(bsSession.getEndpoint());
} }

326
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java

@ -17,15 +17,12 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.store;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.request.BootstrapDeleteRequest; import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
import org.eclipse.leshan.core.request.BootstrapReadRequest;
import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.response.BootstrapDiscoverResponse; import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
import org.eclipse.leshan.core.response.BootstrapReadResponse;
import org.eclipse.leshan.core.response.LwM2mResponse; import org.eclipse.leshan.core.response.LwM2mResponse;
import org.eclipse.leshan.server.bootstrap.BootstrapConfig; import org.eclipse.leshan.server.bootstrap.BootstrapConfig;
import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore; import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
@ -33,7 +30,6 @@ import org.eclipse.leshan.server.bootstrap.BootstrapSession;
import org.eclipse.leshan.server.bootstrap.BootstrapUtil; import org.eclipse.leshan.server.bootstrap.BootstrapUtil;
import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -43,14 +39,18 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest; import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID_0; import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isLwm2mServer;
@Slf4j @Slf4j
public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider { public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider {
@ -77,11 +77,11 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
@Override @Override
public Tasks getTasks(BootstrapSession session, List<LwM2mResponse> previousResponse) { public Tasks getTasks(BootstrapSession session, List<LwM2mResponse> previousResponse) {
// BootstrapConfig config = store.get(session.getEndpoint(), session.getClientTransportData().getIdentity(), session); // BootstrapConfig config = store.get(session.getEndpoint(), session.getClientTransportData().getIdentity(), session);
BootstrapConfig config = store.get(session); BootstrapConfig configNew = store.get(session);
if (config == null) { if (configNew == null) {
return null; return null;
} }
if (previousResponse == null && shouldStartWithDiscover(config)) { if (previousResponse == null && shouldStartWithDiscover(configNew)) {
Tasks tasks = new Tasks(); Tasks tasks = new Tasks();
tasks.requestsToSend = new ArrayList<>(1); tasks.requestsToSend = new ArrayList<>(1);
tasks.requestsToSend.add(new BootstrapDiscoverRequest()); tasks.requestsToSend.add(new BootstrapDiscoverRequest());
@ -96,47 +96,27 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
tasks.supportedObjects = this.supportedObjects; tasks.supportedObjects = this.supportedObjects;
// handle bootstrap discover response // handle bootstrap discover response
if (previousResponse != null) { if (previousResponse != null) {
if (previousResponse.get(0) instanceof BootstrapDiscoverResponse) { if (previousResponse.get(0) instanceof BootstrapDiscoverResponse discoverResponse) {
BootstrapDiscoverResponse discoverResponse = (BootstrapDiscoverResponse) previousResponse.get(0);
if (discoverResponse.isSuccess()) { if (discoverResponse.isSuccess()) {
this.initAfterBootstrapDiscover(discoverResponse); this.initAfterBootstrapDiscover(discoverResponse);
findSecurityInstanceId(discoverResponse.getObjectLinks(), session.getEndpoint()); /// Short Server Ids - in old config
} else { findInstancesIdOldByServerId(discoverResponse, session.getEndpoint());
log.warn( log.warn(
"Bootstrap Discover return error {} : to continue bootstrap session without autoIdForSecurityObject mode. {}", "Bootstrap server instance successfully found in Security Object (0) in response {}. Continuing bootstrap session. Session: {}",
discoverResponse, session); discoverResponse, session);
} } else {
if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0) == null) { log.warn(
log.error( "Unable to find bootstrap server instance in Security Object (0) in response {}. Continuing bootstrap session with autoIdForSecurityObject mode, ignoring information from discoverResponse. Session: {}",
"Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}",
discoverResponse, session); discoverResponse, session);
return null;
}
tasks.requestsToSend = new ArrayList<>(1);
tasks.requestsToSend.add(new BootstrapReadRequest("/1"));
tasks.last = false;
return tasks;
}
BootstrapReadResponse readResponse = (BootstrapReadResponse) previousResponse.get(0);
Integer bootstrapServerIdOld = null;
if (readResponse.isSuccess()) {
findServerInstanceId(readResponse, session.getEndpoint());
if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().size() > 0 && this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getServerInstances().size() > 0) {
bootstrapServerIdOld = this.findBootstrapServerId(session.getEndpoint());
} }
} else {
log.warn(
"Bootstrap ReadResponse return error {} : to continue bootstrap session without find Server Instance Id. {}",
readResponse, session);
} }
// create requests from config // create requests from config
tasks.requestsToSend = this.toRequests(config, tasks.requestsToSend = this.toRequests(configNew,
config.contentFormat != null ? config.contentFormat : session.getContentFormat(), configNew.contentFormat != null ? configNew.contentFormat : session.getContentFormat(), session.getEndpoint());
bootstrapServerIdOld, session.getEndpoint());
} else { } else {
// create requests from config // create requests from config
tasks.requestsToSend = BootstrapUtil.toRequests(config, tasks.requestsToSend = BootstrapUtil.toRequests(configNew,
config.contentFormat != null ? config.contentFormat : session.getContentFormat()); configNew.contentFormat != null ? configNew.contentFormat : session.getContentFormat());
} }
return tasks; return tasks;
} }
@ -148,81 +128,57 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
/** /**
* "Short Server ID": This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. * "Short Server ID": This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'.
* The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server. * "Short Lwm2m Server ID":
* "Short Server ID":
* - Link Instance (lwm2m Server) hase linkParams with key = "ssid" value = "shortId" (ver lvm2m = 1.1). * - Link Instance (lwm2m Server) hase linkParams with key = "ssid" value = "shortId" (ver lvm2m = 1.1).
* - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.0). * The values ID:0 values MUST NOT be used for identifying the LwM2M Server only BS.
*/ */
protected void findSecurityInstanceId(Link[] objectLinks, String endpoint) { protected void findInstancesIdOldByServerId(BootstrapDiscoverResponse discoverResponses, String endpoint) {
log.info("Object after discover: [{}]", objectLinks); log.info("Object after discover: [{}]", Arrays.toString(discoverResponses.getObjectLinks()));
for (Link link : objectLinks) { for (Link link : discoverResponses.getObjectLinks()) {
if (link.getUriReference().startsWith("/0/")) { LwM2mPath path = new LwM2mPath(link.getUriReference());
try { if (path.isObjectInstance()) {
LwM2mPath path = new LwM2mPath(link.getUriReference()); int lwm2mShortServerId = 0;
if (path.isObjectInstance()) { if (path.getObjectId() == 0) {
if (link.getAttributes().get("ssid") != null) { if (link.getAttributes().get("ssid") != null) {
int serverId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue()); lwm2mShortServerId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue());
if (!lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(serverId)) { if (validateLwm2mShortServerId(lwm2mShortServerId)) {
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(lwm2mShortServerId, path.getObjectInstanceId());
} else { } else {
log.error("Invalid lwm2mSecurityInstance by [{}]", path.getObjectInstanceId()); log.error("Invalid lwm2mSecurityInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId);
} }
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); } else {
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(null, path.getObjectInstanceId());
}
} else if (path.getObjectId() == 1) {
if (link.getAttributes().get("ssid") != null) {
lwm2mShortServerId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue());
if (validateLwm2mShortServerId(lwm2mShortServerId)) {
this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().putIfAbsent(lwm2mShortServerId, path.getObjectInstanceId());
} else { } else {
if (!this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(0)) { log.error("Invalid lwm2mServerInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId);
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID_0, path.getObjectInstanceId());
} else {
log.error("Invalid bootstrapSecurityInstance by [{}]", path.getObjectInstanceId());
}
} }
} }
} catch (Exception e) {
// ignore if this is not a LWM2M path
log.error("Invalid LwM2MPath starting by \"/0/\"");
} }
} }
} }
} }
protected void findServerInstanceId(BootstrapReadResponse readResponse, String endpoint) {
try {
((LwM2mObject) readResponse.getContent()).getInstances().values().forEach(instance -> {
var shId = OPAQUE.equals(instance.getResource(0).getType()) ? new BigInteger((byte[]) instance.getResource(0).getValue()).intValue() : instance.getResource(0).getValue();
int shortId;
if (shId instanceof Long) {
shortId = ((Long) shId).intValue();
} else {
shortId = (int) shId;
}
this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().put(shortId, instance.getId());
});
} catch (Exception e) {
log.error("Failed find Server Instance Id. ", e);
}
}
protected Integer findBootstrapServerId(String endpoint) {
Integer bootstrapServerIdOld = null;
Map<Integer, Integer> filteredMap = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().entrySet()
.stream().filter(x -> !this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(x.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (filteredMap.size() > 0) {
bootstrapServerIdOld = filteredMap.keySet().stream().findFirst().get();
}
return bootstrapServerIdOld;
}
public BootstrapConfigStore getStore() { public BootstrapConfigStore getStore() {
return this.store; return this.store;
} }
private void initAfterBootstrapDiscover(BootstrapDiscoverResponse response) { private void initAfterBootstrapDiscover(BootstrapDiscoverResponse response) {
Link[] links = response.getObjectLinks(); Link[] links = response.getObjectLinks();
AtomicReference<String> verDefault = new AtomicReference<>("1.0");
Arrays.stream(links).forEach(link -> { Arrays.stream(links).forEach(link -> {
LwM2mPath path = new LwM2mPath(link.getUriReference()); LwM2mPath path = new LwM2mPath(link.getUriReference());
if (!path.isRoot() && path.getObjectId() < 3) { if (path.isRoot()) {
if (link.hasAttribute() && link.getAttributes().get("lwm2m") != null) {
verDefault.set(link.getAttributes().get("lwm2m").getValue().toString());
}
} else if (path.getObjectId() <= ACCESS_CONTROL) {
if (path.isObject()) { if (path.isObject()) {
String ver = link.getAttributes().get("ver") != null ? link.getAttributes().get("ver").getCoreLinkValue() : "1.0"; String ver = (link.hasAttribute() && link.getAttributes().get("ver") != null) ? link.getAttributes().get("ver").getCoreLinkValue() : verDefault.get();
this.supportedObjects.put(path.getObjectId(), ver); this.supportedObjects.put(path.getObjectId(), ver);
} }
} }
@ -230,94 +186,124 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
} }
public List<BootstrapDownlinkRequest<? extends LwM2mResponse>> toRequests(BootstrapConfig bootstrapConfig, /** Map<serverId ("Short Server ID"), InstanceId> => LwM2MBootstrapClientInstanceIds
* 1) Both
* - (Short) Server ID == null bs)
* SECURITY = 0; InstanceId = 0
* - Short Server ID == 1 - 65534 lwm2m)
* SECURITY = 0; InstanceId = 1
* SERVER = 1; InstanceId = 0
* 2) Only BS Server
* - Short Server ID == null bs)
* SECURITY = 0; InstanceId = 0
* 3) Only Lwm2m Server
* - Short Server ID == 1 - 65534 lwm2m)
* SECURITY = 0; InstanceId = 0
* SERVER = 1; InstanceId = 0
* */
public List<BootstrapDownlinkRequest<? extends LwM2mResponse>> toRequests(BootstrapConfig bootstrapConfigNew,
ContentFormat contentFormat, ContentFormat contentFormat,
Integer bootstrapServerIdOld,
String endpoint) { String endpoint) {
Integer bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(null) == null ?
-2 : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(null);
List<BootstrapDownlinkRequest<? extends LwM2mResponse>> requests = new ArrayList<>(); List<BootstrapDownlinkRequest<? extends LwM2mResponse>> requests = new ArrayList<>();
Set<String> pathsDelete = new HashSet<>(); Set<String> pathsDelete = new HashSet<>();
List<BootstrapDownlinkRequest<? extends LwM2mResponse>> requestsWrite = new ArrayList<>(); ConcurrentHashMap<String, BootstrapDownlinkRequest<? extends LwM2mResponse>> requestsWrite = new ConcurrentHashMap<>();
boolean isBsServer = false;
boolean isLwServer = false; /// handle security & handle
/** Map<serverId ("Short Server ID"), InstanceId> */ // bootstrap Security new - There can only be one instance of bootstrap at a time.
Map<Integer, Integer> instances = new HashMap<>(); /// bs: handle security only
Integer bootstrapServerIdNew = null; for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfigNew.security).values()) {
// handle security if (security.bootstrapServer && bootstrapSecurityInstanceId > -1) {
int lwm2mSecurityInstanceId = 0; // delete old bootstrap Security
int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0); String path = "/" + SECURITY + "/" + bootstrapSecurityInstanceId;
for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) { pathsDelete.add(path);
if (security.bootstrapServer) { security.serverId = null;
requestsWrite.add(toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat)); requestsWrite.put(path, toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat));
isBsServer = true;
bootstrapServerIdNew = security.serverId;
instances.put(security.serverId, bootstrapSecurityInstanceId);
} else {
if (lwm2mSecurityInstanceId == bootstrapSecurityInstanceId) {
lwm2mSecurityInstanceId++;
}
requestsWrite.add(toWriteRequest(lwm2mSecurityInstanceId, security, contentFormat));
instances.put(security.serverId, lwm2mSecurityInstanceId);
isLwServer = true;
if (!isBsServer && this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(security.serverId) &&
lwm2mSecurityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId)) {
pathsDelete.add("/0/" + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId));
}
/**
* If there is an instance in the serverInstances with serverId which we replace in the securityInstances
*/
// find serverId in securityInstances by id (instance)
Integer serverIdOld = null;
for (Map.Entry<Integer, Integer> entry : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().entrySet()) {
if (entry.getValue().equals(lwm2mSecurityInstanceId)) {
serverIdOld = entry.getKey();
}
}
if (!isBsServer && serverIdOld != null && this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(serverIdOld)) {
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(serverIdOld));
}
lwm2mSecurityInstanceId++;
} }
} }
// handle server
for (Map.Entry<Integer, BootstrapConfig.ServerConfig> server : bootstrapConfig.servers.entrySet()) { /** lwm2m servers: Multiple instances of lwm2m servers can run simultaneously by SHORT_ID
int securityInstanceId = instances.get(server.getValue().shortId); if update -> delete and write by InstanceId
requestsWrite.add(toWriteRequest(securityInstanceId, server.getValue(), contentFormat)); if new -> only write with InstanceIdMax++
if (!isBsServer) { */
/** Delete instance if bootstrapServerIdNew not equals bootstrapServerIdOld or securityInstanceBsIdNew not equals serverInstanceBsIdOld */
if (bootstrapServerIdNew != null && server.getValue().shortId == bootstrapServerIdNew && /// lwm2m server: handle security & server
(bootstrapServerIdNew != bootstrapServerIdOld || securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld))) { //max Lwm2m Security instance old id if new
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld)); int lwm2mSecurityInstanceIdMax = -1;
/** Delete instance if serverIdNew is present in serverInstances and securityInstanceIdOld by serverIdNew not equals serverInstanceIdOld */ for (Integer shortId : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().keySet()) {
} else if (this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(server.getValue().shortId) && if (isLwm2mServer(shortId)) {
securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId)) { lwm2mSecurityInstanceIdMax = Math.max(
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId)); this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(shortId),
} lwm2mSecurityInstanceIdMax);
} }
} }
// handle acl //max Lwm2m Server instance old id if new
for (Map.Entry<Integer, BootstrapConfig.ACLConfig> acl : bootstrapConfig.acls.entrySet()) { int lwm2mServerInstanceIdMax = -1;
requestsWrite.add(toWriteRequest(acl.getKey(), acl.getValue(), contentFormat)); for (Integer shortId : this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().keySet()) {
if (isLwm2mServer(shortId)) {
lwm2mServerInstanceIdMax = Math.max(
this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(shortId),
lwm2mServerInstanceIdMax);
}
} }
// handle delete // Lwm2m update or new
if (isBsServer && isLwServer) { for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfigNew.security).values()) {
requests.add(new BootstrapDeleteRequest("/0")); if (!security.bootstrapServer) {
requests.add(new BootstrapDeleteRequest("/1")); // Security
} else { Integer secureInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId);
pathsDelete.forEach(pathDelete -> requests.add(new BootstrapDeleteRequest(pathDelete))); if (secureInstanceId != null) {
pathsDelete.add("/" + SECURITY + "/" + secureInstanceId);
requestsWrite.put("/" + SECURITY + "/" + secureInstanceId, toWriteRequest(secureInstanceId, security, contentFormat));
} else {
secureInstanceId = ++lwm2mSecurityInstanceIdMax;
if (bootstrapSecurityInstanceId.equals(secureInstanceId)) {
secureInstanceId = ++lwm2mSecurityInstanceIdMax;
}
requestsWrite.put("/" + SECURITY + "/" + secureInstanceId, toWriteRequest(secureInstanceId, security, contentFormat));
}
Integer serverInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(security.serverId);
if (serverInstanceId != null) {
pathsDelete.add("/" + SERVER + "/" + serverInstanceId);
} else {
serverInstanceId = ++lwm2mServerInstanceIdMax;
}
Integer finalServerInstanceId = serverInstanceId;
new TreeMap<>(bootstrapConfigNew.servers).values().stream()
.filter(server -> server.shortId == security.serverId)
.findFirst()
.ifPresent(server ->
requestsWrite.put(
"/" + SERVER + "/" + finalServerInstanceId,
toWriteRequest(finalServerInstanceId, server, contentFormat)
)
);
}
} }
// handle write
if (requestsWrite.size() > 0) { /// handle acl
requests.addAll(requestsWrite); for (Map.Entry<Integer, BootstrapConfig.ACLConfig> acl : bootstrapConfigNew.acls.entrySet()) {
requestsWrite.put("/" + ACCESS_CONTROL + "/" + acl.getKey(), toWriteRequest(acl.getKey(), acl.getValue(), contentFormat));
}
/// handle delete
pathsDelete.forEach(pathDelete -> requests.add(new BootstrapDeleteRequest(pathDelete)));
/// handle write
if (!requestsWrite.isEmpty()) {
requests.addAll(requestsWrite.values());
} }
return (requests); return (requests);
} }
private void initSupportedObjectsDefault() { private void initSupportedObjectsDefault() {
this.supportedObjects = new HashMap<>(); this.supportedObjects = new HashMap<>();
this.supportedObjects.put(0, "1.1"); this.supportedObjects.put(SECURITY, "1.1");
this.supportedObjects.put(1, "1.1"); this.supportedObjects.put(SERVER, "1.1");
this.supportedObjects.put(2, "1.0"); this.supportedObjects.put(ACCESS_CONTROL, "1.0");
}
private boolean validateLwm2mShortServerId(int id){
return id >= PRIMARY_LWM2M_SERVER.getId() && id <= LWM2M_SERVER_MAX.getId();
} }
@Override @Override

11
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java

@ -21,6 +21,10 @@ import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
import java.util.Map; import java.util.Map;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isNotLwm2mServer;
public class LwM2MConfigurationChecker extends ConfigurationChecker { public class LwM2MConfigurationChecker extends ConfigurationChecker {
@Override @Override
@ -74,15 +78,16 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker {
* This Resource MUST be set when the Bootstrap-Server Resource has false value. * This Resource MUST be set when the Bootstrap-Server Resource has false value.
* Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server (Section 6.3 of the LwM2M version 1.0 specification). * Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server (Section 6.3 of the LwM2M version 1.0 specification).
*/ */
if (!security.bootstrapServer && (srvCfg.shortId < 1 && srvCfg.shortId > 65534 )) { if (!security.bootstrapServer && isNotLwm2mServer(srvCfg.shortId)) {
throw new InvalidConfigurationException("Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server"); throw new InvalidConfigurationException("Specific ID:" + NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN.getId() + " and ID:" + NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId() + " values MUST NOT be used for identifying the LwM2M Server");
} }
} }
} }
protected static BootstrapConfig.ServerSecurity getSecurityEntry(BootstrapConfig config, int shortId) { protected static BootstrapConfig.ServerSecurity getSecurityEntry(BootstrapConfig config, int shortId) {
for (Map.Entry<Integer, BootstrapConfig.ServerSecurity> es : config.security.entrySet()) { for (Map.Entry<Integer, BootstrapConfig.ServerSecurity> es : config.security.entrySet()) {
if (es.getValue().serverId == shortId) { if ((es.getValue().serverId == null && shortId == 0) ||
(es.getValue().serverId != null && es.getValue().serverId == shortId)) {
return es.getValue(); return es.getValue();
} }
} }

11
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java

@ -38,6 +38,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@ -180,8 +181,16 @@ public class LwM2mTransportServerHelper {
case BOOLEAN: case BOOLEAN:
kvProto.setType(BOOLEAN_V).setBoolV((Boolean) value).build(); kvProto.setType(BOOLEAN_V).setBoolV((Boolean) value).build();
break; break;
case STRING:
case TIME: case TIME:
if (value instanceof Date) {
kvProto.setType(TransportProtos.KeyValueType.LONG_V).setLongV(((Date) value).getTime());
} else if (value instanceof Integer || value instanceof Long) {
kvProto.setType(TransportProtos.KeyValueType.LONG_V).setLongV((long) (value));
} else {
kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV(value.toString());
}
break;
case STRING:
case OPAQUE: case OPAQUE:
case OBJLNK: case OBJLNK:
kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV((String) value); kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV((String) value);

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -66,7 +66,9 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_TRIGGER_PARAMS_ID;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.REGISTRATION_TRIGGER_PARAMS_ID;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@ -342,6 +344,10 @@ public class LwM2mClient {
public String isValidObjectVersion(String path) { public String isValidObjectVersion(String path) {
LwM2mPath pathIds = getLwM2mPathFromString(path); LwM2mPath pathIds = getLwM2mPathFromString(path);
if (pathIds.isResource() && (pathIds.toString().equals(REGISTRATION_TRIGGER_PARAMS_ID ) ||
pathIds.toString().equals(BOOTSTRAP_TRIGGER_PARAMS_ID))) {
return "";
}
LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId()); LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId());
if (verSupportedObject == null) { if (verSupportedObject == null) {
return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId()); return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId());

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java

@ -23,6 +23,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MUplinkTarge
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.util.concurrent.CountDownLatch;
@Slf4j @Slf4j
public class TbLwM2MObserveCompositeCallback extends TbLwM2MUplinkTargetedCallback<ObserveCompositeRequest, ObserveCompositeResponse> { public class TbLwM2MObserveCompositeCallback extends TbLwM2MUplinkTargetedCallback<ObserveCompositeRequest, ObserveCompositeResponse> {

54
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java

@ -94,6 +94,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttrib
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig;
import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfigService; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfigService;
@ -483,9 +485,9 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
private void initClientTelemetry(LwM2mClient lwM2MClient) { private void initClientTelemetry(LwM2mClient lwM2MClient) {
Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getRegistration()); Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getRegistration());
Set<String> supportedObjects = clientContext.getSupportedIdVerInClient(lwM2MClient); Set<String> supportedObjects = clientContext.getSupportedIdVerInClient(lwM2MClient);
if (supportedObjects != null && supportedObjects.size() > 0) { if (supportedObjects != null && !supportedObjects.isEmpty()) {
this.sendReadRequests(lwM2MClient, profile, supportedObjects);
this.sendInitObserveRequests(lwM2MClient, profile, supportedObjects); this.sendInitObserveRequests(lwM2MClient, profile, supportedObjects);
this.sendReadRequests(lwM2MClient, profile, supportedObjects);
this.sendWriteAttributeRequests(lwM2MClient, profile, supportedObjects); this.sendWriteAttributeRequests(lwM2MClient, profile, supportedObjects);
// Removed. Used only for debug. // Removed. Used only for debug.
// this.sendDiscoverRequests(lwM2MClient, profile, supportedObjects); // this.sendDiscoverRequests(lwM2MClient, profile, supportedObjects);
@ -495,14 +497,30 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
private void sendReadRequests(LwM2mClient lwM2MClient, Lwm2mDeviceProfileTransportConfiguration profile, Set<String> supportedObjects) { private void sendReadRequests(LwM2mClient lwM2MClient, Lwm2mDeviceProfileTransportConfiguration profile, Set<String> supportedObjects) {
try { try {
Set<String> targetIds = new HashSet<>(profile.getObserveAttr().getAttribute()); Set<String> targetIds = new HashSet<>(profile.getObserveAttr().getAttribute());
Boolean initAttrTelAsObsStrategy = profile.getObserveAttr().getInitAttrTelAsObsStrategy();
targetIds.addAll(profile.getObserveAttr().getTelemetry()); targetIds.addAll(profile.getObserveAttr().getTelemetry());
targetIds = diffSets(profile.getObserveAttr().getObserve(), targetIds); targetIds = diffSets(profile.getObserveAttr().getObserve(), targetIds);
targetIds = targetIds.stream().filter(target -> isSupportedTargetId(supportedObjects, target)).collect(Collectors.toSet()); targetIds = targetIds.stream().filter(target -> isSupportedTargetId(supportedObjects, target)).collect(Collectors.toSet());
if (!targetIds.isEmpty()) {
CountDownLatch latch = new CountDownLatch(targetIds.size()); TelemetryObserveStrategy observeStrategy = profile.getObserveAttr().getObserveStrategy();
targetIds.forEach(versionedId -> sendReadRequest(lwM2MClient, versionedId, long timeoutMs = config.getTimeout();
new TbLwM2MLatchCallback<>(latch, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId)))); if (initAttrTelAsObsStrategy && observeStrategy != SINGLE) {
latch.await(config.getTimeout(), TimeUnit.MILLISECONDS); switch (observeStrategy) {
case COMPOSITE_ALL -> {
sendReadCompositeRequest(lwM2MClient, targetIds.toArray(new String[0]));
}
case COMPOSITE_BY_OBJECT -> {
Map<Integer, String[]> versionedObjectIds = groupByObjectIdVersionedIds(targetIds);
versionedObjectIds.forEach((k, v) -> sendReadCompositeRequest(lwM2MClient, v));
}
}
} else {
CountDownLatch latch = new CountDownLatch(targetIds.size());
targetIds.forEach(versionedId -> sendReadSingleRequest(lwM2MClient, versionedId,
new TbLwM2MLatchCallback<>(latch, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId))));
latch.await(timeoutMs, TimeUnit.MILLISECONDS);
}
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error("[{}] Failed to await Read requests!", lwM2MClient.getEndpoint(), e); log.error("[{}] Failed to await Read requests!", lwM2MClient.getEndpoint(), e);
} catch (Exception e) { } catch (Exception e) {
@ -529,17 +547,11 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
if (!completed) log.trace("[{}] Timeout occurred during SINGLE observe init", lwM2MClient.getEndpoint()); if (!completed) log.trace("[{}] Timeout occurred during SINGLE observe init", lwM2MClient.getEndpoint());
} }
case COMPOSITE_ALL -> { case COMPOSITE_ALL -> {
CountDownLatch latch = new CountDownLatch(targetIds.size());
sendObserveCompositeRequest(lwM2MClient, targetIds.toArray(new String[0])); sendObserveCompositeRequest(lwM2MClient, targetIds.toArray(new String[0]));
boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS);
if (!completed) log.trace("[{}] Timeout occurred during COMPOSITE_ALL observe init", lwM2MClient.getEndpoint());
} }
case COMPOSITE_BY_OBJECT -> { case COMPOSITE_BY_OBJECT -> {
Map<Integer, String[]> versionedObjectIds = groupByObjectIdVersionedIds(targetIds); Map<Integer, String[]> versionedObjectIds = groupByObjectIdVersionedIds(targetIds);
CountDownLatch latch = new CountDownLatch(versionedObjectIds.size());
versionedObjectIds.forEach((k, v) -> sendObserveCompositeRequest(lwM2MClient, v)); versionedObjectIds.forEach((k, v) -> sendObserveCompositeRequest(lwM2MClient, v));
boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS);
if (!completed) log.trace("[{}] Timeout occurred during COMPOSITE_BY_OBJECT observe init", lwM2MClient.getEndpoint());
} }
} }
} }
@ -562,23 +574,22 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
} }
} }
private void sendReadRequest(LwM2mClient lwM2MClient, String versionedId) { private void sendReadSingleRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback<ReadRequest, ReadResponse> callback) {
sendReadRequest(lwM2MClient, versionedId, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId));
}
private void sendReadRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback<ReadRequest, ReadResponse> callback) {
TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
defaultLwM2MDownlinkMsgHandler.sendReadRequest(lwM2MClient, request, callback); defaultLwM2MDownlinkMsgHandler.sendReadRequest(lwM2MClient, request, callback);
} }
private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId) { private void sendReadCompositeRequest(LwM2mClient lwM2MClient, String[] versionedIds) {
sendObserveRequest(lwM2MClient, versionedId, new TbLwM2MObserveCallback(this, logService, lwM2MClient, versionedId)); TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
var mainCallback = new TbLwM2MReadCompositeCallback(this, logService, lwM2MClient, versionedIds);
defaultLwM2MDownlinkMsgHandler.sendReadCompositeRequest(lwM2MClient, request, mainCallback );
} }
private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback<ObserveRequest, ObserveResponse> callback) { private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback<ObserveRequest, ObserveResponse> callback) {
TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
defaultLwM2MDownlinkMsgHandler.sendObserveRequest(lwM2MClient, request, callback); defaultLwM2MDownlinkMsgHandler.sendObserveRequest(lwM2MClient, request, callback);
} }
private void sendObserveCompositeRequest(LwM2mClient lwM2MClient, String[] versionedIds) { private void sendObserveCompositeRequest(LwM2mClient lwM2MClient, String[] versionedIds) {
TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
@ -853,7 +864,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient);
request.getObjectInstances().forEach(instance -> request.getObjectInstances().forEach(instance ->
instance.getResources().forEach((resId, lwM2mResource) ->{ instance.getResources().forEach((resId, lwM2mResource) ->{
this.updateResourcesValue(updateResource, lwM2mResource, versionId + "/" + resId, Mode.REPLACE, 0); String path = versionId.endsWith("/") ? versionId + resId : versionId + "/" + resId;
this.updateResourcesValue(updateResource, lwM2mResource, path, Mode.REPLACE, 0);
}) })
); );
clientContext.update(lwM2MClient); clientContext.update(lwM2MClient);

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java

@ -81,7 +81,8 @@ public class LwM2MTransportUtil {
public static final String LOG_LWM2M_INFO = "info"; public static final String LOG_LWM2M_INFO = "info";
public static final String LOG_LWM2M_ERROR = "error"; public static final String LOG_LWM2M_ERROR = "error";
public static final String LOG_LWM2M_WARN = "warn"; public static final String LOG_LWM2M_WARN = "warn";
public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0; public static final String REGISTRATION_TRIGGER_PARAMS_ID = "/1/0/8";
public static final String BOOTSTRAP_TRIGGER_PARAMS_ID = "/1/0/9";;
public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) { public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) {
String path = fromVersionedIdToObjectId(pathIdVer); String path = fromVersionedIdToObjectId(pathIdVer);

7
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java

@ -33,6 +33,7 @@ import java.util.Date;
import static org.eclipse.leshan.core.model.ResourceModel.Type.NONE; import static org.eclipse.leshan.core.model.ResourceModel.Type.NONE;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
import static org.eclipse.leshan.core.model.ResourceModel.Type.TIME;
@Slf4j @Slf4j
public class LwM2mValueConverterImpl implements LwM2mValueConverter { public class LwM2mValueConverterImpl implements LwM2mValueConverter {
@ -58,7 +59,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter {
currentType = OPAQUE; currentType = OPAQUE;
} }
if (currentType == expectedType || currentType == NONE) { if (currentType == expectedType || currentType == NONE || currentType == TIME) {
/** expected type */ /** expected type */
return value; return value;
} }
@ -135,7 +136,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter {
**/ **/
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.debug("Unable to convert string to date", e); log.debug("Unable to convert string to date", e);
throw new CodecException("Unable to convert string (%s) to date for resource %s", value, throw new CodecException("Unable to convert string (%s) to %s for resource %s", value, TIME.name(),
resourcePath); resourcePath);
} }
default: default:
@ -149,7 +150,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter {
case FLOAT: case FLOAT:
return String.valueOf(value); return String.valueOf(value);
case TIME: case TIME:
String DATE_FORMAT = "MMM d, yyyy HH:mm a"; String DATE_FORMAT = "yyyy-MM-dd[[ ]['T']HH:mm[:ss[.SSS]][ ][XXX][Z][z][VV][O]]";
Long timeValue; Long timeValue;
try { try {
timeValue = ((Date) value).getTime(); timeValue = ((Date) value).getTime();

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java

@ -46,9 +46,6 @@ import java.security.cert.X509Certificate;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/**
* Created by valerii.sosliuk on 11/6/16.
*/
@Slf4j @Slf4j
@Component("MqttSslHandlerProvider") @Component("MqttSslHandlerProvider")
@ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) @ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -133,9 +133,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic.parseTopic; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic.parseTopic;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.parseTopicPublish; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.parseTopicPublish;
/**
* @author Andrew Shvayka
*/
@Slf4j @Slf4j
public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>>, SessionMsgListener { public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>>, SessionMsgListener {

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java

@ -25,9 +25,6 @@ import io.netty.handler.ssl.SslHandler;
import org.thingsboard.server.transport.mqtt.limits.IpFilter; import org.thingsboard.server.transport.mqtt.limits.IpFilter;
import org.thingsboard.server.transport.mqtt.limits.ProxyIpFilter; import org.thingsboard.server.transport.mqtt.limits.ProxyIpFilter;
/**
* @author Andrew Shvayka
*/
public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> { public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> {
private final MqttTransportContext context; private final MqttTransportContext context;

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -35,9 +35,6 @@ import org.thingsboard.server.common.data.TbTransportService;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
/**
* @author Andrew Shvayka
*/
@Service("MqttTransportService") @Service("MqttTransportService")
@TbMqttTransportComponent @TbMqttTransportComponent
@Slf4j @Slf4j

6
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java

@ -45,10 +45,6 @@ import java.util.UUID;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT; import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT;
/**
* @author Andrew Shvayka
*/
@Component @Component
@Slf4j @Slf4j
public class JsonMqttAdaptor implements MqttTransportAdaptor { public class JsonMqttAdaptor implements MqttTransportAdaptor {
@ -122,7 +118,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException {
return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg); return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg);
} }
@Override @Override
public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) { public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) {
return Optional.of(createMqttPublishMsg(ctx, topic, JsonConverter.toJson(notificationMsg))); return Optional.of(createMqttPublishMsg(ctx, topic, JsonConverter.toJson(notificationMsg)));

4
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java

@ -41,9 +41,6 @@ import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionConte
import java.util.Optional; import java.util.Optional;
/**
* @author Andrew Shvayka
*/
public interface MqttTransportAdaptor { public interface MqttTransportAdaptor {
ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false);
@ -90,4 +87,5 @@ public interface MqttTransportAdaptor {
payload.writeBytes(payloadInBytes); payload.writeBytes(payloadInBytes);
return new MqttPublishMessage(mqttFixedHeader, header, payload); return new MqttPublishMessage(mqttFixedHeader, header, payload);
} }
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java

@ -48,7 +48,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx;
byte[] bytes = toBytes(inbound.payload()); byte[] bytes = toBytes(inbound.payload());
Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMessageDescriptor());
try { try {
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
} catch (Exception e) { } catch (Exception e) {
@ -228,4 +228,5 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
private int getRequestId(String topicName, String topic) { private int getRequestId(String topicName, String topic) {
return Integer.parseInt(topicName.substring(topic.length())); return Integer.parseInt(topicName.substring(topic.length()));
} }
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java

@ -33,9 +33,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
/**
* Created by ashvayka on 19.01.17.
*/
@ToString(callSuper = true) @ToString(callSuper = true)
@Slf4j @Slf4j
public abstract class AbstractGatewayDeviceSessionContext<T extends AbstractGatewaySessionHandler> extends MqttDeviceAwareSessionContext implements SessionMsgListener { public abstract class AbstractGatewayDeviceSessionContext<T extends AbstractGatewaySessionHandler> extends MqttDeviceAwareSessionContext implements SessionMsgListener {

11
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java

@ -96,9 +96,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConn
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName;
/**
* Created by ashvayka on 19.01.17.
*/
@Slf4j @Slf4j
public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDeviceSessionContext> { public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDeviceSessionContext> {
@ -116,6 +113,7 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
@Getter @Getter
protected final UUID sessionId; protected final UUID sessionId;
private final ConcurrentMap<String, Lock> deviceCreationLockMap; private final ConcurrentMap<String, Lock> deviceCreationLockMap;
@Getter
private final ConcurrentMap<String, T> devices; private final ConcurrentMap<String, T> devices;
private final ConcurrentMap<String, ListenableFuture<T>> deviceFutures; private final ConcurrentMap<String, ListenableFuture<T>> deviceFutures;
protected final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; protected final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
@ -821,11 +819,7 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
transportService.process(sessionInfo, postTelemetryMsg, pubAckCallback); transportService.process(sessionInfo, postTelemetryMsg, pubAckCallback);
} }
public ConcurrentMap<String, T> getDevices() { protected <T> TransportServiceCallback<Void> getAggregatePubAckCallback(
return this.devices;
}
protected <T>TransportServiceCallback<Void> getAggregatePubAckCallback(
final ChannelHandlerContext ctx, final ChannelHandlerContext ctx,
final int msgId, final int msgId,
final String deviceName, final String deviceName,
@ -915,4 +909,5 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
log.trace("Failed to send device disconnect to gateway session", e); log.trace("Failed to send device disconnect to gateway session", e);
} }
} }
} }

49
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java

@ -49,9 +49,6 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer; import java.util.function.Consumer;
/**
* @author Andrew Shvayka
*/
@Slf4j @Slf4j
public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
@ -84,13 +81,18 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
private volatile MqttTopicFilter attributesSubscribeTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); private volatile MqttTopicFilter attributesSubscribeTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter();
@Getter @Getter
private volatile TransportPayloadType payloadType = TransportPayloadType.JSON; private volatile TransportPayloadType payloadType = TransportPayloadType.JSON;
@Getter
private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor; private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor;
@Getter
private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor; private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor;
@Getter
private volatile Descriptors.Descriptor rpcResponseDynamicMessageDescriptor; private volatile Descriptors.Descriptor rpcResponseDynamicMessageDescriptor;
@Getter
private volatile DynamicMessage.Builder rpcRequestDynamicMessageBuilder; private volatile DynamicMessage.Builder rpcRequestDynamicMessageBuilder;
private volatile MqttTransportAdaptor adaptor; private volatile MqttTransportAdaptor adaptor;
private volatile boolean jsonPayloadFormatCompatibilityEnabled; private volatile boolean jsonPayloadFormatCompatibilityEnabled;
private volatile boolean useJsonPayloadFormatForDefaultDownlinkTopics; private volatile boolean useJsonPayloadFormatForDefaultDownlinkTopics;
@Getter
private volatile boolean sendAckOnValidationException; private volatile boolean sendAckOnValidationException;
@Getter @Getter
@ -131,26 +133,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
return payloadType.equals(TransportPayloadType.JSON); return payloadType.equals(TransportPayloadType.JSON);
} }
public boolean isSendAckOnValidationException() {
return sendAckOnValidationException;
}
public Descriptors.Descriptor getTelemetryDynamicMsgDescriptor() {
return telemetryDynamicMessageDescriptor;
}
public Descriptors.Descriptor getAttributesDynamicMessageDescriptor() {
return attributesDynamicMessageDescriptor;
}
public Descriptors.Descriptor getRpcResponseDynamicMessageDescriptor() {
return rpcResponseDynamicMessageDescriptor;
}
public DynamicMessage.Builder getRpcRequestDynamicMessageBuilder() {
return rpcRequestDynamicMessageBuilder;
}
@Override @Override
public void setDeviceProfile(DeviceProfile deviceProfile) { public void setDeviceProfile(DeviceProfile deviceProfile) {
super.setDeviceProfile(deviceProfile); super.setDeviceProfile(deviceProfile);
@ -166,8 +148,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
private void updateDeviceSessionConfiguration(DeviceProfile deviceProfile) { private void updateDeviceSessionConfiguration(DeviceProfile deviceProfile) {
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
if (transportConfiguration.getType().equals(DeviceTransportType.MQTT) && if (transportConfiguration.getType().equals(DeviceTransportType.MQTT) &&
transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { transportConfiguration instanceof MqttDeviceProfileTransportConfiguration mqttConfig) {
MqttDeviceProfileTransportConfiguration mqttConfig = (MqttDeviceProfileTransportConfiguration) transportConfiguration;
TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttConfig.getTransportPayloadTypeConfiguration(); TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttConfig.getTransportPayloadTypeConfiguration();
payloadType = transportPayloadTypeConfiguration.getTransportPayloadType(); payloadType = transportPayloadTypeConfiguration.getTransportPayloadType();
deviceProfileMqttTransportType = true; deviceProfileMqttTransportType = true;
@ -199,16 +180,12 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
} }
public MqttTransportAdaptor getAdaptor(TopicType topicType) { public MqttTransportAdaptor getAdaptor(TopicType topicType) {
switch (topicType) { return switch (topicType) {
case V2: case V2 -> getDefaultAdaptor();
return getDefaultAdaptor(); case V2_JSON -> context.getJsonMqttAdaptor();
case V2_JSON: case V2_PROTO -> context.getProtoMqttAdaptor();
return context.getJsonMqttAdaptor(); default -> useJsonPayloadFormatForDefaultDownlinkTopics ? context.getJsonMqttAdaptor() : getDefaultAdaptor();
case V2_PROTO: };
return context.getProtoMqttAdaptor();
default:
return useJsonPayloadFormatForDefaultDownlinkTopics ? context.getJsonMqttAdaptor() : getDefaultAdaptor();
}
} }
private MqttTransportAdaptor getDefaultAdaptor() { private MqttTransportAdaptor getDefaultAdaptor() {
@ -269,7 +246,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
} }
} }
public Collection<MqttMessage> getMsgQueueSnapshot(){ public Collection<MqttMessage> getMsgQueueSnapshot() {
return Collections.unmodifiableCollection(msgQueue); return Collections.unmodifiableCollection(msgQueue);
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java

@ -22,9 +22,6 @@ import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
/**
* Created by nickAS21 on 26.12.22
*/
@ToString(callSuper = true) @ToString(callSuper = true)
public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext<GatewaySessionHandler> { public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext<GatewaySessionHandler> {

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java

@ -28,9 +28,6 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
/**
* Created by nickAS21 on 26.12.22
*/
@Slf4j @Slf4j
public class GatewaySessionHandler extends AbstractGatewaySessionHandler<GatewayDeviceSessionContext> { public class GatewaySessionHandler extends AbstractGatewaySessionHandler<GatewayDeviceSessionContext> {

5
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java

@ -25,9 +25,6 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Created by ashvayka on 30.08.18.
*/
@ToString(callSuper = true) @ToString(callSuper = true)
public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext { public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext {
@ -47,7 +44,7 @@ public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionCo
.stream() .stream()
.filter(entry -> entry.getKey().matches(topic)) .filter(entry -> entry.getKey().matches(topic))
.map(Map.Entry::getValue) .map(Map.Entry::getValue)
.collect(Collectors.toList()); .toList();
if (!qosList.isEmpty()) { if (!qosList.isEmpty()) {
return MqttQoS.valueOf(qosList.get(0)); return MqttQoS.valueOf(qosList.get(0));
} else { } else {

12
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java

@ -15,26 +15,25 @@
*/ */
package org.thingsboard.server.transport.mqtt.session; package org.thingsboard.server.transport.mqtt.session;
import lombok.Getter;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class MqttTopicMatcher { public class MqttTopicMatcher {
@Getter
private final String topic; private final String topic;
private final Pattern topicRegex; private final Pattern topicRegex;
public MqttTopicMatcher(String topic) { public MqttTopicMatcher(String topic) {
if(topic == null){ if (topic == null) {
throw new NullPointerException("topic"); throw new NullPointerException("topic");
} }
this.topic = topic; this.topic = topic;
this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$");
} }
public String getTopic() { public boolean matches(String topic) {
return topic;
}
public boolean matches(String topic){
return this.topicRegex.matcher(topic).matches(); return this.topicRegex.matcher(topic).matches();
} }
@ -52,4 +51,5 @@ public class MqttTopicMatcher {
public int hashCode() { public int hashCode() {
return topic.hashCode(); return topic.hashCode();
} }
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java

@ -64,9 +64,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_STATE_REGEXP; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_STATE_REGEXP;
/**
* Created by nickAS21 on 12.12.22
*/
@Slf4j @Slf4j
@SpecVersion(spec = "sparkplug", version = "3.0.0") @SpecVersion(spec = "sparkplug", version = "3.0.0")
public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<SparkplugDeviceSessionContext> { public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<SparkplugDeviceSessionContext> {

2
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -160,7 +160,7 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
private Asset saveAsset(Asset asset, boolean doValidate, NameConflictStrategy nameConflictStrategy) { private Asset saveAsset(Asset asset, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing saveAsset [{}]", asset); log.trace("Executing saveAsset [{}]", asset);
Asset oldAsset = (asset.getId() != null) ? findAssetById(asset.getTenantId(), asset.getId()) : null; Asset oldAsset = (asset.getId() != null) ? assetDao.findById(asset.getTenantId(), asset.getId().getId()) : null;
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldAsset == null || !oldAsset.getName().equals(asset.getName()))) { if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldAsset == null || !oldAsset.getName().equals(asset.getName()))) {
uniquifyEntityName(asset, oldAsset, asset::setName, EntityType.ASSET, nameConflictStrategy); uniquifyEntityName(asset, oldAsset, asset::setName, EntityType.ASSET, nameConflictStrategy);
} }

2
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -156,7 +156,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
private Customer saveCustomer(Customer customer, boolean doValidate, NameConflictStrategy nameConflictStrategy) { private Customer saveCustomer(Customer customer, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing saveCustomer [{}]", customer); log.trace("Executing saveCustomer [{}]", customer);
Customer oldCustomer = (customer.getId() != null) ? findCustomerById(customer.getTenantId(), customer.getId()) : null; Customer oldCustomer = (customer.getId() != null) ? customerDao.findById(customer.getTenantId(), customer.getId().getId()) : null;
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldCustomer == null || !oldCustomer.getTitle().equals(customer.getTitle()))) { if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldCustomer == null || !oldCustomer.getTitle().equals(customer.getTitle()))) {
uniquifyEntityName(customer, oldCustomer, customer::setTitle, EntityType.CUSTOMER, nameConflictStrategy); uniquifyEntityName(customer, oldCustomer, customer::setTitle, EntityType.CUSTOMER, nameConflictStrategy);
} }

4
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java

@ -18,9 +18,9 @@ package org.thingsboard.server.dao.device;
import lombok.Data; import lombok.Data;
@Data @Data
class DeviceCredentialsEvictEvent { public class DeviceCredentialsEvictEvent {
private final String newCedentialsId; private final String newCredentialsId;
private final String oldCredentialsId; private final String oldCredentialsId;
} }

27
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -47,6 +47,8 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator;
import java.util.Objects;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString; import static org.thingsboard.server.dao.service.Validator.validateString;
@ -61,8 +63,8 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
@TransactionalEventListener(classes = DeviceCredentialsEvictEvent.class) @TransactionalEventListener(classes = DeviceCredentialsEvictEvent.class)
@Override @Override
public void handleEvictEvent(DeviceCredentialsEvictEvent event) { public void handleEvictEvent(DeviceCredentialsEvictEvent event) {
cache.evict(event.getNewCedentialsId()); cache.evict(event.getNewCredentialsId());
if (StringUtils.isNotEmpty(event.getOldCredentialsId()) && !event.getNewCedentialsId().equals(event.getOldCredentialsId())) { if (StringUtils.isNotEmpty(event.getOldCredentialsId()) && !event.getNewCredentialsId().equals(event.getOldCredentialsId())) {
cache.evict(event.getOldCredentialsId()); cache.evict(event.getOldCredentialsId());
} }
} }
@ -107,7 +109,7 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
try { try {
var value = deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials); var value = deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials);
publishEvictEvent(new DeviceCredentialsEvictEvent(value.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null)); publishEvictEvent(new DeviceCredentialsEvictEvent(value.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null));
if (oldDeviceCredentials != null) { if (oldDeviceCredentials != null && isCredentialsChanged(oldDeviceCredentials, value)) {
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entity(value).entityId(value.getDeviceId()).actionType(ActionType.CREDENTIALS_UPDATED).build()); eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entity(value).entityId(value.getDeviceId()).actionType(ActionType.CREDENTIALS_UPDATED).build());
} }
return value; return value;
@ -140,13 +142,11 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
@Override @Override
public JsonNode toCredentialsInfo(DeviceCredentials deviceCredentials) { public JsonNode toCredentialsInfo(DeviceCredentials deviceCredentials) {
switch (deviceCredentials.getCredentialsType()) { return switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN: case ACCESS_TOKEN -> JacksonUtil.valueToTree(deviceCredentials.getCredentialsId());
return JacksonUtil.valueToTree(deviceCredentials.getCredentialsId()); case X509_CERTIFICATE -> JacksonUtil.valueToTree(deviceCredentials.getCredentialsValue());
case X509_CERTIFICATE: default -> JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), JsonNode.class);
return JacksonUtil.valueToTree(deviceCredentials.getCredentialsValue()); };
}
return JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), JsonNode.class);
} }
private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) {
@ -407,4 +407,11 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
} }
} }
private boolean isCredentialsChanged(DeviceCredentials oldCredentials, DeviceCredentials newCredentials) {
return !Objects.equals(oldCredentials.getCredentialsId(), newCredentials.getCredentialsId())
|| oldCredentials.getCredentialsType() != newCredentials.getCredentialsType()
|| !Objects.equals(oldCredentials.getCredentialsValue(), newCredentials.getCredentialsValue())
|| !Objects.equals(oldCredentials.getDeviceId(), newCredentials.getDeviceId());
}
} }

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -229,7 +229,7 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
private Device saveDeviceWithoutCredentials(Device device, boolean doValidate, NameConflictStrategy nameConflictStrategy) { private Device saveDeviceWithoutCredentials(Device device, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing saveDevice [{}]", device); log.trace("Executing saveDevice [{}]", device);
Device oldDevice = (device.getId() != null) ? findDeviceById(device.getTenantId(), device.getId()) : null; Device oldDevice = (device.getId() != null) ? deviceDao.findById(device.getTenantId(), device.getId().getId()) : null;
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldDevice == null || !oldDevice.getName().equals(device.getName()))) { if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldDevice == null || !oldDevice.getName().equals(device.getName()))) {
uniquifyEntityName(device, oldDevice, device::setName, EntityType.DEVICE, nameConflictStrategy); uniquifyEntityName(device, oldDevice, device::setName, EntityType.DEVICE, nameConflictStrategy);
} }

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

Loading…
Cancel
Save