Browse Source

Merge pull request #14226 from irynamatveieva/feature/entity-alarm-rules

Merge with master
pull/14231/head
Viacheslav Klimov 11 months ago
committed by GitHub
parent
commit
75b95979c8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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. 14
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  4. 2
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  5. 47
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  6. 50
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  7. 41
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
  8. 30
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  10. 11
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java
  12. 13
      application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java
  13. 24
      application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java
  14. 22
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  15. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java
  16. 49
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  17. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java
  19. 8
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  20. 32
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  21. 22
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  22. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  23. 13
      application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java
  24. 28
      application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java
  25. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  26. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  27. 2
      application/src/main/resources/thingsboard.yml
  28. 49
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  29. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  30. 45
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  31. 23
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  32. 152
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java
  33. 74
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  34. 2
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  35. 43
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  36. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java
  37. 174
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  38. 72
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  39. 92
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java
  40. 92
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java
  41. 32
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java
  42. 37
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java
  43. 29
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java
  44. 37
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java
  45. 39
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java
  46. 29
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java
  47. 31
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java
  48. 50
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java
  49. 13
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java
  50. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java
  51. 8
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java
  52. 6
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java
  53. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java
  54. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  55. 112
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java
  56. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java
  57. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java
  58. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java
  59. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java
  60. 19
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java
  61. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java
  62. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java
  63. 3
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java
  64. 13
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  65. 4
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java
  66. 17
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java
  67. 326
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
  68. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java
  69. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  70. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  71. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java
  72. 54
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  73. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  74. 7
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java
  75. 51
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  76. 27
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java
  77. 1
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  78. 83
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  79. 20
      dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java
  80. 22
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java
  81. 6
      pom.xml
  82. 17
      ui-ngx/src/app/core/auth/auth.service.ts
  83. 10
      ui-ngx/src/app/core/guards/auth.guard.ts
  84. 70
      ui-ngx/src/app/core/http/device.service.ts
  85. 14
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html
  86. 56
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts
  87. 3
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.html
  88. 4
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  89. 10
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html
  90. 14
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts
  91. 5
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
  92. 5
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
  93. 1
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
  94. 1
      ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html
  95. 351
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html
  96. 7
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss
  97. 72
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts
  98. 13
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html
  99. 2
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts
  100. 11
      ui-ngx/src/app/modules/login/login-routing.module.ts

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

@ -472,21 +472,26 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArguments(ctx.getLinkedAndDynamicArgs(entityId), data); return mapToArguments(ctx.getLinkedAndDynamicArgs(entityId), 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;
@ -505,19 +510,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;
} }
@ -535,24 +542,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;
} }

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

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

@ -51,9 +51,9 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
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.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
@ -65,6 +65,7 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -79,9 +80,9 @@ public class CalculatedFieldCtx implements Closeable {
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 Map<ReferencedEntityKey, String> dynamicEntityArguments; private final Map<ReferencedEntityKey, Set<String>> dynamicEntityArguments;
private final List<String> argNames; private final List<String> argNames;
private Output output; private Output output;
private String expression; private String expression;
@ -138,14 +139,15 @@ public class CalculatedFieldCtx implements Closeable {
continue; continue;
} }
if (entry.getValue().hasOwnerSource()) { if (entry.getValue().hasOwnerSource()) {
dynamicEntityArguments.put(refKey, entry.getKey()); dynamicEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey()));
} else { } else {
mainEntityArguments.put(refKey, entry.getKey()); mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey()));
} }
} else if (refId.equals(calculatedField.getEntityId())) { } else if (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());
@ -346,7 +348,7 @@ public class CalculatedFieldCtx implements Closeable {
return matchesAttributes(dynamicEntityArguments, values, scope); return matchesAttributes(dynamicEntityArguments, values, scope);
} }
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;
} }
@ -360,7 +362,7 @@ public class CalculatedFieldCtx implements Closeable {
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;
} }
@ -397,7 +399,7 @@ public class CalculatedFieldCtx implements Closeable {
return matchesTimeSeriesKeys(dynamicEntityArguments, keys); return matchesTimeSeriesKeys(dynamicEntityArguments, 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;
} }
@ -412,7 +414,7 @@ public class CalculatedFieldCtx implements Closeable {
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;
} }
@ -481,8 +483,8 @@ public class CalculatedFieldCtx implements Closeable {
} }
} }
public Map<ReferencedEntityKey, String> getLinkedAndDynamicArgs(EntityId entityId) { public Map<ReferencedEntityKey, Set<String>> getLinkedAndDynamicArgs(EntityId entityId) {
var argNames = new HashMap<ReferencedEntityKey, String>(); var argNames = new HashMap<ReferencedEntityKey, Set<String>>();
var linkedArgNames = linkedEntityArguments.get(entityId); var linkedArgNames = linkedEntityArguments.get(entityId);
if (linkedArgNames != null && !linkedArgNames.isEmpty()) { if (linkedArgNames != null && !linkedArgNames.isEmpty()) {
argNames.putAll(linkedArgNames); argNames.putAll(linkedArgNames);

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;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1202,7 +1202,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

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

@ -1160,6 +1160,55 @@ 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

@ -148,6 +148,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;
@ -215,7 +216,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";
@ -608,8 +609,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.getToken()); SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.getToken());

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

@ -23,6 +23,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;
@ -109,4 +111,8 @@ 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);
} }

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;

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

@ -124,4 +124,17 @@ public class CollectionsUtil {
} }
} }
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());
}
} }

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

51
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java

@ -25,17 +25,13 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -50,9 +46,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
@Service @Service
@Slf4j @Slf4j
@ -115,49 +108,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
@Override @Override
public PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, PageLink pageLink) { public PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, PageLink pageLink) {
UsersFilter usersFilter = targetConfig.getUsersFilter(); UsersFilter usersFilter = targetConfig.getUsersFilter();
switch (usersFilter.getType()) { return userService.findUsersByFilter(tenantId, usersFilter, pageLink);
case USER_LIST: {
List<User> users = ((UserListFilter) usersFilter).getUsersIds().stream()
.limit(pageLink.getPageSize())
.map(UserId::new).map(userId -> userService.findUserById(tenantId, userId))
.filter(Objects::nonNull).collect(Collectors.toList());
return new PageData<>(users, 1, users.size(), false);
}
case CUSTOMER_USERS: {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
throw new IllegalArgumentException("Customer users target is not supported for system administrator");
}
CustomerUsersFilter filter = (CustomerUsersFilter) usersFilter;
return userService.findCustomerUsers(tenantId, new CustomerId(filter.getCustomerId()), pageLink);
}
case TENANT_ADMINISTRATORS: {
TenantAdministratorsFilter filter = (TenantAdministratorsFilter) usersFilter;
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return userService.findTenantAdmins(tenantId, pageLink);
} else {
if (isNotEmpty(filter.getTenantsIds())) {
return userService.findTenantAdminsByTenantsIds(filter.getTenantsIds().stream()
.map(TenantId::fromUUID).collect(Collectors.toList()), pageLink);
} else if (isNotEmpty(filter.getTenantProfilesIds())) {
return userService.findTenantAdminsByTenantProfilesIds(filter.getTenantProfilesIds().stream()
.map(TenantProfileId::new).collect(Collectors.toList()), pageLink);
} else {
return userService.findAllTenantAdmins(pageLink);
}
}
}
case SYSTEM_ADMINISTRATORS:
return userService.findSysAdmins(pageLink);
case ALL_USERS: {
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return userService.findUsersByTenantId(tenantId, pageLink);
} else {
return userService.findAllUsers(pageLink);
}
}
default:
throw new IllegalArgumentException("Recipient type not supported");
}
} }
@Override @Override

27
dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java

@ -69,6 +69,10 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
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.isNotLwm2mServer;
@Slf4j @Slf4j
@Component @Component
public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<DeviceProfile> { public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<DeviceProfile> {
@ -337,19 +341,22 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
throw new DeviceCredentialsValidationException("Bootstrap config must not include \"Bootstrap Server\". \"Include Bootstrap Server updates\" is " + isBootstrapServerUpdateEnable + "."); throw new DeviceCredentialsValidationException("Bootstrap config must not include \"Bootstrap Server\". \"Include Bootstrap Server updates\" is " + isBootstrapServerUpdateEnable + ".");
} }
if (serverConfig.getShortServerId() != null) { if (serverConfig.isBootstrapServerIs()){
if (serverConfig.isBootstrapServerIs()) { if (serverConfig.getShortServerId() != null) {
if (serverConfig.getShortServerId() < 0 || serverConfig.getShortServerId() > 65535) { if (serverConfig.getShortServerId() == 0) {
throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be in range [0 - 65535]!"); serverConfig.setShortServerId(null);
} } else {
} else { throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null!");
if (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534) {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [1 - 65534]!");
} }
} }
} else { } else {
String serverName = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; if (serverConfig.getShortServerId() != null) {
throw new DeviceCredentialsValidationException(serverName + " ShortServerId must not be null!"); if (isNotLwm2mServer(serverConfig.getShortServerId())) {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [" + PRIMARY_LWM2M_SERVER.getId() + " - " + LWM2M_SERVER_MAX.getId() + "]!");
}
} else {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must not be null!");
}
} }
String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server";

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

@ -75,6 +75,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Autowired @Autowired
private ApiUsageStateService apiUsageStateService; private ApiUsageStateService apiUsageStateService;
@Autowired @Autowired
@Lazy
private NotificationSettingsService notificationSettingsService; private NotificationSettingsService notificationSettingsService;
@Autowired @Autowired
private QrCodeSettingService qrCodeSettingService; private QrCodeSettingService qrCodeSettingService;

83
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -44,6 +44,11 @@ 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.mobile.UserMobileSessionInfo; import org.thingsboard.server.common.data.mobile.UserMobileSessionInfo;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
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.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -62,6 +67,7 @@ import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -71,7 +77,9 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
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.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -97,6 +105,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
private final UserSettingsService userSettingsService; private final UserSettingsService userSettingsService;
private final UserSettingsDao userSettingsDao; private final UserSettingsDao userSettingsDao;
private final SecuritySettingsService securitySettingsService; private final SecuritySettingsService securitySettingsService;
private final TbTenantProfileCache tenantProfileCache;
private final DataValidator<User> userValidator; private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator; private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
@ -496,6 +505,80 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
return userCredentialsDao.incrementFailedLoginAttempts(tenantId, userId); return userCredentialsDao.incrementFailedLoginAttempts(tenantId, userId);
} }
@Override
public PageData<User> findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink) {
switch (filter.getType()) {
case USER_LIST -> {
List<User> users = ((UserListFilter) filter).getUsersIds().stream()
.limit(pageLink.getPageSize())
.map(UserId::new).map(userId -> findUserById(tenantId, userId))
.filter(Objects::nonNull).collect(Collectors.toList());
return new PageData<>(users, 1, users.size(), false);
}
case CUSTOMER_USERS -> {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
throw new IllegalArgumentException("Customer users target is not supported for system administrator");
}
CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) filter;
return findCustomerUsers(tenantId, new CustomerId(customerUsersFilter.getCustomerId()), pageLink);
}
case TENANT_ADMINISTRATORS -> {
TenantAdministratorsFilter tenantAdministratorsFilter = (TenantAdministratorsFilter) filter;
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return findTenantAdmins(tenantId, pageLink);
} else {
if (isNotEmpty(tenantAdministratorsFilter.getTenantsIds())) {
return findTenantAdminsByTenantsIds(tenantAdministratorsFilter.getTenantsIds().stream()
.map(TenantId::fromUUID).collect(Collectors.toList()), pageLink);
} else if (isNotEmpty(tenantAdministratorsFilter.getTenantProfilesIds())) {
return findTenantAdminsByTenantProfilesIds(tenantAdministratorsFilter.getTenantProfilesIds().stream()
.map(TenantProfileId::new).collect(Collectors.toList()), pageLink);
} else {
return findAllTenantAdmins(pageLink);
}
}
}
case SYSTEM_ADMINISTRATORS -> {
return findSysAdmins(pageLink);
}
case ALL_USERS -> {
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return findUsersByTenantId(tenantId, pageLink);
} else {
return findAllUsers(pageLink);
}
}
default -> throw new IllegalArgumentException("Recipient type not supported");
}
}
@Override
public boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user) {
switch (filter.getType()) {
case TENANT_ADMINISTRATORS -> {
if (user.isSystemAdmin() || user.isCustomerUser()) {
return false;
}
TenantAdministratorsFilter tenantAdministratorsFilter = (TenantAdministratorsFilter) filter;
if (isNotEmpty(tenantAdministratorsFilter.getTenantsIds())) {
return tenantAdministratorsFilter.getTenantsIds().contains(user.getTenantId().getId());
} else if (isNotEmpty(tenantAdministratorsFilter.getTenantProfilesIds())) {
return tenantAdministratorsFilter.getTenantProfilesIds().contains(tenantProfileCache.get(user.getTenantId()).getUuidId());
} else {
return user.getAuthority() == Authority.TENANT_ADMIN;
}
}
case SYSTEM_ADMINISTRATORS -> {
return user.getAuthority() == Authority.SYS_ADMIN;
}
case ALL_USERS -> {
return true;
}
default -> throw new IllegalArgumentException("Recipient type not supported");
}
}
private void updatePasswordHistory(UserCredentials userCredentials) { private void updatePasswordHistory(UserCredentials userCredentials) {
JsonNode additionalInfo = userCredentials.getAdditionalInfo(); JsonNode additionalInfo = userCredentials.getAdditionalInfo();
if (!(additionalInfo instanceof ObjectNode)) { if (!(additionalInfo instanceof ObjectNode)) {

20
dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java

@ -48,6 +48,9 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
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.NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER;
@SpringBootTest(classes = DeviceProfileDataValidator.class) @SpringBootTest(classes = DeviceProfileDataValidator.class)
class DeviceProfileDataValidatorTest { class DeviceProfileDataValidatorTest {
@ -74,8 +77,8 @@ class DeviceProfileDataValidatorTest {
" \"clientOnlyObserveAfterConnect\": 1\n" + " \"clientOnlyObserveAfterConnect\": 1\n" +
" }"; " }";
private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [1 - 65534]!"; private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [" + PRIMARY_LWM2M_SERVER.getId() + " - " + LWM2M_SERVER_MAX.getId() + "]!";
private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be in range [0 - 65535]!"; private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be null!";
private static final String msgErrorNotNull = " Server ShortServerId must not be null!"; private static final String msgErrorNotNull = " Server ShortServerId must not be null!";
private static final String host = "localhost"; private static final String host = "localhost";
private static final String hostBs = "localhost"; private static final String hostBs = "localhost";
@ -124,7 +127,7 @@ class DeviceProfileDataValidatorTest {
@Test @Test
void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() { void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() {
Integer shortServerId = 123; Integer shortServerId = 123;
Integer shortServerIdBs = 0; Integer shortServerIdBs = null;
DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs); DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs);
validator.validateDataImpl(tenantId, deviceProfile); validator.validateDataImpl(tenantId, deviceProfile);
@ -132,8 +135,13 @@ class DeviceProfileDataValidatorTest {
} }
@Test @Test
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_null_Error() { void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_validate_0_to_null_Ok() {
verifyValidationError(123, null, "Bootstrap" + msgErrorNotNull); Integer shortServerId = 123;
Integer shortServerIdBs = 0;
DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs);
validator.validateDataImpl(tenantId, deviceProfile);
verify(validator).validateString("Device profile name", deviceProfile.getName());
} }
@Test @Test
@ -153,7 +161,7 @@ class DeviceProfileDataValidatorTest {
@Test @Test
void testValidateDeviceProfile_Lwm2mShortServerId_More_65534_Error_BootstrapShortServerId_Ok() { void testValidateDeviceProfile_Lwm2mShortServerId_More_65534_Error_BootstrapShortServerId_Ok() {
verifyValidationError(65535, 111, msgErrorLwm2mRange); verifyValidationError(NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId(), 111, msgErrorLwm2mRange);
} }
@Test @Test

22
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java

@ -21,6 +21,7 @@ import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@ -28,6 +29,8 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.msa.AbstractCoapClientTest; import org.thingsboard.server.msa.AbstractCoapClientTest;
import org.thingsboard.server.msa.DisableUIListeners; import org.thingsboard.server.msa.DisableUIListeners;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
@ -52,14 +55,27 @@ public class CoapClientTest extends AbstractCoapClientTest{
DeviceProfile deviceProfile = testRestClient.getDeviceProfileById(device.getDeviceProfileId()); DeviceProfile deviceProfile = testRestClient.getDeviceProfileById(device.getDeviceProfileId());
deviceProfile = updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); deviceProfile = updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES);
DeviceCredentials expectedDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); DeviceCredentials deviceCreds = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
JsonNode provisionResponse = JacksonUtil.fromBytes(createCoapClientAndPublish(device.getName())); JsonNode provisionResponse = JacksonUtil.fromBytes(createCoapClientAndPublish(device.getName()));
assertThat(provisionResponse.get("credentialsType").asText()).isEqualTo(expectedDeviceCredentials.getCredentialsType().name()); assertThat(provisionResponse.get("credentialsType").asText()).isEqualTo(deviceCreds.getCredentialsType().name());
assertThat(provisionResponse.get("credentialsValue").asText()).isEqualTo(expectedDeviceCredentials.getCredentialsId()); assertThat(provisionResponse.get("credentialsValue").asText()).isEqualTo(deviceCreds.getCredentialsId());
assertThat(provisionResponse.get("status").asText()).isEqualTo("SUCCESS"); assertThat(provisionResponse.get("status").asText()).isEqualTo("SUCCESS");
JsonNode attributes = testRestClient.getAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "provisionState");
assertThat(attributes.get(0).get("value").asText()).isEqualTo("provisioned");
// provision second time should fail
JsonNode provisionResponse2 = JacksonUtil.fromBytes(createCoapClientAndPublish(device.getName()));
assertThat(provisionResponse2.get("status").asText()).isEqualTo("FAILURE");
// update provision attribute to non-valid value
testRestClient.postTelemetryAttribute(device.getId(), AttributeScope.SERVER_SCOPE, JacksonUtil.valueToTree(Map.of("provisionState", "non-valid")));
JsonNode provisionResponse3 = JacksonUtil.fromBytes(createCoapClientAndPublish(device.getName()));
assertThat(provisionResponse3.get("status").asText()).isEqualTo("FAILURE");
updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.DISABLED); updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.DISABLED);
} }

6
pom.xml

@ -38,7 +38,7 @@
<pkg.implementationTitle>${project.name}</pkg.implementationTitle> <pkg.implementationTitle>${project.name}</pkg.implementationTitle>
<pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder> <pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<spring-boot.version>3.4.8</spring-boot.version> <spring-boot.version>3.4.10</spring-boot.version>
<javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version> <javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version>
<jedis.version>5.1.5</jedis.version> <jedis.version>5.1.5</jedis.version>
<jjwt.version>0.12.5</jjwt.version> <jjwt.version>0.12.5</jjwt.version>
@ -129,7 +129,7 @@
<testcontainers.version>1.20.6</testcontainers.version> <testcontainers.version>1.20.6</testcontainers.version>
<testcontainers-junit4-mock.version>1.0.2</testcontainers-junit4-mock.version> <testcontainers-junit4-mock.version>1.0.2</testcontainers-junit4-mock.version>
<zeroturnaround.version>1.12</zeroturnaround.version> <zeroturnaround.version>1.12</zeroturnaround.version>
<webdrivermanager.version>5.8.0</webdrivermanager.version> <webdrivermanager.version>6.1.0</webdrivermanager.version>
<allure-testng.version>2.27.0</allure-testng.version> <allure-testng.version>2.27.0</allure-testng.version>
<allure-maven.version>2.12.0</allure-maven.version> <allure-maven.version>2.12.0</allure-maven.version>
@ -146,7 +146,7 @@
<firebase-admin.version>9.2.0</firebase-admin.version> <firebase-admin.version>9.2.0</firebase-admin.version>
<snappy.version>1.1.10.5</snappy.version> <snappy.version>1.1.10.5</snappy.version>
<rocksdbjni.version>9.10.0</rocksdbjni.version> <rocksdbjni.version>9.10.0</rocksdbjni.version>
<netty.version>4.1.124.Final</netty.version> <netty.version>4.1.125.Final</netty.version> <!-- to fix CVEs. TODO: remove when fixed in spring-boot-dependencies -->
</properties> </properties>
<modules> <modules>

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

@ -68,6 +68,7 @@ export class AuthService {
redirectUrl: string; redirectUrl: string;
oauth2Clients: Array<OAuth2ClientLoginInfo> = null; oauth2Clients: Array<OAuth2ClientLoginInfo> = null;
twoFactorAuthProviders: Array<TwoFaProviderInfo> = null; twoFactorAuthProviders: Array<TwoFaProviderInfo> = null;
forceTwoFactorAuthProviders: Array<TwoFactorAuthProviderType> = null;
private refreshTokenSubject: ReplaySubject<LoginResponse> = null; private refreshTokenSubject: ReplaySubject<LoginResponse> = null;
private jwtHelper = new JwtHelperService(); private jwtHelper = new JwtHelperService();
@ -117,6 +118,9 @@ export class AuthService {
if (loginResponse.scope === Authority.PRE_VERIFICATION_TOKEN) { if (loginResponse.scope === Authority.PRE_VERIFICATION_TOKEN) {
this.router.navigateByUrl(`login/mfa`); this.router.navigateByUrl(`login/mfa`);
} }
if (loginResponse.scope === Authority.MFA_CONFIGURATION_TOKEN) {
this.router.navigateByUrl(`login/force-mfa`);
}
} }
)); ));
} }
@ -239,6 +243,15 @@ export class AuthService {
); );
} }
public getAvailableTwoFaProviders(): Observable<Array<TwoFaProviderInfo>> {
return this.http.get<Array<TwoFaProviderInfo>>(`/api/2fa/providers`, defaultHttpOptions()).pipe(
catchError(() => of([])),
tap((providers) => {
this.forceTwoFactorAuthProviders = providers;
})
);
}
public forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean { public forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean {
if (authState && authState.authUser) { if (authState && authState.authUser) {
if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) { if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) {
@ -266,6 +279,8 @@ export class AuthService {
if (isAuthenticated) { if (isAuthenticated) {
if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {
result = this.router.parseUrl('login/mfa'); result = this.router.parseUrl('login/mfa');
} else if (authState.authUser.authority === Authority.MFA_CONFIGURATION_TOKEN) {
result = this.router.parseUrl('login/force-mfa');
} else if (!path || path === 'login' || this.forceDefaultPlace(authState, path, params)) { } else if (!path || path === 'login' || this.forceDefaultPlace(authState, path, params)) {
if (this.redirectUrl) { if (this.redirectUrl) {
const redirectUrl = this.redirectUrl; const redirectUrl = this.redirectUrl;
@ -399,7 +414,7 @@ export class AuthService {
loadUserSubject.error(err); loadUserSubject.error(err);
} }
); );
} else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN) { } else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN || authPayload.authUser?.authority === Authority.MFA_CONFIGURATION_TOKEN) {
loadUserSubject.next(authPayload); loadUserSubject.next(authPayload);
loadUserSubject.complete(); loadUserSubject.complete();
} else if (authPayload.authUser?.userId) { } else if (authPayload.authUser?.userId) {

10
ui-ngx/src/app/core/guards/auth.guard.ts

@ -104,6 +104,16 @@ export class AuthGuard {
} }
this.authService.logout(); this.authService.logout();
return of(this.authService.defaultUrl(false)); return of(this.authService.defaultUrl(false));
} else if (path === 'login.force-mfa') {
if (authState.authUser?.authority === Authority.MFA_CONFIGURATION_TOKEN) {
return this.authService.getAvailableTwoFaProviders().pipe(
map(() => {
return true;
})
);
}
this.authService.logout();
return of(this.authService.defaultUrl(false));
} else { } else {
return of(true); return of(true);
} }

70
ui-ngx/src/app/core/http/device.service.ts

@ -16,7 +16,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable, ReplaySubject } from 'rxjs'; import { catchError, Observable, of, ReplaySubject, throwError, timeout } from 'rxjs';
import { map, switchMap } from "rxjs/operators";
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
@ -225,4 +226,71 @@ export class DeviceService {
public downloadGatewayDockerComposeFile(deviceId: string): Observable<any> { public downloadGatewayDockerComposeFile(deviceId: string): Observable<any> {
return this.resourcesService.downloadResource(`/api/device-connectivity/gateway-launch/${deviceId}/docker-compose/download`); return this.resourcesService.downloadResource(`/api/device-connectivity/gateway-launch/${deviceId}/docker-compose/download`);
} }
public rebootDevice(deviceId: string, isBootstrapServer: boolean, config?: RequestConfig): Observable<{
result: string,
msg: string
}> {
const rebootName = isBootstrapServer ? 'Bootstrap-Request Trigger' : 'Registration Update Trigger';
return this.sendTwoWayRpcCommand(deviceId, {method: 'DiscoverAll'}, config).pipe(
timeout(10000),
switchMap((response: any) => {
if (response.result && response.result.toUpperCase() === 'CONTENT') {
const resourceId = isBootstrapServer ? 9 : 8;
const resourcePath = `/1/0/${resourceId}`;
return this.rebootTrigger(deviceId, resourcePath, config).pipe(
map((responseReboot: any) => {
if (responseReboot.result === 'CHANGED') {
return {
result: 'SUCCESS',
msg: `<b>"${rebootName}"</b> - Started Successfully.`
};
} else {
return {
result: 'ERROR',
msg: `<b>"${rebootName}"</b> failed:<pre>${JSON.stringify(responseReboot, null, 2)}</pre>`
}
}
}),
catchError(err =>
of({
result: 'ERROR',
msg: `<b>"${rebootName}"</b> failed.<br>Error: ${err.message || err}`
})
)
);
} else {
return of({
result: 'ERROR',
msg: `<b>"${rebootName}"</b> failed.<br>Bad registration device with id = ${deviceId}.<br><b>"DiscoverAll"</b> - RPC result is not "CONTENT"`
});
}
}),
catchError(err =>
of({
result: 'ERROR',
msg: `<b>"${rebootName}"</b> failed.<br>Bad registration device with id = ${deviceId}.<br>Error: ${err.message || err}`
})
)
);
}
private rebootTrigger(deviceId: string, resourcePath: string, config?: RequestConfig): Observable<{ result: string, msg?: string }> {
return this.sendTwoWayRpcCommand(deviceId, {method: 'Execute', params: {id: resourcePath}}, config).pipe(
timeout(10000),
map(res => {
if (res?.result?.toUpperCase() === 'CHANGED') {
return {result: 'CHANGED'};
} else {
return {
result: `${res?.result}`,
msg: `${res?.error}`
}
}
}),
catchError(err => {
return throwError(() => err);
})
);
}
} }

14
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<mat-tab-group [formGroup]="lwm2mConfigFormGroup"> <mat-tab-group [formGroup]="lwm2mConfigFormGroup" dynamicHeight>
<mat-tab label="{{ 'device.lwm2m-security-config.client-tab' | translate }}"> <mat-tab label="{{ 'device.lwm2m-security-config.client-tab' | translate }}">
<ng-container formGroupName="client"> <ng-container formGroupName="client">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
@ -72,6 +72,12 @@
</textarea> </textarea>
<mat-hint translate>device.lwm2m-security-config.client-public-key-hint</mat-hint> <mat-hint translate>device.lwm2m-security-config.client-public-key-hint</mat-hint>
</mat-form-field> </mat-form-field>
<button *ngIf="deviceId"
mat-raised-button color="primary" type="button"
(click)="rebootDevice(false)"
[disabled]="lwm2mConfigFormGroup.get('client').invalid">
{{ 'device.lwm2m-security-config.client-reboot' | translate }}
</button>
</ng-container> </ng-container>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'device.lwm2m-security-config.bootstrap-tab' | translate }}"> <mat-tab label="{{ 'device.lwm2m-security-config.bootstrap-tab' | translate }}">
@ -103,5 +109,11 @@
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>
</div> </div>
<button *ngIf="deviceId"
mat-raised-button color="primary" style="margin-top: 22px" type="button"
(click)="rebootDevice(true)"
[disabled]="lwm2mConfigFormGroup.get('client').invalid">
{{ 'device.lwm2m-security-config.bootstrap-reboot' | translate }}
</button>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>

56
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts

@ -14,20 +14,21 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, OnDestroy } from '@angular/core'; import { Component, forwardRef, Input, OnDestroy } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
UntypedFormBuilder,
UntypedFormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors, ValidationErrors,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { import {
getDefaultClientSecurityConfig, getDefaultClientSecurityConfig,
getDefaultServerSecurityConfig, Lwm2mClientKeyTooltipTranslationsMap, getDefaultServerSecurityConfig,
Lwm2mClientKeyTooltipTranslationsMap,
Lwm2mSecurityConfigModels, Lwm2mSecurityConfigModels,
Lwm2mSecurityType, Lwm2mSecurityType,
Lwm2mSecurityTypeTranslationMap Lwm2mSecurityTypeTranslationMap
@ -35,6 +36,11 @@ import {
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { DeviceId } from "@shared/models/id/device-id";
import { DeviceService } from "@core/http/device.service";
import { ActionNotificationShow } from "@core/notification/notification.actions";
import { Store } from "@ngrx/store";
import { AppState } from "@core/core.state";
@Component({ @Component({
selector: 'tb-device-credentials-lwm2m', selector: 'tb-device-credentials-lwm2m',
@ -65,7 +71,12 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {}; private propagateChange = (v: any) => {};
constructor(private fb: UntypedFormBuilder) { @Input()
deviceId: DeviceId;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,
private deviceService: DeviceService) {
this.lwm2mConfigFormGroup = this.initLwm2mConfigForm(); this.lwm2mConfigFormGroup = this.initLwm2mConfigForm();
} }
@ -101,6 +112,41 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
this.destroy$.complete(); this.destroy$.complete();
} }
/**
* AbstractRpcController -> rpcController
* - API
* "/api/plugins/rpc/twoway/${this.deviceId.id}"
* - DiscoveryAll
* requestBody = "{\"method\":\"DiscoverAll\"}";
* - "Registration Update Trigger",
* requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1/0/8\"}}
* - "Bootstrap-Request Trigger"
* requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1/0/9\"}}
*/
public rebootDevice(isBootstrapServer: boolean): void {
this.deviceService.rebootDevice(this.deviceId.id, isBootstrapServer).subscribe(responseReboot => {
if (responseReboot.result === 'SUCCESS') {
this.store.dispatch(new ActionNotificationShow(
{
message: responseReboot.msg,
type: 'success',
duration: 1500,
verticalPosition: 'top',
horizontalPosition: 'left'
}));
} else {
this.store.dispatch(new ActionNotificationShow(
{
message: responseReboot.msg,
type: 'error',
verticalPosition: 'top',
horizontalPosition: 'left'
}));
}
});
}
private initClientSecurityConfig(config: Lwm2mSecurityConfigModels): void { private initClientSecurityConfig(config: Lwm2mSecurityConfigModels): void {
this.lwm2mConfigFormGroup.patchValue(config, {emitEvent: false}); this.lwm2mConfigFormGroup.patchValue(config, {emitEvent: false});
this.securityConfigClientUpdateValidators(config.client.securityConfigClientMode); this.securityConfigClientUpdateValidators(config.client.securityConfigClientMode);

3
ui-ngx/src/app/modules/home/components/device/device-credentials.component.html

@ -81,7 +81,8 @@
</tb-device-credentials-mqtt-basic> </tb-device-credentials-mqtt-basic>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="deviceCredentialsType.LWM2M_CREDENTIALS"> <ng-template [ngSwitchCase]="deviceCredentialsType.LWM2M_CREDENTIALS">
<tb-device-credentials-lwm2m formControlName="credentialsValue"> <tb-device-credentials-lwm2m formControlName="credentialsValue"
[deviceId]="deviceId">
</tb-device-credentials-lwm2m> </tb-device-credentials-lwm2m>
</ng-template> </ng-template>
</div> </div>

4
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -36,6 +36,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { generateSecret, isDefinedAndNotNull } from '@core/utils'; import { generateSecret, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { DeviceId } from "@shared/models/id/device-id";
@Component({ @Component({
selector: 'tb-device-credentials', selector: 'tb-device-credentials',
@ -88,6 +89,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
credentialTypeNamesMap = credentialTypeNames; credentialTypeNamesMap = credentialTypeNames;
deviceId: DeviceId;
private propagateChange = null; private propagateChange = null;
private propagateChangePending = false; private propagateChangePending = false;
@ -126,6 +129,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
writeValue(value: DeviceCredentials | null): void { writeValue(value: DeviceCredentials | null): void {
if (isDefinedAndNotNull(value)) { if (isDefinedAndNotNull(value)) {
this.deviceId = value.deviceId;
const credentialsType = this.credentialsTypes.includes(value.credentialsType) ? value.credentialsType : this.credentialsTypes[0]; const credentialsType = this.credentialsTypes.includes(value.credentialsType) ? value.credentialsType : this.credentialsTypes[0];
this.deviceCredentialsFormGroup.patchValue({ this.deviceCredentialsFormGroup.patchValue({
credentialsType, credentialsType,

10
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html

@ -24,7 +24,7 @@
'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}</div> 'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}</div>
<div *ngIf="!serverPanel.expanded" style="font-size:14px" class="no-wrap flex flex-row"> <div *ngIf="!serverPanel.expanded" style="font-size:14px" class="no-wrap flex flex-row">
<div style="margin-left:32px">{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }} <div style="margin-left:32px">{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }}
<span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value }}</span> <span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value ? serverFormGroup.get('shortServerId').value : '' }}</span>
</div> </div>
<div style="margin-left:32px">{{ ('device-profile.lwm2m.mode' | translate) + ': ' }} <div style="margin-left:32px">{{ ('device-profile.lwm2m.mode' | translate) + ': ' }}
<span style="font-style: italic">{{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[serverFormGroup.get('securityMode').value]) }}</span> <span style="font-style: italic">{{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[serverFormGroup.get('securityMode').value]) }}</span>
@ -54,16 +54,18 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-1"> <mat-form-field class="flex-1" *ngIf="!isBootstrap">
<mat-label>{{ 'device-profile.lwm2m.short-id' | translate }}</mat-label> <mat-label>{{ 'device-profile.lwm2m.short-id' | translate }}</mat-label>
<mat-icon *ngIf="!disabled" class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;" <mat-icon *ngIf="!disabled" class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{ (isBootstrap ? 'device-profile.lwm2m.short-id-tooltip-bootstrap': 'device-profile.lwm2m.short-id-tooltip') | translate }}">help</mat-icon> matTooltip="{{ (isBootstrap ? 'device-profile.lwm2m.short-id-tooltip-bootstrap': 'device-profile.lwm2m.short-id-tooltip') | translate }}">help</mat-icon>
<input matInput type="number" [min]="shortServerIdMin" [max]="shortServerIdMax" formControlName="shortServerId" required> <input matInput type="number" formControlName="shortServerId" required
[min]="isBootstrap ? undefined : shortServerIdMin"
[max]="isBootstrap ? undefined : shortServerIdMax">
<mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('required')"> <mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('required')">
{{ 'device-profile.lwm2m.short-id-required' | translate }} {{ 'device-profile.lwm2m.short-id-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('pattern')"> <mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('pattern')">
{{ 'device-profile.lwm2m.short-id-pattern' | translate }} {{ (isBootstrap ? 'device-profile.lwm2m.short-id-pattern-bs' : 'device-profile.lwm2m.short-id-pattern') | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('min') || <mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('min') ||
serverFormGroup.get('shortServerId').hasError('max')"> serverFormGroup.get('shortServerId').hasError('max')">

14
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts

@ -74,8 +74,8 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
currentSecurityMode = null; currentSecurityMode = null;
bootstrapDisabled = false; bootstrapDisabled = false;
shortServerIdMin = 1; readonly shortServerIdMin = 1;
shortServerIdMax = 65534; readonly shortServerIdMax = 65534;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
@ -94,18 +94,15 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
} }
ngOnInit(): void { ngOnInit(): void {
if (this.isBootstrap) {
this.shortServerIdMin = 0;
this.shortServerIdMax = 65535;
}
this.serverFormGroup = this.fb.group({ this.serverFormGroup = this.fb.group({
host: ['', Validators.required], host: ['', Validators.required],
port: ['', [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern('[0-9]*')]], port: ['', [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern('[0-9]*')]],
securityMode: [Lwm2mSecurityType.NO_SEC], securityMode: [Lwm2mSecurityType.NO_SEC],
serverPublicKey: [''], serverPublicKey: [''],
clientHoldOffTime: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], clientHoldOffTime: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
shortServerId: ['', shortServerId: ['', this.isBootstrap ?
[Validators.required, Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax), Validators.pattern('[0-9]*')]], [] : [Validators.required, Validators.pattern('[0-9]*'), Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax)]
],
bootstrapServerAccountTimeout: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], bootstrapServerAccountTimeout: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
binding: [''], binding: [''],
lifetime: [null, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], lifetime: [null, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
@ -129,6 +126,7 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
this.serverFormGroup.get('serverPublicKey').patchValue(serverSecurityConfig.serverCertificate, {emitEvent: false}); this.serverFormGroup.get('serverPublicKey').patchValue(serverSecurityConfig.serverCertificate, {emitEvent: false});
} }
}); });
this.serverFormGroup.valueChanges.pipe( this.serverFormGroup.valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe(value => { ).subscribe(value => {

5
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html

@ -25,6 +25,11 @@
(removeList)="removeObjectsList($event)" (removeList)="removeObjectsList($event)"
formControlName="objectIds"> formControlName="objectIds">
</tb-profile-lwm2m-object-list> </tb-profile-lwm2m-object-list>
<mat-checkbox formControlName="initAttrTelAsObsStrategy">
<span tb-hint-tooltip-icon="{{ 'device-profile.lwm2m.init-attr-tel-as-obs-strategy-hint' | translate }}">
{{ 'device-profile.lwm2m.init-attr-tel-as-obs-strategy' | translate }}
</span>
</mat-checkbox>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>device-profile.lwm2m.observe-strategy.observe-strategy</mat-label> <mat-label translate>device-profile.lwm2m.observe-strategy.observe-strategy</mat-label>
<mat-select formControlName="observeStrategy"> <mat-select formControlName="observeStrategy">

5
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts

@ -108,6 +108,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
bootstrapServerUpdateEnable: [false], bootstrapServerUpdateEnable: [false],
bootstrap: [[]], bootstrap: [[]],
observeStrategy: [null, []], observeStrategy: [null, []],
initAttrTelAsObsStrategy: [false],
clientLwM2mSettings: this.fb.group({ clientLwM2mSettings: this.fb.group({
clientOnlyObserveAfterConnect: [1, []], clientOnlyObserveAfterConnect: [1, []],
useObject19ForOtaInfo: [false], useObject19ForOtaInfo: [false],
@ -183,6 +184,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe(value => this.updateObserveStrategy(value)); ).subscribe(value => this.updateObserveStrategy(value));
this.lwm2mDeviceProfileFormGroup.valueChanges.pipe( this.lwm2mDeviceProfileFormGroup.valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe((value) => { ).subscribe((value) => {
@ -272,6 +274,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
bootstrap: this.configurationValue.bootstrap, bootstrap: this.configurationValue.bootstrap,
bootstrapServerUpdateEnable: this.configurationValue.bootstrapServerUpdateEnable || false, bootstrapServerUpdateEnable: this.configurationValue.bootstrapServerUpdateEnable || false,
observeStrategy: this.configurationValue.observeAttr.observeStrategy || ObserveStrategy.SINGLE, observeStrategy: this.configurationValue.observeAttr.observeStrategy || ObserveStrategy.SINGLE,
initAttrTelAsObsStrategy: this.configurationValue.observeAttr.initAttrTelAsObsStrategy ?? false,
clientLwM2mSettings: { clientLwM2mSettings: {
clientOnlyObserveAfterConnect: this.configurationValue.clientLwM2mSettings.clientOnlyObserveAfterConnect, clientOnlyObserveAfterConnect: this.configurationValue.clientLwM2mSettings.clientOnlyObserveAfterConnect,
useObject19ForOtaInfo: this.configurationValue.clientLwM2mSettings.useObject19ForOtaInfo ?? false, useObject19ForOtaInfo: this.configurationValue.clientLwM2mSettings.useObject19ForOtaInfo ?? false,
@ -440,6 +443,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
const attributes: any = {}; const attributes: any = {};
const keyNameNew = {}; const keyNameNew = {};
const observeStrategyValue = this.lwm2mDeviceProfileFormGroup.get('observeStrategy').value; const observeStrategyValue = this.lwm2mDeviceProfileFormGroup.get('observeStrategy').value;
const initAttrTelAsObsStrategyValue = this.lwm2mDeviceProfileFormGroup.get('initAttrTelAsObsStrategy').value;
const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val)); const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val));
observeJson.forEach(obj => { observeJson.forEach(obj => {
if (isDefinedAndNotNull(obj.attributes) && !isEmpty(obj.attributes)) { if (isDefinedAndNotNull(obj.attributes) && !isEmpty(obj.attributes)) {
@ -481,6 +485,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
telemetry: telemetryArray, telemetry: telemetryArray,
keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew), keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew),
attributeLwm2m: attributes, attributeLwm2m: attributes,
initAttrTelAsObsStrategy: initAttrTelAsObsStrategyValue,
observeStrategy: observeStrategyValue observeStrategy: observeStrategyValue
}; };
} }

1
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts

@ -223,6 +223,7 @@ export interface ObservableAttributes {
keyName: {}; keyName: {};
attributeLwm2m: AttributesNameValueMap; attributeLwm2m: AttributesNameValueMap;
observeStrategy: ObserveStrategy; observeStrategy: ObserveStrategy;
initAttrTelAsObsStrategy?: boolean;
} }
export function getDefaultProfileObserveAttrConfig(): ObservableAttributes { export function getDefaultProfileObserveAttrConfig(): ObservableAttributes {

1
ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html

@ -72,6 +72,7 @@
</mat-form-field> </mat-form-field>
<tb-entity-list <tb-entity-list
class="flex-1" class="flex-1"
syncIdsWithDB
allowCreateNew allowCreateNew
placeholderText="{{ 'rule-node-config.ai.ai-resources' | translate }}" placeholderText="{{ 'rule-node-config.ai.ai-resources' | translate }}"
[inlineField]="true" [inlineField]="true"

351
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html

@ -30,163 +30,216 @@
<mat-card-content> <mat-card-content>
<form [formGroup]="twoFaFormGroup" (ngSubmit)="save()"> <form [formGroup]="twoFaFormGroup" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<div> <div class="tb-form-panel no-padding no-border">
<fieldset class="fields-group" formArrayName="providers"> <div class="tb-form-panel stroked tb-slide-toggle">
<legend class="group-title" translate>admin.2fa.available-providers</legend> <mat-expansion-panel class="tb-settings no-padding-bottom">
<ng-container *ngFor="let provider of providersForm.controls; let i = index; let $last = last; trackBy: trackByElement">
<mat-expansion-panel class="provider" [formGroupName]="i">
<mat-expansion-panel-header>
<mat-panel-title class="flex items-center justify-start">
<mat-slide-toggle
(mousedown)="toggleExtensionPanel($event, i, provider.get('enable').value)"
formControlName="enable">
{{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<ng-container [ngSwitch]="provider.get('providerType').value">
<ng-container *ngSwitchCase="twoFactorAuthProviderType.TOTP">
<mat-form-field class="mat-block flex-1">
<mat-label translate>admin.2fa.issuer-name</mat-label>
<input matInput formControlName="issuerName" required>
<mat-error *ngIf="provider.get('issuerName').hasError('required') ||
provider.get('issuerName').hasError('pattern')">
{{ "admin.2fa.issuer-name-required" | translate }}
</mat-error>
</mat-form-field>
</ng-container>
<div *ngSwitchCase="twoFactorAuthProviderType.SMS"
class="flex flex-row xs:flex-col gt-xs:gap-2">
<mat-form-field class="mat-block flex-1">
<mat-label translate>admin.2fa.verification-message-template</mat-label>
<input matInput formControlName="smsVerificationMessageTemplate" required>
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('required')">
{{ "admin.2fa.verification-message-template-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('pattern')">
{{ "admin.2fa.verification-message-template-pattern" | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block flex-1">
<mat-label translate>admin.2fa.verification-code-lifetime</mat-label>
<input matInput formControlName="verificationCodeLifetime" type="number" step="1" min="1" required>
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('required')">
{{ "admin.2fa.verification-code-lifetime-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('min') ||
provider.get('verificationCodeLifetime').hasError('pattern')">
{{ "admin.2fa.verification-code-lifetime-pattern" | translate }}
</mat-error>
</mat-form-field>
</div>
<div *ngSwitchCase="twoFactorAuthProviderType.EMAIL">
<mat-form-field class="mat-block flex-1">
<mat-label translate>admin.2fa.verification-code-lifetime</mat-label>
<input matInput formControlName="verificationCodeLifetime" type="number" step="1" min="1" required>
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('required')">
{{ "admin.2fa.verification-code-lifetime-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('min') ||
provider.get('verificationCodeLifetime').hasError('pattern')">
{{ "admin.2fa.verification-code-lifetime-pattern" | translate }}
</mat-error>
</mat-form-field>
</div>
<div *ngSwitchCase="twoFactorAuthProviderType.BACKUP_CODE">
<mat-form-field class="mat-block flex-1">
<mat-label translate>admin.2fa.number-of-codes</mat-label>
<input matInput formControlName="codesQuantity" type="number" step="1" min="1" required>
<mat-error *ngIf="provider.get('codesQuantity').hasError('required')">
{{ "admin.2fa.number-of-codes-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('codesQuantity').hasError('min') ||
provider.get('codesQuantity').hasError('pattern')">
{{ "admin.2fa.number-of-codes-pattern" | translate }}
</mat-error>
</mat-form-field>
</div>
</ng-container>
</ng-template>
</mat-expansion-panel>
<mat-divider *ngIf="!$last"></mat-divider>
</ng-container>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>admin.2fa.verification-limitations</legend>
<div class="input-row flex flex-row xs:flex-col gt-xs:gap-2">
<mat-form-field class="mat-block flex-1">
<mat-label translate>admin.2fa.total-allowed-time-for-verification</mat-label>
<input matInput required formControlName="totalAllowedTimeForVerification" type="number" step="1" min="60">
<mat-error *ngIf="twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('required')">
{{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('pattern')
|| twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('min')">
{{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block flex-1">
<mat-label translate>admin.2fa.retry-verification-code-period</mat-label>
<input matInput required formControlName="minVerificationCodeSendPeriod" type="number" step="1" min="5">
<mat-error *ngIf="twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('required')">
{{ 'admin.2fa.retry-verification-code-period-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('pattern')
|| twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('min')">
{{ 'admin.2fa.retry-verification-code-period-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block flex-1">
<mat-label translate>admin.2fa.max-verification-failures-before-user-lockout</mat-label>
<input matInput formControlName="maxVerificationFailuresBeforeUserLockout" type="number" step="1" min="0" max="65535">
<mat-error *ngIf="twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('pattern')
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('min')
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('max')">
{{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-expansion-panel class="provider">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title class="flex items-center justify-start"> <mat-panel-title>
<mat-slide-toggle (mousedown)="toggleExtensionPanel($event, providersForm.length, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)" <mat-slide-toggle class="mat-slide flex items-center justify-start"
formControlName="verificationCodeCheckRateLimitEnable"> (mousedown)="toggleExtensionPanel($event, 0, twoFaFormGroup.get('enforceTwoFa').value)"
formControlName="enforceTwoFa">
{{ 'admin.2fa.force-2fa' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
{{ 'admin.2fa.verification-code-check-rate-limit' | translate }}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="flex flex-row xs:flex-col gt-xs:gap-2"> <section class="tb-form-panel no-padding no-border" formGroupName="enforcedUsersFilter">
<mat-form-field class="mat-block flex-1"> <mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>admin.2fa.number-of-checking-attempts</mat-label> <mat-label translate>admin.2fa.enforce-for</mat-label>
<input matInput formControlName="verificationCodeCheckRateLimitNumber" required type="number" step="1" min="1"> <mat-select formControlName="type">
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('required')"> <mat-option *ngFor="let type of notificationTargetConfigTypes" [value]="type">
{{ 'admin.2fa.number-of-checking-attempts-required' | translate }} {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }}
</mat-error> </mat-option>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('pattern') </mat-select>
|| twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('min')">
{{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block flex-1"> <section class="tb-form-panel no-padding no-border" *ngIf="twoFaFormGroup.get('enforcedUsersFilter.type').value === notificationTargetConfigType.TENANT_ADMINISTRATORS">
<mat-label translate>admin.2fa.within-time</mat-label> <div class="flex flex-1 items-center justify-center">
<input matInput formControlName="verificationCodeCheckRateLimitTime" required type="number" step="1" min="1"> <tb-toggle-select class="tb-notification-tenant-group" appearance="fill"
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('required')"> formControlName="filterByTenants">
{{ 'admin.2fa.within-time-required' | translate }} <tb-toggle-option [value]="true">{{ 'tenant.tenant' | translate }}</tb-toggle-option>
</mat-error> <tb-toggle-option [value]="false">{{ 'tenant-profile.tenant-profile' | translate }}</tb-toggle-option>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('pattern') </tb-toggle-select>
|| twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('min')"> </div>
{{ 'admin.2fa.within-time-pattern' | translate }} <ng-container *ngIf="twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').value; else tenantProfiles">
</mat-error> <tb-entity-list
</mat-form-field> formControlName="tenantsIds"
</div> subscriptSizing="dynamic"
appearance="outline"
labelText="{{ 'tenant.tenants' | translate }}"
placeholderText="{{ 'tenant.tenants' | translate }}"
hint="{{ 'notification.tenants-list-rule-hint' | translate }}"
[entityType]="entityType.TENANT">
</tb-entity-list>
</ng-container>
<ng-template #tenantProfiles>
<tb-entity-list
formControlName="tenantProfilesIds"
subscriptSizing="dynamic"
appearance="outline"
labelText="{{ 'tenant-profile.tenant-profiles' | translate }}"
placeholderText="{{ 'tenant-profile.tenant-profiles' | translate }}"
hint="{{ 'notification.tenant-profiles-list-rule-hint' | translate }}"
[entityType]="entityType.TENANT_PROFILE">
</tb-entity-list>
</ng-template>
</section>
</section>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<section class="tb-form-panel stroked" formArrayName="providers">
<div class="tb-form-panel-title" translate>admin.2fa.available-providers</div>
<ng-container *ngFor="let provider of providersForm.controls; let i = index; trackBy: trackByElement">
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [formGroupName]="i">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-center justify-start"
(mousedown)="toggleExtensionPanel($event, i, provider.get('enable').value)"
formControlName="enable">
{{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<ng-container [ngSwitch]="provider.get('providerType').value">
<ng-container *ngSwitchCase="twoFactorAuthProviderType.TOTP">
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>admin.2fa.issuer-name</mat-label>
<input matInput formControlName="issuerName" required>
<mat-error *ngIf="provider.get('issuerName').hasError('required') ||
provider.get('issuerName').hasError('pattern')">
{{ "admin.2fa.issuer-name-required" | translate }}
</mat-error>
</mat-form-field>
</ng-container>
<div *ngSwitchCase="twoFactorAuthProviderType.SMS"
>
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>admin.2fa.verification-message-template</mat-label>
<input matInput formControlName="smsVerificationMessageTemplate" required>
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('required')">
{{ "admin.2fa.verification-message-template-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('pattern')">
{{ "admin.2fa.verification-message-template-pattern" | translate }}
</mat-error>
</mat-form-field>
<tb-time-unit-input
appearance="outline"
subscriptSizing="dynamic"
required
labelText="{{ 'admin.2fa.verification-code-lifetime' | translate }}"
requiredText="{{ 'admin.2fa.verification-code-lifetime-required' | translate }}"
minErrorText="{{ 'admin.2fa.verification-code-lifetime-pattern' | translate }}"
[minTime]="1"
formControlName="verificationCodeLifetime">
</tb-time-unit-input>
</div>
<div *ngSwitchCase="twoFactorAuthProviderType.EMAIL">
<tb-time-unit-input
appearance="outline"
subscriptSizing="dynamic"
required
labelText="{{ 'admin.2fa.verification-code-lifetime' | translate }}"
requiredText="{{ 'admin.2fa.verification-code-lifetime-required' | translate }}"
minErrorText="{{ 'admin.2fa.verification-code-lifetime-pattern' | translate }}"
[minTime]="1"
formControlName="verificationCodeLifetime">
</tb-time-unit-input>
</div>
<div *ngSwitchCase="twoFactorAuthProviderType.BACKUP_CODE">
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>admin.2fa.number-of-codes</mat-label>
<input matInput formControlName="codesQuantity" type="number" step="1" min="1" required>
<mat-error *ngIf="provider.get('codesQuantity').hasError('required')">
{{ "admin.2fa.number-of-codes-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('codesQuantity').hasError('min') ||
provider.get('codesQuantity').hasError('pattern')">
{{ "admin.2fa.number-of-codes-pattern" | translate }}
</mat-error>
</mat-form-field>
</div>
</ng-container>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>
</section>
<section class="tb-form-panel stroked mb-4">
<div class="tb-form-panel-title" translate>admin.2fa.verification-limitations</div>
<div class="tb-form-panel no-gap no-border no-padding">
<div class="input-row flex flex-col">
<tb-time-unit-input
appearance="outline"
required
labelText="{{ 'admin.2fa.total-allowed-time-for-verification' | translate }}"
requiredText="{{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }}"
minErrorText="{{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }}"
[minTime]="60"
formControlName="totalAllowedTimeForVerification">
</tb-time-unit-input>
<tb-time-unit-input
appearance="outline"
required
labelText="{{ 'admin.2fa.retry-verification-code-period' | translate }}"
requiredText="{{ 'admin.2fa.retry-verification-code-period-required' | translate }}"
minErrorText="{{ 'admin.2fa.retry-verification-code-period-pattern' | translate }}"
[minTime]="5"
formControlName="minVerificationCodeSendPeriod">
</tb-time-unit-input>
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>admin.2fa.max-verification-failures-before-user-lockout</mat-label>
<input matInput formControlName="maxVerificationFailuresBeforeUserLockout" type="number" step="1" min="0" max="65535">
<mat-error *ngIf="twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('pattern')
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('min')
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('max')">
{{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-center justify-start" (mousedown)="toggleExtensionPanel($event, providersForm.length, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)"
formControlName="verificationCodeCheckRateLimitEnable">
{{ 'admin.2fa.verification-code-check-rate-limit' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="flex flex-col">
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>admin.2fa.number-of-checking-attempts</mat-label>
<input matInput formControlName="verificationCodeCheckRateLimitNumber" required type="number" step="1" min="1">
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('required')">
{{ 'admin.2fa.number-of-checking-attempts-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('pattern')
|| twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('min')">
{{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }}
</mat-error>
</mat-form-field>
<tb-time-unit-input
appearance="outline"
subscriptSizing="dynamic"
required
labelText="{{ 'admin.2fa.within-time' | translate }}"
requiredText="{{ 'admin.2fa.within-time-required' | translate }}"
minErrorText="{{ 'admin.2fa.within-time-pattern' | translate }}"
[minTime]="1"
formControlName="verificationCodeCheckRateLimitTime">
</tb-time-unit-input>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
</section>
</div> </div>
<div class="flex flex-row items-center justify-end gap-2"> <div class="flex flex-row items-center justify-end gap-2">
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"

7
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss

@ -71,13 +71,6 @@
} }
:host ::ng-deep { :host ::ng-deep {
.mat-mdc-form-field {
.mat-mdc-form-field-infix {
width: 100%;
}
}
.mat-expansion-panel { .mat-expansion-panel {
.mat-expansion-panel-content { .mat-expansion-panel-content {
font-size: 16px; font-size: 16px;

72
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; import { Component, DestroyRef, OnInit, QueryList, ViewChildren } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -28,32 +28,40 @@ import {
TwoFactorAuthSettings, TwoFactorAuthSettings,
TwoFactorAuthSettingsForm TwoFactorAuthSettingsForm
} from '@shared/models/two-factor-auth.models'; } from '@shared/models/two-factor-auth.models';
import { isNotEmptyStr } from '@core/utils'; import { isDefined, isNotEmptyStr } from '@core/utils';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { MatExpansionPanel } from '@angular/material/expansion'; import { MatExpansionPanel } from '@angular/material/expansion';
import { NotificationTargetConfigType, NotificationTargetConfigTypeInfoMap } from '@shared/models/notification.models';
import { EntityType } from '@shared/models/entity-type.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'tb-2fa-settings', selector: 'tb-2fa-settings',
templateUrl: './two-factor-auth-settings.component.html', templateUrl: './two-factor-auth-settings.component.html',
styleUrls: [ './settings-card.scss', './two-factor-auth-settings.component.scss'] styleUrls: [ './settings-card.scss', './two-factor-auth-settings.component.scss']
}) })
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
private readonly destroy$ = new Subject<void>();
private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]; private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)];
twoFaFormGroup: UntypedFormGroup; twoFaFormGroup: UntypedFormGroup;
twoFactorAuthProviderType = TwoFactorAuthProviderType; twoFactorAuthProviderType = TwoFactorAuthProviderType;
twoFactorAuthProvidersData = twoFactorAuthProvidersData; twoFactorAuthProvidersData = twoFactorAuthProvidersData;
notificationTargetConfigType = NotificationTargetConfigType;
notificationTargetConfigTypes: NotificationTargetConfigType[] = this.allowNotificationTargetConfigTypes();
notificationTargetConfigTypeInfoMap = NotificationTargetConfigTypeInfoMap;
filterByTenants: boolean;
entityType = EntityType;
showMainLoadingBar = false; showMainLoadingBar = false;
@ViewChildren(MatExpansionPanel) expansionPanel: QueryList<MatExpansionPanel>; @ViewChildren(MatExpansionPanel) expansionPanel: QueryList<MatExpansionPanel>;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private twoFaService: TwoFactorAuthenticationService, private twoFaService: TwoFactorAuthenticationService,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
super(store); super(store);
} }
@ -64,12 +72,6 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
}); });
} }
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
confirmForm(): UntypedFormGroup { confirmForm(): UntypedFormGroup {
return this.twoFaFormGroup; return this.twoFaFormGroup;
} }
@ -80,7 +82,10 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
this.joinRateLimit(setting, 'verificationCodeCheckRateLimit'); this.joinRateLimit(setting, 'verificationCodeCheckRateLimit');
const providers = setting.providers.filter(provider => provider.enable); const providers = setting.providers.filter(provider => provider.enable);
providers.forEach(provider => delete provider.enable); providers.forEach(provider => delete provider.enable);
const config = Object.assign(setting, {providers}); const enforcedUsersFilter = this.twoFaFormGroup.get('enforcedUsersFilter').value;
delete enforcedUsersFilter.filterByTenants;
const config = Object.assign(setting, {providers}, {enforcedUsersFilter});
this.filterByTenants = this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').value;
this.twoFaService.saveTwoFaSettings(config).subscribe( this.twoFaService.saveTwoFaSettings(config).subscribe(
(settings) => { (settings) => {
this.setAuthConfigFormValue(settings); this.setAuthConfigFormValue(settings);
@ -117,6 +122,13 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
private build2faSettingsForm(): void { private build2faSettingsForm(): void {
this.twoFaFormGroup = this.fb.group({ this.twoFaFormGroup = this.fb.group({
enforceTwoFa: [false],
enforcedUsersFilter: this.fb.group({
type: [NotificationTargetConfigType.ALL_USERS],
filterByTenants: [true],
tenantsIds: [],
tenantProfilesIds: []
}),
maxVerificationFailuresBeforeUserLockout: [30, [ maxVerificationFailuresBeforeUserLockout: [30, [
Validators.pattern(/^\d*$/), Validators.pattern(/^\d*$/),
Validators.min(0), Validators.min(0),
@ -137,7 +149,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
this.buildProvidersSettingsForm(provider); this.buildProvidersSettingsForm(provider);
}); });
this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe( this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe(
takeUntil(this.destroy$) takeUntilDestroyed(this.destroyRef)
).subscribe(value => { ).subscribe(value => {
if (value) { if (value) {
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false}); this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false});
@ -148,7 +160,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
} }
}); });
this.providersForm.valueChanges.pipe( this.providersForm.valueChanges.pipe(
takeUntil(this.destroy$) takeUntilDestroyed(this.destroyRef)
).subscribe((value: TwoFactorAuthProviderConfigForm[]) => { ).subscribe((value: TwoFactorAuthProviderConfigForm[]) => {
const activeProvider = value.filter(provider => provider.enable); const activeProvider = value.filter(provider => provider.enable);
const indexBackupCode = Object.values(TwoFactorAuthProviderType).indexOf(TwoFactorAuthProviderType.BACKUP_CODE); const indexBackupCode = Object.values(TwoFactorAuthProviderType).indexOf(TwoFactorAuthProviderType.BACKUP_CODE);
@ -161,6 +173,15 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
this.providersForm.at(indexBackupCode).get('enable').enable( {emitEvent: false}); this.providersForm.at(indexBackupCode).get('enable').enable( {emitEvent: false});
} }
}); });
this.twoFaFormGroup.get('enforceTwoFa').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
if (value) {
this.twoFaFormGroup.get('enforcedUsersFilter').enable({emitEvent: false});
} else {
this.twoFaFormGroup.get('enforcedUsersFilter').disable({emitEvent: false});
}
});
} }
private setAuthConfigFormValue(settings: TwoFactorAuthSettings) { private setAuthConfigFormValue(settings: TwoFactorAuthSettings) {
@ -172,19 +193,24 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
verificationCodeCheckRateLimitTime: checkRateLimitTime || 900, verificationCodeCheckRateLimitTime: checkRateLimitTime || 900,
providers: [] providers: []
}); });
if (settings?.enforceTwoFa) {
this.getByIndexPanel(0).open();
}
if (checkRateLimitNumber > 0) { if (checkRateLimitNumber > 0) {
this.getByIndexPanel(this.providersForm.length).open(); this.getByIndexPanel(this.providersForm.length+1).open();
} }
Object.values(TwoFactorAuthProviderType).forEach((provider, index) => { Object.values(TwoFactorAuthProviderType).forEach((provider, index) => {
const findIndex = allowProvidersConfig.indexOf(provider); const findIndex = allowProvidersConfig.indexOf(provider);
if (findIndex > -1) { if (findIndex > -1) {
processFormValue.providers.push(Object.assign(settings.providers[findIndex], {enable: true})); processFormValue.providers.push(Object.assign(settings.providers[findIndex], {enable: true}));
this.getByIndexPanel(index).open(); this.getByIndexPanel(index+1).open();
} else { } else {
processFormValue.providers.push({enable: false}); processFormValue.providers.push({enable: false});
} }
}); });
this.twoFaFormGroup.patchValue(processFormValue); this.twoFaFormGroup.patchValue(processFormValue);
this.filterByTenants = isDefined(this.filterByTenants) ? this.filterByTenants : !Array.isArray(settings?.enforcedUsersFilter.tenantProfilesIds);
this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').patchValue(this.filterByTenants, {onlySelf: true});
} }
private buildProvidersSettingsForm(provider: TwoFactorAuthProviderType) { private buildProvidersSettingsForm(provider: TwoFactorAuthProviderType) {
@ -212,7 +238,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
} }
const newProviders = this.fb.group(formControlConfig); const newProviders = this.fb.group(formControlConfig);
newProviders.get('enable').valueChanges.pipe( newProviders.get('enable').valueChanges.pipe(
takeUntil(this.destroy$) takeUntilDestroyed(this.destroyRef)
).subscribe(value => { ).subscribe(value => {
if (value) { if (value) {
newProviders.enable({emitEvent: false}); newProviders.enable({emitEvent: false});
@ -245,4 +271,12 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
delete processFormValue[`${property}Number`]; delete processFormValue[`${property}Number`];
delete processFormValue[`${property}Time`]; delete processFormValue[`${property}Time`];
} }
private allowNotificationTargetConfigTypes(): NotificationTargetConfigType[] {
return [
NotificationTargetConfigType.ALL_USERS,
NotificationTargetConfigType.TENANT_ADMINISTRATORS,
NotificationTargetConfigType.SYSTEM_ADMINISTRATORS
];
}
} }

13
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html

@ -52,6 +52,19 @@
<form [formGroup]="totpConfigForm" class="flex flex-col items-center justify-start" (ngSubmit)="onSaveConfig()"> <form [formGroup]="totpConfigForm" class="flex flex-col items-center justify-start" (ngSubmit)="onSaveConfig()">
<p class="mat-body qr-code-description" translate>security.2fa.dialog.scan-qr-code</p> <p class="mat-body qr-code-description" translate>security.2fa.dialog.scan-qr-code</p>
<canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas> <canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas>
<p class="mat-body qr-code-description" translate>login.enter-key-manually</p>
<div class="flex flex-row items-center w-full overflow-hidden max-w-[375px]">
<span tbTruncateWithTooltip class="w-full">{{ totpAuthURLSecret }}</span>
<tb-copy-button
class="attribute-copy"
[disabled]="isLoading$ | async"
[copyText]="totpAuthURLSecret"
tooltipText="{{ 'attribute.copy-key' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{'font-size': '24px'}">
</tb-copy-button>
</div>
<p class="mat-body qr-code-description" style="margin-top: 30px;" translate>security.2fa.dialog.enter-verification-code</p> <p class="mat-body qr-code-description" style="margin-top: 30px;" translate>security.2fa.dialog.enter-verification-code</p>
<mat-form-field class="mat-block code-container flex-1"> <mat-form-field class="mat-block code-container flex-1">
<input matInput formControlName="verificationCode" <input matInput formControlName="verificationCode"

2
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts

@ -42,6 +42,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
totpConfigForm: UntypedFormGroup; totpConfigForm: UntypedFormGroup;
totpAuthURL: string; totpAuthURL: string;
totpAuthURLSecret: string;
@ViewChild('stepper', {static: false}) stepper: MatStepper; @ViewChild('stepper', {static: false}) stepper: MatStepper;
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>; @ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
@ -55,6 +56,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => { this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => {
this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig; this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
this.totpAuthURL = this.authAccountConfig.authUrl; this.totpAuthURL = this.authAccountConfig.authUrl;
this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret');
this.authAccountConfig.useByDefault = true; this.authAccountConfig.useByDefault = true;
import('qrcode').then((QRCode) => { import('qrcode').then((QRCode) => {
unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);

11
ui-ngx/src/app/modules/login/login-routing.module.ts

@ -25,6 +25,7 @@ import { CreatePasswordComponent } from '@modules/login/pages/login/create-passw
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component'; import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -83,6 +84,16 @@ const routes: Routes = [
}, },
canActivate: [AuthGuard] canActivate: [AuthGuard]
}, },
{
path: 'login/force-mfa',
component: ForceTwoFactorAuthLoginComponent,
data: {
title: 'login.two-factor-authentication',
auth: [Authority.MFA_CONFIGURATION_TOKEN],
module: 'public'
},
canActivate: [AuthGuard]
},
{ {
path: 'activationLinkExpired', path: 'activationLinkExpired',
component: LinkExpiredComponent, component: LinkExpiredComponent,

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

Loading…
Cancel
Save