Browse Source

Merge branch 'master' into feature/notification-system

# Conflicts:
#	application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
#	application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
pull/7511/head
ViacheslavKlimov 4 years ago
parent
commit
1ba77a30d7
  1. 2
      application/pom.xml
  2. 8
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  3. 13
      application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json
  4. 8
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  5. 67
      application/src/main/data/upgrade/3.4.1/schema_update.sql
  6. 9
      application/src/main/data/upgrade/3.4.1/schema_update_before.sql
  7. 1
      application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java
  8. 16
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  9. 3
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  10. 42
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  11. 49
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  12. 5
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  13. 72
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  14. 8
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  15. 28
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  16. 4
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  17. 6
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  18. 17
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  19. 9
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  20. 5
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  21. 4
      application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java
  22. 54
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  23. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java
  24. 64
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java
  25. 58
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java
  26. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java
  27. 19
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
  28. 37
      application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java
  29. 26
      application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java
  30. 15
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  31. 19
      application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java
  32. 68
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  33. 4
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  34. 9
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  35. 14
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  36. 9
      application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java
  37. 38
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  38. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  39. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  40. 23
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  41. 14
      application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java
  42. 8
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java
  43. 71
      application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java
  44. 60
      application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java
  45. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
  46. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
  47. 162
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java
  48. 69
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
  49. 29
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java
  50. 35
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java
  51. 23
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java
  52. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
  53. 25
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  54. 9
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  55. 10
      application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
  56. 42
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  57. 41
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  58. 1
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  59. 24
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  60. 10
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  61. 15
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  62. 2
      application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
  63. 5
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java
  64. 6
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  65. 6
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
  66. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
  67. 5
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  68. 18
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  69. 2
      application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java
  70. 22
      application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java
  71. 57
      application/src/main/resources/thingsboard.yml
  72. 21
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  73. 122
      application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java
  74. 4
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java
  75. 16
      application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java
  76. 12
      application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java
  77. 69
      application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java
  78. 22
      application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
  79. 20
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  80. 4
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  81. 19
      application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java
  82. 2
      application/src/test/java/org/thingsboard/server/edge/BaseTelemetryEdgeTest.java
  83. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  84. 247
      application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java
  85. 2
      application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java
  86. 107
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java
  87. 10
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  88. 167
      application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java
  89. 68
      application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandlerTest.java
  90. 48
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  91. 12
      application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java
  92. 1
      application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java
  93. 5
      application/src/test/resources/application-test.properties
  94. 2
      application/src/test/resources/logback-test.xml
  95. 2
      common/actor/pom.xml
  96. 2
      common/cache/pom.xml
  97. 16
      common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java
  98. 2
      common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java
  99. 34
      common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java
  100. 36
      common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.4.2-SNAPSHOT</version>
<version>3.4.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>

8
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -57,9 +57,9 @@
"name": "Log RPC from Device",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
@ -71,9 +71,9 @@
"name": "Log Other",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{

13
application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json

@ -70,9 +70,9 @@
"name": "Log RPC from Device",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
@ -84,9 +84,9 @@
"name": "Log Other",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
@ -174,11 +174,6 @@
"fromIndex": 3,
"toIndex": 7,
"type": "Timeseries Updated"
},
{
"fromIndex": 4,
"toIndex": 7,
"type": "Success"
}
],
"ruleChainConnections": null

8
application/src/main/data/json/tenant/rule_chains/root_rule_chain.json

@ -57,9 +57,9 @@
"name": "Log RPC from Device",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
@ -71,9 +71,9 @@
"name": "Log Other",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{

67
application/src/main/data/upgrade/3.4.1/schema_update.sql

@ -14,6 +14,7 @@
-- limitations under the License.
--
-- AUDIT LOGS MIGRATION START
DO
$$
DECLARE table_partition RECORD;
@ -21,6 +22,8 @@ $$
-- in case of running the upgrade script a second time:
IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_audit_log')) THEN
ALTER TABLE audit_log RENAME TO old_audit_log;
CREATE INDEX IF NOT EXISTS idx_old_audit_log_created_time ON old_audit_log(created_time);
ALTER INDEX IF EXISTS idx_audit_log_tenant_id_and_created_time RENAME TO idx_old_audit_log_tenant_id_and_created_time;
FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts
@ -50,6 +53,7 @@ CREATE TABLE IF NOT EXISTS audit_log (
action_failure_details varchar(1000000)
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_id_and_created_time ON audit_log(tenant_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_audit_log_id ON audit_log(id);
CREATE OR REPLACE PROCEDURE migrate_audit_logs(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT)
LANGUAGE plpgsql AS
@ -73,3 +77,66 @@ BEGIN
WHERE created_time >= start_time_ms AND created_time < end_time_ms;
END;
$$;
-- AUDIT LOGS MIGRATION END
-- EDGE EVENTS MIGRATION START
DO
$$
DECLARE table_partition RECORD;
BEGIN
-- in case of running the upgrade script a second time:
IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN
ALTER TABLE edge_event RENAME TO old_edge_event;
CREATE INDEX IF NOT EXISTS idx_old_blob_entity_created_time_tmp ON old_blob_entity(created_time);
ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time;
FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts
FROM pg_tables WHERE tablename LIKE 'edge_event_%'
LOOP
EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts);
END LOOP;
ELSE
RAISE NOTICE 'Table old_edge_event already exists, leaving as is';
END IF;
END;
$$;
CREATE TABLE IF NOT EXISTS edge_event (
id uuid NOT NULL,
created_time bigint NOT NULL,
edge_id uuid,
edge_event_type varchar(255),
edge_event_uid varchar(255),
entity_id uuid,
edge_event_action varchar(255),
body varchar(10000000),
tenant_id uuid,
ts bigint NOT NULL
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id);
CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT)
LANGUAGE plpgsql AS
$$
DECLARE
p RECORD;
partition_end_ts BIGINT;
BEGIN
FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event
WHERE created_time >= start_time_ms AND created_time < end_time_ms
LOOP
partition_end_ts = p.partition_ts + partition_size_ms;
RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts;
EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' ||
'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts);
END LOOP;
INSERT INTO edge_event
SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts
FROM old_edge_event
WHERE created_time >= start_time_ms AND created_time < end_time_ms;
END;
$$;
-- EDGE EVENTS MIGRATION END

9
application/src/main/data/upgrade/3.4.1/schema_update_before.sql

@ -37,9 +37,10 @@ CREATE OR REPLACE PROCEDURE update_asset_profiles()
LANGUAGE plpgsql AS
$$
BEGIN
UPDATE asset as a SET asset_profile_id = p.id
FROM
(SELECT id, tenant_id, name from asset_profile) as p
WHERE a.asset_profile_id IS NULL AND p.tenant_id = a.tenant_id AND a.type = p.name;
UPDATE asset a SET asset_profile_id = COALESCE(
(SELECT id from asset_profile p WHERE p.tenant_id = a.tenant_id AND a.type = p.name),
(SELECT id from asset_profile p WHERE p.tenant_id = a.tenant_id AND p.name = 'default')
)
WHERE a.asset_profile_id IS NULL;
END;
$$;

1
application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java

@ -29,6 +29,7 @@ import java.util.Arrays;
@ComponentScan({"org.thingsboard.server.install",
"org.thingsboard.server.service.component",
"org.thingsboard.server.service.install",
"org.thingsboard.server.service.security.auth.jwt.settings",
"org.thingsboard.server.dao",
"org.thingsboard.server.common.stats",
"org.thingsboard.server.common.transport.config.ssl",

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

@ -34,7 +34,7 @@ import org.thingsboard.rule.engine.api.NotificationManager;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.cluster.TbClusterService;
@ -77,6 +77,8 @@ import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
@ -273,7 +275,7 @@ public class ActorSystemContext {
@Autowired(required = false)
@Getter
private MvelInvokeService mvelInvokeService;
private TbelInvokeService tbelInvokeService;
@Autowired
@Getter
@ -384,6 +386,16 @@ public class ActorSystemContext {
@Getter
private QueueService queueService;
@Lazy
@Autowired(required = false)
@Getter
private WidgetsBundleService widgetsBundleService;
@Lazy
@Autowired(required = false)
@Getter
private WidgetTypeService widgetTypeService;
@Value("${actors.session.max_concurrent_sessions_per_device:1}")
@Getter
private long maxConcurrentSessionsPerDevice;

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

@ -823,6 +823,9 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
body.put("expirationTime", msg.getExpirationTime());
body.put("method", msg.getBody().getMethod());
body.put("params", msg.getBody().getParams());
body.put("persisted", msg.isPersisted());
body.put("retries", msg.getRetries());
body.put("additionalInfo", msg.getAdditionalInfo());
EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL, deviceId, body);

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

@ -27,6 +27,7 @@ import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationManager;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
@ -36,6 +37,7 @@ import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.rule.engine.util.TenantIdLoader;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.cluster.TbClusterService;
@ -90,11 +92,13 @@ import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine;
import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
import java.util.Collections;
import java.util.List;
@ -471,11 +475,11 @@ class DefaultTbContext implements TbContext {
return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsInvokeService(), script, argNames);
}
private ScriptEngine createMvelScriptEngine(String script, String... argNames) {
if (mainCtx.getMvelInvokeService() == null) {
throw new RuntimeException("MVEL execution is disabled!");
private ScriptEngine createTbelScriptEngine(String script, String... argNames) {
if (mainCtx.getTbelInvokeService() == null) {
throw new RuntimeException("TBEL execution is disabled!");
}
return new RuleNodeMvelScriptEngine(getTenantId(), mainCtx.getMvelInvokeService(), script, argNames);
return new RuleNodeTbelScriptEngine(getTenantId(), mainCtx.getTbelInvokeService(), script, argNames);
}
@Override
@ -489,11 +493,11 @@ class DefaultTbContext implements TbContext {
switch (scriptLang) {
case JS:
return createJsScriptEngine(script, argNames);
case MVEL:
case TBEL:
if (Arrays.isNullOrEmpty(argNames)) {
return createMvelScriptEngine(script, "msg", "metadata", "msgType");
return createTbelScriptEngine(script, "msg", "metadata", "msgType");
} else {
return createMvelScriptEngine(script, argNames);
return createTbelScriptEngine(script, argNames);
}
default:
throw new RuntimeException("Unsupported script language: " + scriptLang.name());
@ -757,12 +761,34 @@ class DefaultTbContext implements TbContext {
return mainCtx.getTenantProfileCache().get(getTenantId());
}
@Override
public WidgetsBundleService getWidgetBundleService() {
return mainCtx.getWidgetsBundleService();
}
@Override
public WidgetTypeService getWidgetTypeService() {
return mainCtx.getWidgetTypeService();
}
@Override
public RuleEngineApiUsageStateService getRuleEngineApiUsageStateService() {
return mainCtx.getApiUsageStateService();
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());
return metaData;
}
@Override
public void checkTenantEntity(EntityId entityId) {
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) {
throw new RuntimeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.");
}
}
private class SimpleTbQueueCallback implements TbQueueCallback {
private final Runnable onSuccess;
private final Consumer<Throwable> onFailure;

49
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -22,7 +22,9 @@ import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
@ -37,8 +39,13 @@ import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -65,6 +72,14 @@ public class AdminController extends BaseController {
@Autowired
private SystemSecurityService systemSecurityService;
@Lazy
@Autowired
private JwtSettingsService jwtSettingsService;
@Lazy
@Autowired
private JwtTokenFactory tokenFactory;
@Autowired
private EntitiesVersionControlService versionControlService;
@ -152,6 +167,40 @@ public class AdminController extends BaseController {
}
}
@ApiOperation(value = "Get the JWT Settings object (getJwtSettings)",
notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.GET)
@ResponseBody
public JwtSettings getJwtSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(jwtSettingsService.getJwtSettings());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Update JWT Settings (saveJwtSettings)",
notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.POST)
@ResponseBody
public JwtPair saveJwtSettings(
@ApiParam(value = "A JSON value representing the JWT Settings.")
@RequestBody JwtSettings jwtSettings) throws ThingsboardException {
try {
SecurityUser securityUser = getCurrentUser();
accessControlService.checkPermission(securityUser, Resource.ADMIN_SETTINGS, Operation.WRITE);
checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings));
return tokenFactory.createTokenPair(securityUser);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Send test email (sendTestMail)",
notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " +
"You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH)

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

@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
@ -86,7 +85,6 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
@RestController
@TbCoreComponent
@ -148,9 +146,6 @@ public class AssetController extends BaseController {
@RequestMapping(value = "/asset", method = RequestMethod.POST)
@ResponseBody
public Asset saveAsset(@ApiParam(value = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception {
if (TB_SERVICE_QUEUE.equals(asset.getType())) {
throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
asset.setTenantId(getTenantId());
checkEntity(asset.getId(), asset, Resource.ASSET);
return tbAssetService.save(asset, getCurrentUser());

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

@ -42,24 +42,22 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.model.JwtToken;
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.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
import org.thingsboard.server.service.security.model.ResetPasswordRequest;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import ua_parser.Client;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
@ -73,7 +71,6 @@ import java.net.URISyntaxException;
public class AuthController extends BaseController {
private final BCryptPasswordEncoder passwordEncoder;
private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final MailService mailService;
private final SystemSecurityService systemSecurityService;
private final AuditLogService auditLogService;
@ -128,7 +125,7 @@ public class AuthController extends BaseController {
sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED);
eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId()));
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
ObjectNode response = JacksonUtil.newObjectNode();
response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken());
response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken());
@ -239,7 +236,7 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtTokenPair activateUser(
public JwtPair activateUser(
@ApiParam(value = "Activate user request.")
@RequestBody ActivateUserRequest activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
@ -268,10 +265,7 @@ public class AuthController extends BaseController {
sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
return tokenFactory.createTokenPair(securityUser);
} catch (Exception e) {
throw handleException(e);
}
@ -284,7 +278,7 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtTokenPair resetPassword(
public JwtPair resetPassword(
@ApiParam(value = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {
@ -309,11 +303,9 @@ public class AuthController extends BaseController {
String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email);
eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId()));
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
return tokenFactory.createTokenPair(securityUser);
} else {
throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
@ -324,49 +316,9 @@ public class AuthController extends BaseController {
private void logLogoutAction(HttpServletRequest request) throws ThingsboardException {
try {
SecurityUser user = getCurrentUser();
RestAuthenticationDetails details = new RestAuthenticationDetails(request);
String clientAddress = details.getClientAddress();
String browser = "Unknown";
String os = "Unknown";
String device = "Unknown";
if (details.getUserAgent() != null) {
Client userAgent = details.getUserAgent();
if (userAgent.userAgent != null) {
browser = userAgent.userAgent.family;
if (userAgent.userAgent.major != null) {
browser += " " + userAgent.userAgent.major;
if (userAgent.userAgent.minor != null) {
browser += "." + userAgent.userAgent.minor;
if (userAgent.userAgent.patch != null) {
browser += "." + userAgent.userAgent.patch;
}
}
}
}
if (userAgent.os != null) {
os = userAgent.os.family;
if (userAgent.os.major != null) {
os += " " + userAgent.os.major;
if (userAgent.os.minor != null) {
os += "." + userAgent.os.minor;
if (userAgent.os.patch != null) {
os += "." + userAgent.os.patch;
if (userAgent.os.patchMinor != null) {
os += "." + userAgent.os.patchMinor;
}
}
}
}
}
if (userAgent.device != null) {
device = userAgent.device.family;
}
}
auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device);
var user = getCurrentUser();
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGOUT, null);
eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId()));
} catch (Exception e) {
throw handleException(e);
}

8
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -123,6 +123,7 @@ import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -423,9 +424,12 @@ public abstract class BaseController {
}
PageLink createPageLink(int pageSize, int page, String textSearch, String sortProperty, String sortOrder) throws ThingsboardException {
if (!StringUtils.isEmpty(sortProperty)) {
if (StringUtils.isNotEmpty(sortProperty)) {
if (!Validator.isValidProperty(sortProperty)) {
throw new IllegalArgumentException("Invalid sort property");
}
SortOrder.Direction direction = SortOrder.Direction.ASC;
if (!StringUtils.isEmpty(sortOrder)) {
if (StringUtils.isNotEmpty(sortOrder)) {
try {
direction = SortOrder.Direction.valueOf(sortOrder.toUpperCase());
} catch (IllegalArgumentException e) {

28
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -38,7 +38,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.EventInfo;
@ -69,7 +69,7 @@ import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine;
import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -146,7 +146,7 @@ public class RuleChainController extends BaseController {
private JsInvokeService jsInvokeService;
@Autowired(required = false)
private MvelInvokeService mvelInvokeService;
private TbelInvokeService tbelInvokeService;
@Autowired(required = false)
private ActorSystemContext actorContext;
@ -154,8 +154,8 @@ public class RuleChainController extends BaseController {
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
private boolean debugPerTenantEnabled;
@Value("${mvel.enabled:true}")
private boolean mvelEnabled;
@Value("${tbel.enabled:true}")
private boolean tbelEnabled;
@ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@ -378,13 +378,13 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Is MVEL script executor enabled",
notes = "Returns 'True' if the MVEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Is TBEL script executor enabled",
notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/mvelEnabled", method = RequestMethod.GET)
@RequestMapping(value = "/ruleChain/tbelEnabled", method = RequestMethod.GET)
@ResponseBody
public Boolean isMvelEnabled() {
return mvelEnabled;
public Boolean isTbelEnabled() {
return tbelEnabled;
}
@ApiOperation(value = "Test Script function",
@ -393,7 +393,7 @@ public class RuleChainController extends BaseController {
@RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
@ResponseBody
public JsonNode testScript(
@ApiParam(value = "Script language: JS or MVEL")
@ApiParam(value = "Script language: JS or TBEL")
@RequestParam(required = false) ScriptLanguage scriptLang,
@ApiParam(value = "Test JS request. See API call description above.")
@RequestBody JsonNode inputParams) throws ThingsboardException {
@ -418,10 +418,10 @@ public class RuleChainController extends BaseController {
if (ScriptLanguage.JS.equals(scriptLang)) {
engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, script, argNames);
} else {
if (mvelInvokeService == null) {
throw new IllegalArgumentException("MVEL script engine is disabled!");
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new RuleNodeMvelScriptEngine(getTenantId(), mvelInvokeService, script, argNames);
engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames);
}
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) {

4
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -661,10 +661,8 @@ public class TelemetryController extends BaseController {
logAttributesDeleted(user, entityId, scope, keys, null);
if (entityIdSrc.getEntityType().equals(EntityType.DEVICE)) {
DeviceId deviceId = new DeviceId(entityId.getId());
Set<AttributeKey> keysToNotify = new HashSet<>();
keys.forEach(key -> keysToNotify.add(new AttributeKey(scope, key)));
tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(
user.getTenantId(), deviceId, keysToNotify), null);
user.getTenantId(), deviceId, scope, keys), null);
}
result.setResult(new ResponseEntity<>(HttpStatus.OK));
}

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

@ -39,7 +39,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -87,8 +87,8 @@ public class TwoFactorAuthController extends BaseController {
"and Too Many Requests error if rate limits are exceeded.")
@PostMapping("/verification/check")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
public JwtPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
if (verificationSuccess) {

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

@ -43,12 +43,10 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -95,7 +93,6 @@ public class UserController extends BaseController {
private final MailService mailService;
private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final SystemSecurityService systemSecurityService;
private final ApplicationEventPublisher eventPublisher;
private final TbUserService tbUserService;
@ -148,7 +145,7 @@ public class UserController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET)
@ResponseBody
public JwtTokenPair getUserToken(
public JwtPair getUserToken(
@ApiParam(value = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId) throws ThingsboardException {
checkParameter(USER_ID, strUserId);
@ -163,9 +160,7 @@ public class UserController extends BaseController {
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
UserCredentials credentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), userId);
SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
return tokenFactory.createTokenPair(securityUser);
} catch (Exception e) {
throw handleException(e);
}
@ -187,7 +182,7 @@ public class UserController extends BaseController {
@RequestBody User user,
@ApiParam(value = "Send activation email (or use activation link)", defaultValue = "true")
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException {
if (Authority.TENANT_ADMIN.equals(getCurrentUser().getAuthority())) {
if (!Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
user.setTenantId(getCurrentUser().getTenantId());
}
checkEntity(user.getId(), user, Resource.USER);
@ -376,7 +371,7 @@ public class UserController extends BaseController {
userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled);
if (!userCredentialsEnabled) {
eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId));
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
}
} catch (Exception e) {
throw handleException(e);

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

@ -26,6 +26,7 @@ import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.install.DatabaseEntitiesUpgradeService;
import org.thingsboard.server.service.install.DatabaseTsUpgradeService;
import org.thingsboard.server.service.install.EntityDatabaseSchemaService;
import org.thingsboard.server.service.install.NoSqlKeyspaceService;
import org.thingsboard.server.service.install.SystemDataLoaderService;
import org.thingsboard.server.service.install.TsDatabaseSchemaService;
import org.thingsboard.server.service.install.TsLatestDatabaseSchemaService;
@ -51,6 +52,9 @@ public class ThingsboardInstallService {
@Autowired
private EntityDatabaseSchemaService entityDatabaseSchemaService;
@Autowired(required = false)
private NoSqlKeyspaceService noSqlKeyspaceService;
@Autowired
private TsDatabaseSchemaService tsDatabaseSchemaService;
@ -252,6 +256,10 @@ public class ThingsboardInstallService {
log.info("Installing DataBase schema for timeseries...");
if (noSqlKeyspaceService != null) {
noSqlKeyspaceService.createDatabaseSchema();
}
tsDatabaseSchemaService.createDatabaseSchema();
if (tsLatestDatabaseSchemaService != null) {
@ -265,6 +273,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.createSysAdmin();
systemDataLoaderService.createDefaultTenantProfiles();
systemDataLoaderService.createAdminSettings();
systemDataLoaderService.createRandomJwtSettings();
systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.createOAuth2Templates();
systemDataLoaderService.createQueues();

5
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -395,6 +395,11 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
}
}
@Override
public ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id) {
return apiUsageStateService.findApiUsageStateById(tenantId, id);
}
private interface StateChecker {
boolean check(long threshold, long warnThreshold, long value);
}

4
application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java

@ -16,7 +16,7 @@
package org.thingsboard.server.service.apiusage;
import org.springframework.context.ApplicationListener;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
@ -26,7 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
public interface TbApiUsageStateService extends TbApiUsageStateClient, ApplicationListener<PartitionChangeEvent> {
public interface TbApiUsageStateService extends TbApiUsageStateClient, RuleEngineApiUsageStateService, ApplicationListener<PartitionChangeEvent> {
void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback);

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

@ -187,10 +187,7 @@ public final class EdgeGrpcSession implements Closeable {
public void startSyncProcess(TenantId tenantId, EdgeId edgeId, boolean fullSync) {
log.trace("[{}][{}] Staring edge sync process", tenantId, edgeId);
syncCompleted = false;
if (sessionState.getSendDownlinkMsgsFuture() != null && sessionState.getSendDownlinkMsgsFuture().isDone()) {
String errorMsg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId);
sessionState.getSendDownlinkMsgsFuture().setException(new RuntimeException(errorMsg));
}
interruptGeneralProcessingOnSync(tenantId, edgeId);
doSync(new EdgeSyncCursor(ctx, edge, fullSync));
}
@ -244,7 +241,7 @@ public final class EdgeGrpcSession implements Closeable {
@Override
public void onFailure(Throwable t) {
String errorMsg = t.getMessage() != null ? t.getMessage() : "";
String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t);
UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder()
.setUplinkMsgId(uplinkMsg.getUplinkMsgId())
.setSuccess(false).setErrorMsg(errorMsg).build();
@ -265,10 +262,7 @@ public final class EdgeGrpcSession implements Closeable {
}
if (sessionState.getPendingMsgsMap().isEmpty()) {
log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey());
if (sessionState.getScheduledSendDownlinkTask() != null) {
sessionState.getScheduledSendDownlinkTask().cancel(false);
}
sessionState.getSendDownlinkMsgsFuture().set(null);
stopCurrentSendDownlinkMsgsTask(null);
}
} catch (Exception e) {
log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e);
@ -391,15 +385,14 @@ public final class EdgeGrpcSession implements Closeable {
}
private ListenableFuture<Void> sendDownlinkMsgsPack(List<DownlinkMsg> downlinkMsgsPack) {
if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) {
String errorMsg = "[" + this.sessionId + "] Previous send downlink future was not properly completed, stopping it now";
log.error(errorMsg);
sessionState.getSendDownlinkMsgsFuture().setException(new RuntimeException(errorMsg));
}
interruptPreviousSendDownlinkMsgsTask();
sessionState.setSendDownlinkMsgsFuture(SettableFuture.create());
sessionState.getPendingMsgsMap().clear();
downlinkMsgsPack.forEach(msg -> sessionState.getPendingMsgsMap().put(msg.getDownlinkMsgId(), msg));
scheduleDownlinkMsgsPackSend(1);
return sessionState.getSendDownlinkMsgsFuture();
}
@ -422,13 +415,13 @@ public final class EdgeGrpcSession implements Closeable {
} else {
log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}",
this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy);
sessionState.getSendDownlinkMsgsFuture().set(null);
stopCurrentSendDownlinkMsgsTask(null);
}
} else {
sessionState.getSendDownlinkMsgsFuture().set(null);
stopCurrentSendDownlinkMsgsTask(null);
}
} catch (Exception e) {
sessionState.getSendDownlinkMsgsFuture().setException(e);
stopCurrentSendDownlinkMsgsTask(e);
}
};
@ -611,7 +604,7 @@ public final class EdgeGrpcSession implements Closeable {
}
if (uplinkMsg.getDeviceRpcCallMsgCount() > 0) {
for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) {
result.add(ctx.getDeviceProcessor().processDeviceRpcCallResponseFromEdge(edge.getTenantId(), deviceRpcCallMsg));
result.add(ctx.getDeviceProcessor().processDeviceRpcCallFromEdge(edge.getTenantId(), edge, deviceRpcCallMsg));
}
}
if (uplinkMsg.getWidgetBundleTypesRequestMsgCount() > 0) {
@ -673,4 +666,29 @@ public final class EdgeGrpcSession implements Closeable {
log.debug("[{}] Failed to close output stream: {}", sessionId, e.getMessage());
}
}
private void interruptPreviousSendDownlinkMsgsTask() {
String msg = String.format("[%s] Previous send downlink future was not properly completed, stopping it now!", this.sessionId);
stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg));
}
private void interruptGeneralProcessingOnSync(TenantId tenantId, EdgeId edgeId) {
String msg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId);
stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg));
}
public void stopCurrentSendDownlinkMsgsTask(Exception e) {
if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) {
if (e != null) {
log.warn(e.getMessage(), e);
sessionState.getSendDownlinkMsgsFuture().setException(e);
} else {
sessionState.getSendDownlinkMsgsFuture().set(null);
}
}
if (sessionState.getScheduledSendDownlinkTask() != null) {
sessionState.getScheduledSendDownlinkTask().cancel(true);
}
}
}

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.SettableFuture;
import lombok.Data;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
@ -26,7 +27,7 @@ import java.util.concurrent.ScheduledFuture;
@Data
public class EdgeSessionState {
private final Map<Integer, DownlinkMsg> pendingMsgsMap = new LinkedHashMap<>();
private final Map<Integer, DownlinkMsg> pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>());
private SettableFuture<Void> sendDownlinkMsgsFuture;
private ScheduledFuture<?> scheduledSendDownlinkTask;
}

64
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java

@ -21,13 +21,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RpcRequestMsg;
import org.thingsboard.server.gen.edge.v1.RpcResponseMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -97,25 +97,55 @@ public class DeviceMsgConstructor {
}
public DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body) {
int requestId = body.get("requestId").asInt();
boolean oneway = body.get("oneway").asBoolean();
UUID requestUUID = UUID.fromString(body.get("requestUUID").asText());
long expirationTime = body.get("expirationTime").asLong();
String method = body.get("method").asText();
String params = body.get("params").asText();
DeviceRpcCallMsg.Builder builder = constructDeviceRpcMsg(deviceId, body);
if (body.has("error") || body.has("response")) {
RpcResponseMsg.Builder responseBuilder = RpcResponseMsg.newBuilder();
if (body.has("error")) {
responseBuilder.setError(body.get("error").asText());
} else {
responseBuilder.setResponse(body.get("response").asText());
}
builder.setResponseMsg(responseBuilder.build());
} else {
RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder();
requestBuilder.setMethod(body.get("method").asText());
requestBuilder.setParams(body.get("params").asText());
builder.setRequestMsg(requestBuilder.build());
}
return builder.build();
}
RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder();
requestBuilder.setMethod(method);
requestBuilder.setParams(params);
private DeviceRpcCallMsg.Builder constructDeviceRpcMsg(UUID deviceId, JsonNode body) {
DeviceRpcCallMsg.Builder builder = DeviceRpcCallMsg.newBuilder()
.setDeviceIdMSB(deviceId.getMostSignificantBits())
.setDeviceIdLSB(deviceId.getLeastSignificantBits())
.setRequestUuidMSB(requestUUID.getMostSignificantBits())
.setRequestUuidLSB(requestUUID.getLeastSignificantBits())
.setRequestId(requestId)
.setExpirationTime(expirationTime)
.setOneway(oneway)
.setRequestMsg(requestBuilder.build());
return builder.build();
.setRequestId(body.get("requestId").asInt());
if (body.get("oneway") != null) {
builder.setOneway(body.get("oneway").asBoolean());
}
if (body.get("requestUUID") != null) {
UUID requestUUID = UUID.fromString(body.get("requestUUID").asText());
builder.setRequestUuidMSB(requestUUID.getMostSignificantBits())
.setRequestUuidLSB(requestUUID.getLeastSignificantBits());
}
if (body.get("expirationTime") != null) {
builder.setExpirationTime(body.get("expirationTime").asLong());
}
if (body.get("persisted") != null) {
builder.setPersisted(body.get("persisted").asBoolean());
}
if (body.get("retries") != null) {
builder.setRetries(body.get("retries").asInt());
}
if (body.get("additionalInfo") != null) {
builder.setAdditionalInfo(JacksonUtil.toString(body.get("additionalInfo")));
}
if (body.get("serviceId") != null) {
builder.setServiceId(body.get("serviceId").asText());
}
if (body.get("sessionId") != null) {
builder.setSessionId(body.get("sessionId").asText());
}
return builder;
}
}

58
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java

@ -52,6 +52,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
@ -325,8 +326,17 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor {
return metaData;
}
public ListenableFuture<Void> processDeviceRpcCallResponseFromEdge(TenantId tenantId, DeviceRpcCallMsg deviceRpcCallMsg) {
log.trace("[{}] processDeviceRpcCallResponseMsg [{}]", tenantId, deviceRpcCallMsg);
public ListenableFuture<Void> processDeviceRpcCallFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) {
log.trace("[{}] processDeviceRpcCallFromEdge [{}]", tenantId, deviceRpcCallMsg);
if (deviceRpcCallMsg.hasResponseMsg()) {
return processDeviceRpcResponseFromEdge(tenantId, deviceRpcCallMsg);
} else if (deviceRpcCallMsg.hasRequestMsg()) {
return processDeviceRpcRequestFromEdge(tenantId, edge, deviceRpcCallMsg);
}
return Futures.immediateFuture(null);
}
private ListenableFuture<Void> processDeviceRpcResponseFromEdge(TenantId tenantId, DeviceRpcCallMsg deviceRpcCallMsg) {
SettableFuture<Void> futureToSet = SettableFuture.create();
UUID requestUuid = new UUID(deviceRpcCallMsg.getRequestUuidMSB(), deviceRpcCallMsg.getRequestUuidLSB());
DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB()));
@ -357,6 +367,46 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor {
return futureToSet;
}
private ListenableFuture<Void> processDeviceRpcRequestFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) {
DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB()));
try {
TbMsgMetaData metaData = new TbMsgMetaData();
String requestId = Integer.toString(deviceRpcCallMsg.getRequestId());
metaData.putValue("requestId", requestId);
metaData.putValue("serviceId", deviceRpcCallMsg.getServiceId());
metaData.putValue("sessionId", deviceRpcCallMsg.getSessionId());
metaData.putValue(DataConstants.EDGE_ID, edge.getId().toString());
Device device = deviceService.findDeviceById(tenantId, deviceId);
if (device != null) {
metaData.putValue("deviceName", device.getName());
metaData.putValue("deviceType", device.getType());
metaData.putValue(DataConstants.DEVICE_ID, deviceId.getId().toString());
}
ObjectNode data = JacksonUtil.OBJECT_MAPPER.createObjectNode();
data.put("method", deviceRpcCallMsg.getRequestMsg().getMethod());
data.put("params", deviceRpcCallMsg.getRequestMsg().getParams());
TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, null, metaData,
TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(data));
tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.debug("Successfully send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}",
device, deviceRpcCallMsg);
}
@Override
public void onFailure(Throwable t) {
log.debug("Failed to send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}",
device, deviceRpcCallMsg, t);
}
});
} catch (JsonProcessingException | IllegalArgumentException e) {
log.warn("[{}] Failed to push TO_SERVER_RPC_REQUEST to rule engine. deviceRpcCallMsg {}", deviceId, deviceRpcCallMsg, e);
}
return Futures.immediateFuture(null);
}
public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent) {
DeviceId deviceId = new DeviceId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
@ -413,11 +463,9 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor {
private DownlinkMsg convertRpcCallEventToDownlink(EdgeEvent edgeEvent) {
log.trace("Executing convertRpcCallEventToDownlink, edgeEvent [{}]", edgeEvent);
DeviceRpcCallMsg deviceRpcCallMsg =
deviceMsgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody());
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDeviceRpcCallMsg(deviceRpcCallMsg)
.addDeviceRpcCallMsg(deviceMsgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody()))
.build();
}

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

@ -287,12 +287,8 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor {
List<String> attributeNames = attributeDeleteMsg.getAttributeNamesList();
attributesService.removeAll(tenantId, entityId, scope, attributeNames);
if (EntityType.DEVICE.name().equals(entityType)) {
Set<AttributeKey> attributeKeys = new HashSet<>();
for (String attributeName : attributeNames) {
attributeKeys.add(new AttributeKey(scope, attributeName));
}
tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(
tenantId, (DeviceId) entityId, attributeKeys), new TbQueueCallback() {
tenantId, (DeviceId) entityId, scope, attributeNames), new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
futureToSet.set(null);

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

@ -22,33 +22,50 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import java.util.List;
import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
@Service
@AllArgsConstructor
public class DefaultTbAssetService extends AbstractTbEntityService implements TbAssetService {
private final AssetService assetService;
private final TbAssetProfileCache assetProfileCache;
@Override
public Asset save(Asset asset, User user) throws Exception {
ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = asset.getTenantId();
try {
if (TB_SERVICE_QUEUE.equals(asset.getType())) {
throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (asset.getAssetProfileId() != null) {
AssetProfile assetProfile = assetProfileCache.get(tenantId, asset.getAssetProfileId());
if (assetProfile != null && TB_SERVICE_QUEUE.equals(assetProfile.getName())) {
throw new ThingsboardException("Unable to save asset with profile " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
autoCommit(user, savedAsset.getId());
notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedAsset.getId(), savedAsset,
asset.getCustomerId(), actionType, user);
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedAsset.getId(),
asset.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
return savedAsset;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET), asset, actionType, user, e);
@ -65,7 +82,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
assetService.deleteAsset(tenantId, assetId);
notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(),
ActionType.DELETED, relatedEdgeIds, user, assetId.toString());
tbClusterService.broadcastEntityStateChangeEvent(tenantId, assetId, ComponentLifecycleEvent.DELETED);
return removeAlarmsByEntityId(tenantId, assetId);
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET), ActionType.DELETED, user, e,

37
application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2022 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.install;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.NoSqlAnyDaoNonCloud;
/*
* Create keyspace for Cassandra NoSQL database for non-cloud deployment.
* For cloud service like Astra DBaas admin have to create keyspace manually on cloud UI.
* Then create tokens with database admin role and put it on Thingsboard parameters.
* Without this service cloud DB will end up with exception like
* UnauthorizedException: Missing correct permission on thingsboard
* */
@Service
@NoSqlAnyDaoNonCloud
@Profile("install")
public class CassandraKeyspaceService extends CassandraAbstractDatabaseSchemaService
implements NoSqlKeyspaceService {
public CassandraKeyspaceService() {
super("schema-keyspace.cql");
}
}

26
application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2022 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.install;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
@Component
@Profile("install")
public class DbUpgradeExecutorService extends DbCallbackExecutorService {
}

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

@ -82,6 +82,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
@ -167,6 +168,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private QueueService queueService;
@Autowired
private JwtSettingsService jwtSettingsService;
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@ -263,6 +267,16 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings);
}
@Override
public void createRandomJwtSettings() throws Exception {
jwtSettingsService.createRandomJwtSettings();
}
@Override
public void saveLegacyYmlSettings() throws Exception {
jwtSettingsService.saveLegacyYmlSettings();
}
@Override
public void createOAuth2Templates() throws Exception {
installScripts.createOAuth2Templates();
@ -656,4 +670,5 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
queueService.saveQueue(sequentialByOriginatorQueue);
}
}
}

19
application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java

@ -0,0 +1,19 @@
/**
* Copyright © 2016-2022 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.install;
public interface NoSqlKeyspaceService extends DatabaseSchemaService {
}

68
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.service.install;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -32,14 +34,14 @@ import org.thingsboard.server.common.data.queue.ProcessingStrategyType;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.dao.asset.AssetDao;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.sql.tenant.TenantRepository;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration;
@ -56,7 +58,9 @@ import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO;
@ -106,11 +110,14 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
@Autowired
private TenantService tenantService;
@Autowired
private TenantRepository tenantRepository;
@Autowired
private DeviceService deviceService;
@Autowired
private AssetService assetService;
private AssetDao assetDao;
@Autowired
private DeviceProfileService deviceProfileService;
@ -129,10 +136,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
private TbRuleEngineQueueConfigService queueConfig;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private TenantProfileService tenantProfileService;
private DbUpgradeExecutorService dbUpgradeExecutor;
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
@ -620,26 +624,45 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.1", "schema_update_before.sql");
loadSql(schemaUpdateFile, conn);
conn.createStatement().execute("DELETE FROM asset a WHERE NOT exists(SELECT id FROM tenant WHERE id = a.tenant_id);");
log.info("Creating default asset profiles...");
PageLink pageLink = new PageLink(100);
PageData<Tenant> pageData;
PageLink pageLink = new PageLink(1000);
PageData<TenantId> tenantIds;
do {
pageData = tenantService.findTenants(pageLink);
for (Tenant tenant : pageData.getData()) {
List<EntitySubtype> assetTypes = assetService.findAssetTypesByTenantId(tenant.getId()).get();
try {
assetProfileService.createDefaultAssetProfile(tenant.getId());
} catch (Exception e) {
}
for (EntitySubtype assetType : assetTypes) {
List<ListenableFuture<?>> futures = new ArrayList<>();
tenantIds = tenantService.findTenantsIds(pageLink);
for (TenantId tenantId : tenantIds.getData()) {
futures.add(dbUpgradeExecutor.submit(() -> {
try {
assetProfileService.findOrCreateAssetProfile(tenant.getId(), assetType.getType());
} catch (Exception e) {
}
assetProfileService.createDefaultAssetProfile(tenantId);
} catch (Exception e) {}
}));
}
Futures.allAsList(futures).get();
pageLink = pageLink.nextPageLink();
} while (tenantIds.hasNext());
pageLink = new PageLink(1000);
PageData<TbPair<UUID, String>> pairs;
do {
List<ListenableFuture<?>> futures = new ArrayList<>();
pairs = assetDao.getAllAssetTypes(pageLink);
for (TbPair<UUID, String> pair : pairs.getData()) {
TenantId tenantId = new TenantId(pair.getFirst());
String assetType = pair.getSecond();
if (!"default".equals(assetType)) {
futures.add(dbUpgradeExecutor.submit(() -> {
try {
assetProfileService.findOrCreateAssetProfile(tenantId, assetType);
} catch (Exception e) {}
}));
}
}
Futures.allAsList(futures).get();
pageLink = pageLink.nextPageLink();
} while (pageData.hasNext());
} while (pairs.hasNext());
log.info("Updating asset profiles...");
conn.createStatement().execute("call update_asset_profiles()");
@ -728,5 +751,4 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
return queue;
}
}

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

@ -23,6 +23,10 @@ public interface SystemDataLoaderService {
void createAdminSettings() throws Exception;
void createRandomJwtSettings() throws Exception;
void saveLegacyYmlSettings() throws Exception;
void createOAuth2Templates() throws Exception;
void loadSystemWidgets() throws Exception;

9
application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java

@ -73,6 +73,15 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
log.info("Clear cache to upgrade from version 3.3.4 to 3.4.0 ...");
clearAll();
break;
case "3.4.1":
log.info("Clear cache to upgrade from version 3.4.1 to 3.4.2 ...");
clearCacheByName("assets");
clearCacheByName("repositorySettings");
break;
case "3.4.2":
log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ...");
clearCacheByName("repositorySettings");
break;
default:
//Do nothing, since cache cleanup is optional.
}

14
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -64,6 +64,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfi
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.audit.AuditLogDao;
import org.thingsboard.server.dao.edge.EdgeEventDao;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
@ -142,6 +143,9 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private AuditLogDao auditLogDao;
@Autowired
private EdgeEventDao edgeEventDao;
@Override
public void updateData(String fromVersion) throws Exception {
switch (fromVersion) {
@ -181,14 +185,22 @@ public class DefaultDataUpdateService implements DataUpdateService {
}
break;
case "3.4.1":
log.info("Updating data from version 3.4.1 to 3.4.2 ...");
systemDataLoaderService.saveLegacyYmlSettings();
boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false);
if (!skipAuditLogsMigration) {
log.info("Updating data from version 3.4.1 to 3.4.2 ...");
log.info("Starting audit logs migration. Can be skipped with TB_SKIP_AUDIT_LOGS_MIGRATION env variable set to true");
auditLogDao.migrateAuditLogs();
} else {
log.info("Skipping audit logs migration");
}
boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false);
if (!skipEdgeEventsMigration) {
log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
edgeEventDao.migrateEdgeEvents();
} else {
log.info("Skipping edge events migration");
}
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);

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

@ -181,10 +181,11 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService {
private void update(TenantId tenantId, DeviceProfile deviceProfile, OtaPackageType otaPackageType) {
Consumer<Device> updateConsumer;
OtaPackageId packageId = OtaPackageUtil.getOtaPackageId(deviceProfile, otaPackageType);
if (deviceProfile.getFirmwareId() != null) {
if (packageId != null) {
long ts = System.currentTimeMillis();
updateConsumer = d -> send(d.getTenantId(), d.getId(), deviceProfile.getFirmwareId(), ts, otaPackageType);
updateConsumer = d -> send(d.getTenantId(), d.getId(), packageId, ts, otaPackageType);
} else {
updateConsumer = d -> remove(d, otaPackageType);
}
@ -360,9 +361,7 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService {
@Override
public void onSuccess(@Nullable Void tmp) {
log.trace("[{}] Success remove target {} attributes!", device.getId(), otaPackageType);
Set<AttributeKey> keysToNotify = new HashSet<>();
attributesKeys.forEach(key -> keysToNotify.add(new AttributeKey(DataConstants.SHARED_SCOPE, key)));
tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(device.getTenantId(), device.getId(), keysToNotify), null);
tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(device.getTenantId(), device.getId(), DataConstants.SHARED_SCOPE, attributesKeys), null);
}
@Override

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

@ -88,10 +88,8 @@ public class DefaultTbClusterService implements TbClusterService {
@Value("${cluster.stats.enabled:false}")
private boolean statsEnabled;
@Value("${edges.enabled}")
@Value("${edges.enabled:true}")
protected boolean edgesEnabled;
@Value("${service.type:monolith}")
private String serviceType;
private final AtomicInteger toCoreMsgs = new AtomicInteger(0);
private final AtomicInteger toCoreNfs = new AtomicInteger(0);
@ -584,25 +582,27 @@ public class DefaultTbClusterService implements TbClusterService {
}
private void doSendQueueNotifications(ToRuleEngineNotificationMsg ruleEngineMsg, ToCoreNotificationMsg coreMsg, ToTransportMsg transportMsg) {
Set<TransportProtos.ServiceInfo> tbRuleEngineServices = partitionService.getAllServices(ServiceType.TB_RULE_ENGINE);
for (TransportProtos.ServiceInfo ruleEngineService : tbRuleEngineServices) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngineService.getServiceId());
Set<String> tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE);
Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);
Set<String> tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT);
// No need to push notifications twice
tbTransportServices.removeAll(tbCoreServices);
tbCoreServices.removeAll(tbRuleEngineServices);
for (String ruleEngineServiceId : tbRuleEngineServices) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngineServiceId);
producerProvider.getRuleEngineNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), ruleEngineMsg), null);
toRuleEngineNfs.incrementAndGet();
}
if (!serviceType.equals("monolith")) {
Set<TransportProtos.ServiceInfo> tbCoreServices = partitionService.getAllServices(ServiceType.TB_CORE);
for (TransportProtos.ServiceInfo coreService : tbCoreServices) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, coreService.getServiceId());
producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), coreMsg), null);
toCoreNfs.incrementAndGet();
}
Set<TransportProtos.ServiceInfo> tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT);
for (TransportProtos.ServiceInfo transportService : tbTransportServices) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportService.getServiceId());
producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), null);
toTransportNfs.incrementAndGet();
}
for (String coreServiceId : tbCoreServices) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, coreServiceId);
producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), coreMsg), null);
toCoreNfs.incrementAndGet();
}
for (String transportServiceId : tbTransportServices) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId);
producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), null);
toTransportNfs.incrementAndGet();
}
}
}

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

@ -38,6 +38,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -152,8 +153,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
OtaPackageStateService firmwareStateService,
GitVersionControlQueueService vcQueueService,
PartitionService partitionService,
Optional<JwtSettingsService> jwtSettingsService,
NotificationSchedulerService notificationSchedulerService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer());
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();
@ -520,7 +522,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
subscriptionManagerService.onAttributesDelete(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), proto.getKeysList(), callback);
proto.getScope(), proto.getKeysList(), proto.getNotifyDevice(), callback);
} else if (msg.hasTsDelete()) {
TbTimeSeriesDeleteProto proto = msg.getTsDelete();
subscriptionManagerService.onTimeSeriesDelete(

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

@ -70,6 +70,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -126,7 +127,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, QueueService queueService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty());
this.statisticsService = statisticsService;
this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;
this.submitStrategyFactory = submitStrategyFactory;

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

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -76,11 +77,13 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final PartitionService partitionService;
protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
protected final Optional<JwtSettingsService> jwtSettingsService;
public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer) {
PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, Optional<JwtSettingsService> jwtSettingsService) {
this.actorContext = actorContext;
this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache;
@ -89,6 +92,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
this.apiUsageStateService = apiUsageStateService;
this.partitionService = partitionService;
this.nfConsumer = nfConsumer;
this.jwtSettingsService = jwtSettingsService;
}
public void init(String mainConsumerThreadName, String nfConsumerThreadName) {
@ -172,12 +176,17 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
}
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tenantProfileCache.evict(componentLifecycleMsg.getTenantId());
partitionService.removeTenant(componentLifecycleMsg.getTenantId());
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete((TenantId) componentLifecycleMsg.getEntityId());
if (TenantId.SYS_TENANT_ID.equals(componentLifecycleMsg.getTenantId())) {
jwtSettingsService.ifPresent(JwtSettingsService::reloadJwtSettings);
return;
} else {
tenantProfileCache.evict(componentLifecycleMsg.getTenantId());
partitionService.removeTenant(componentLifecycleMsg.getTenantId());
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete((TenantId) componentLifecycleMsg.getEntityId());
}
}
} else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));

14
application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java

@ -19,6 +19,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse;
@ -27,6 +30,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
@ -52,6 +56,7 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi
private final PartitionService partitionService;
private final TbClusterService clusterService;
private final TbServiceInfoProvider serviceInfoProvider;
private final RpcService rpcService;
private final ConcurrentMap<UUID, Consumer<FromDeviceRpcResponse>> toDeviceRpcRequests = new ConcurrentHashMap<>();
@ -61,10 +66,12 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi
public DefaultTbRuleEngineRpcService(PartitionService partitionService,
TbClusterService clusterService,
TbServiceInfoProvider serviceInfoProvider) {
TbServiceInfoProvider serviceInfoProvider,
RpcService rpcService) {
this.partitionService = partitionService;
this.clusterService = clusterService;
this.serviceInfoProvider = serviceInfoProvider;
this.rpcService = rpcService;
}
@Autowired(required = false)
@ -119,6 +126,11 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi
});
}
@Override
public Rpc findRpcById(TenantId tenantId, RpcId id) {
return rpcService.findById(tenantId, id);
}
@Override
public void processRpcResponseFromDevice(FromDeviceRpcResponse response) {
log.trace("[{}] Received response to server-side RPC request from Core RPC Service", response.getId());

8
application/src/main/java/org/thingsboard/server/service/script/RuleNodeMvelScriptEngine.java → application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java

@ -23,7 +23,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.RuleNodeScriptFactory;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
@ -42,9 +42,9 @@ import java.util.stream.Collectors;
@Slf4j
public class RuleNodeMvelScriptEngine extends RuleNodeScriptEngine<MvelInvokeService, Object> {
public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeService, Object> {
public RuleNodeMvelScriptEngine(TenantId tenantId, MvelInvokeService scriptInvokeService, String script, String... argNames) {
public RuleNodeTbelScriptEngine(TenantId tenantId, TbelInvokeService scriptInvokeService, String script, String... argNames) {
super(tenantId, scriptInvokeService, script, argNames);
}
@ -64,7 +64,7 @@ public class RuleNodeMvelScriptEngine extends RuleNodeScriptEngine<MvelInvokeSer
List<TbMsg> res = new ArrayList<>();
for (Object resObject : (Collection) result) {
if (resObject instanceof Map) {
res.add(unbindMsg((Map) result, msg));
res.add(unbindMsg((Map) resObject, msg));
} else {
return wrongResultType(resObject);
}

71
application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java

@ -0,0 +1,71 @@
/**
* Copyright © 2016-2022 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 io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.Optional;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@Service
public class DefaultTokenOutdatingService implements TokenOutdatingService {
private final TbTransactionalCache<String, Long> cache;
private final JwtTokenFactory tokenFactory;
public DefaultTokenOutdatingService(@Qualifier("UsersSessionInvalidation") TbTransactionalCache<String, Long> cache, JwtTokenFactory tokenFactory) {
this.cache = cache;
this.tokenFactory = tokenFactory;
}
@EventListener(classes = UserAuthDataChangedEvent.class)
public void onUserAuthDataChanged(UserAuthDataChangedEvent event) {
if (StringUtils.hasText(event.getId())) {
cache.put(event.getId(), event.getTs());
}
}
@Override
public boolean isOutdated(JwtToken token, UserId userId) {
Claims claims = tokenFactory.parseTokenClaims(token).getBody();
long issueTime = claims.getIssuedAt().getTime();
String sessionId = claims.get("sessionId", String.class);
if (isTokenOutdated(issueTime, userId.toString())){
return true;
} else {
return sessionId != null && isTokenOutdated(issueTime, sessionId);
}
}
private Boolean isTokenOutdated(long issueTime, String sessionId) {
return Optional.ofNullable(cache.get(sessionId)).map(outdatageTime -> isTokenOutdated(issueTime, outdatageTime.get())).orElse(false);
}
private boolean isTokenOutdated(long issueTime, Long outdatageTime) {
return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime);
}
}

60
application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java

@ -15,67 +15,11 @@
*/
package org.thingsboard.server.service.security.auth;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import javax.annotation.PostConstruct;
import java.util.Optional;
public interface TokenOutdatingService {
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
boolean isOutdated(JwtToken token, UserId userId);
@Service
@RequiredArgsConstructor
public class TokenOutdatingService {
private final CacheManager cacheManager;
private final JwtTokenFactory tokenFactory;
private final JwtSettings jwtSettings;
private Cache usersUpdateTimeCache;
@PostConstruct
protected void initCache() {
usersUpdateTimeCache = cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE);
}
@EventListener(classes = UserAuthDataChangedEvent.class)
public void onUserAuthDataChanged(UserAuthDataChangedEvent event) {
usersUpdateTimeCache.put(toKey(event.getUserId()), event.getTs());
}
public boolean isOutdated(JwtToken token, UserId userId) {
Claims claims = tokenFactory.parseTokenClaims(token).getBody();
long issueTime = claims.getIssuedAt().getTime();
return Optional.ofNullable(usersUpdateTimeCache.get(toKey(userId), Long.class))
.map(outdatageTime -> {
if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) {
return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime);
} else {
/*
* Means that since the outdating has passed more than
* the lifetime of refresh token (the longest lived)
* and there is no need to store outdatage time anymore
* as all the tokens issued before the outdatage time
* are now expired by themselves
* */
usersUpdateTimeCache.evict(toKey(userId));
return false;
}
})
.orElse(false);
}
private String toKey(UserId userId) {
return userId.getId().toString();
}
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java

@ -20,8 +20,8 @@ import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.thingsboard.server.service.security.auth.TokenOutdatingService;
import org.thingsboard.server.service.security.auth.JwtAuthenticationToken;
import org.thingsboard.server.service.security.auth.TokenOutdatingService;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;

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

@ -33,11 +33,11 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.auth.TokenOutdatingService;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
import org.thingsboard.server.service.security.auth.TokenOutdatingService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -66,7 +66,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
} else {
securityUser = authenticateByPublicId(principal.getValue());
}
securityUser.setSessionId(unsafeUser.getSessionId());
if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) {
throw new CredentialsExpiredException("Token is outdated");
}

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

@ -0,0 +1,162 @@
/**
* Copyright © 2016-2022 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.jwt.settings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultJwtSettingsService implements JwtSettingsService {
@Lazy
private final AdminSettingsService adminSettingsService;
@Lazy
private final Optional<TbClusterService> tbClusterService;
private final JwtSettingsValidator jwtSettingsValidator;
@Value("${security.jwt.tokenExpirationTime:9000}")
private Integer tokenExpirationTime;
@Value("${security.jwt.refreshTokenExpTime:604800}")
private Integer refreshTokenExpTime;
@Value("${security.jwt.tokenIssuer:thingsboard.io}")
private String tokenIssuer;
@Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}")
private String tokenSigningKey;
private volatile JwtSettings jwtSettings = null; //lazy init
/**
* Create JWT admin settings is intended to be called from Install scripts only
*/
@Override
public void createRandomJwtSettings() {
if (getJwtSettingsFromDb() == null) {
log.info("Creating JWT admin settings...");
this.jwtSettings = getJwtSettingsFromYml();
if (isSigningKeyDefault(jwtSettings)) {
this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
}
saveJwtSettings(jwtSettings);
} else {
log.info("Skip creating JWT admin settings because they already exist.");
}
}
/**
* Create JWT admin settings is intended to be called from Upgrade scripts only
*/
@Override
public void saveLegacyYmlSettings() {
log.info("Saving legacy JWT admin settings from YML...");
if (getJwtSettingsFromDb() == null) {
saveJwtSettings(getJwtSettingsFromYml());
}
}
@Override
public JwtSettings saveJwtSettings(JwtSettings jwtSettings) {
jwtSettingsValidator.validate(jwtSettings);
final AdminSettings adminJwtSettings = mapJwtToAdminSettings(jwtSettings);
final AdminSettings existedSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
if (existedSettings != null) {
adminJwtSettings.setId(existedSettings.getId());
}
log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored");
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings);
tbClusterService.ifPresent(cs -> cs.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED));
return reloadJwtSettings();
}
@Override
public JwtSettings reloadJwtSettings() {
return getJwtSettings(true);
}
@Override
public JwtSettings getJwtSettings() {
return getJwtSettings(false);
}
public JwtSettings getJwtSettings(boolean forceReload) {
if (this.jwtSettings == null || forceReload) {
synchronized (this) {
if (this.jwtSettings == null || forceReload) {
JwtSettings result = getJwtSettingsFromDb();
if (result == null) {
result = getJwtSettingsFromYml();
log.warn("Loading the JWT settings from YML since there are no settings in DB. Looks like the upgrade script was not applied.");
}
if (isSigningKeyDefault(result)) {
log.warn("WARNING: The platform is configured to use default JWT Signing Key. " +
"This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " +
"Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.");
}
this.jwtSettings = result;
}
}
}
return this.jwtSettings;
}
private JwtSettings getJwtSettingsFromYml() {
return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey);
}
private JwtSettings getJwtSettingsFromDb() {
AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
return adminJwtSettings != null ? mapAdminToJwtSettings(adminJwtSettings) : null;
}
private JwtSettings mapAdminToJwtSettings(AdminSettings adminSettings) {
Objects.requireNonNull(adminSettings, "adminSettings for JWT is null");
return JacksonUtil.treeToValue(adminSettings.getJsonValue(), JwtSettings.class);
}
private AdminSettings mapJwtToAdminSettings(JwtSettings jwtSettings) {
Objects.requireNonNull(jwtSettings, "jwtSettings is null");
AdminSettings adminJwtSettings = new AdminSettings();
adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID);
adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY);
adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings));
return adminJwtSettings;
}
private boolean isSigningKeyDefault(JwtSettings settings) {
return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey());
}
}

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

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2022 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.jwt.settings;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.util.Arrays;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.Base64;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
@Override
public void validate(JwtSettings jwtSettings) {
if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) {
throw new DataValidationException("JWT token issuer should be specified!");
}
if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) {
throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!");
}
if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) {
throw new DataValidationException("JWT token expiration time should be at least 1 minute!");
}
if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) {
throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!");
}
if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) {
throw new DataValidationException("JWT token signing key should be specified!");
}
byte[] decodedKey;
try {
decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey());
} catch (Exception e) {
throw new DataValidationException("JWT token signing key should be a valid Base64 encoded string! " + e.getMessage());
}
if (Arrays.isNullOrEmpty(decodedKey)) {
throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!");
}
if (decodedKey.length * Byte.SIZE < 256 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) {
throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!");
}
System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory
}
}

29
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java → application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java

@ -13,26 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.jwt;
package org.thingsboard.server.service.security.auth.jwt.settings;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.common.data.security.model.JwtSettings;
@Component
public class RefreshTokenRepository {
/**
* During Install or upgrade the validation is suppressed to keep existing data
* */
private final JwtTokenFactory tokenFactory;
@Primary
@Profile("install")
@Component
@RequiredArgsConstructor
public class InstallJwtSettingsValidator implements JwtSettingsValidator {
@Autowired
public RefreshTokenRepository(final JwtTokenFactory tokenFactory) {
this.tokenFactory = tokenFactory;
}
@Override
public void validate(JwtSettings jwtSettings) {
public JwtToken requestRefreshToken(SecurityUser user) {
return tokenFactory.createRefreshToken(user);
}
}

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

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2022 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.jwt.settings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
public interface JwtSettingsService {
String ADMIN_SETTINGS_JWT_KEY = "jwt";
String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey";
JwtSettings getJwtSettings();
JwtSettings reloadJwtSettings();
void createRandomJwtSettings();
void saveLegacyYmlSettings();
JwtSettings saveJwtSettings(JwtSettings jwtSettings);
}

23
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 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.jwt.settings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
public interface JwtSettingsValidator {
void validate(JwtSettings jwtSettings);
}

6
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java

@ -47,6 +47,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -81,6 +82,9 @@ public abstract class AbstractOAuth2ClientMapper {
@Autowired
private InstallScripts installScripts;
@Autowired
private TbUserService tbUserService;
@Autowired
protected TbTenantProfileCache tenantProfileCache;
@ -146,7 +150,7 @@ public abstract class AbstractOAuth2ClientMapper {
user.setAdditionalInfo(additionalInfo);
user = userService.saveUser(user);
user = tbUserService.save(tenantId, customerId, user, false, null, null);
if (config.isActivateUser()) {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode(""));

25
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -25,14 +25,15 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -54,7 +55,6 @@ import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAut
public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final OAuth2ClientMapperProvider oauth2ClientMapperProvider;
private final OAuth2Service oAuth2Service;
private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
@ -63,14 +63,12 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
@Autowired
public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory,
final RefreshTokenRepository refreshTokenRepository,
final OAuth2ClientMapperProvider oauth2ClientMapperProvider,
final OAuth2Service oAuth2Service,
final OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository,
final SystemSecurityService systemSecurityService) {
this.tokenFactory = tokenFactory;
this.refreshTokenRepository = refreshTokenRepository;
this.oauth2ClientMapperProvider = oauth2ClientMapperProvider;
this.oAuth2Service = oAuth2Service;
this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService;
@ -106,11 +104,11 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
registration);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
clearAuthenticationAttributes(request, response);
getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
JwtPair tokenPair = tokenFactory.createTokenPair(securityUser);
getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(baseUrl, tokenPair));
systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, registration.getName(), null);
} catch (Exception e) {
log.debug("Error occurred during processing authentication success result. " +
"request [{}], response [{}], authentication [{}]", request, response, authentication, e);
@ -130,4 +128,13 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
super.clearAuthenticationAttributes(request);
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
String getRedirectUrl(String baseUrl, JwtPair tokenPair) {
if (baseUrl.indexOf("?") > 0) {
baseUrl += "&";
} else {
baseUrl += "/?";
}
return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken();
}
}

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

@ -25,9 +25,8 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -45,13 +44,12 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory;
private final TwoFaConfigManager twoFaConfigManager;
private final RefreshTokenRepository refreshTokenRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
JwtTokenPair tokenPair = new JwtTokenPair();
JwtPair tokenPair = new JwtPair();
if (authentication instanceof MfaAuthenticationToken) {
int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)
@ -62,8 +60,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
tokenPair.setRefreshToken(null);
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
} else {
tokenPair.setToken(tokenFactory.createAccessJwtToken(securityUser).getToken());
tokenPair.setRefreshToken(refreshTokenRepository.requestRefreshToken(securityUser).getToken());
tokenPair = tokenFactory.createTokenPair(securityUser);
}
response.setStatus(HttpStatus.OK.value());

10
application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.UserId;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -31,6 +32,7 @@ public class SecurityUser extends User {
private Collection<GrantedAuthority> authorities;
private boolean enabled;
private UserPrincipal userPrincipal;
private String sessionId;
public SecurityUser() {
super();
@ -44,6 +46,7 @@ public class SecurityUser extends User {
super(user);
this.enabled = enabled;
this.userPrincipal = userPrincipal;
this.sessionId = UUID.randomUUID().toString();
}
public Collection<GrantedAuthority> getAuthorities() {
@ -71,4 +74,11 @@ public class SecurityUser extends User {
this.userPrincipal = userPrincipal;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
}

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

@ -24,8 +24,8 @@ import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
@ -35,9 +35,9 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -49,6 +49,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtTokenFactory {
@ -60,13 +61,9 @@ public class JwtTokenFactory {
private static final String IS_PUBLIC = "isPublic";
private static final String TENANT_ID = "tenantId";
private static final String CUSTOMER_ID = "customerId";
private static final String SESSION_ID = "sessionId";
private final JwtSettings settings;
@Autowired
public JwtTokenFactory(JwtSettings settings) {
this.settings = settings;
}
private final JwtSettingsService jwtSettingsService;
/**
* Factory method for issuing new JWT Tokens.
@ -79,7 +76,7 @@ public class JwtTokenFactory {
UserPrincipal principal = securityUser.getUserPrincipal();
JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()), settings.getTokenExpirationTime());
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()), jwtSettingsService.getJwtSettings().getTokenExpirationTime());
jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName())
.claim(LAST_NAME, securityUser.getLastName())
.claim(ENABLED, securityUser.isEnabled())
@ -119,6 +116,9 @@ public class JwtTokenFactory {
if (customerId != null) {
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
}
if (claims.get(SESSION_ID, String.class) != null) {
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
}
UserPrincipal principal;
if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) {
@ -138,7 +138,7 @@ public class JwtTokenFactory {
public JwtToken createRefreshToken(SecurityUser securityUser) {
UserPrincipal principal = securityUser.getUserPrincipal();
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), settings.getRefreshTokenExpTime())
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), jwtSettingsService.getJwtSettings().getRefreshTokenExpTime())
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID)
.setId(UUID.randomUUID().toString()).compact();
@ -161,6 +161,9 @@ public class JwtTokenFactory {
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
securityUser.setUserPrincipal(principal);
if (claims.get(SESSION_ID, String.class) != null) {
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
}
return securityUser;
}
@ -183,35 +186,38 @@ public class JwtTokenFactory {
Claims claims = Jwts.claims().setSubject(principal.getValue());
claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(SCOPES, scopes);
if (securityUser.getSessionId() != null) {
claims.put(SESSION_ID, securityUser.getSessionId());
}
ZonedDateTime currentTime = ZonedDateTime.now();
return Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
.setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
.setIssuedAt(Date.from(currentTime.toInstant()))
.setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey());
.signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey());
}
public Jws<Claims> parseTokenClaims(JwtToken token) {
try {
return Jwts.parser()
.setSigningKey(settings.getTokenSigningKey())
.setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey())
.parseClaimsJws(token.getToken());
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) {
log.debug("Invalid JWT Token", ex);
throw new BadCredentialsException("Invalid JWT token: ", ex);
} catch (ExpiredJwtException expiredEx) {
} catch (SignatureException | ExpiredJwtException expiredEx) {
log.debug("JWT Token is expired", expiredEx);
throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx);
}
}
public JwtTokenPair createTokenPair(SecurityUser securityUser) {
public JwtPair createTokenPair(SecurityUser securityUser) {
JwtToken accessToken = createAccessJwtToken(securityUser);
JwtToken refreshToken = createRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
return new JwtPair(accessToken.getToken(), refreshToken.getToken());
}
}

41
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -263,6 +263,11 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
@Override
public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e) {
logLoginAction(user, authenticationDetails, actionType, null, e);
}
@Override
public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, String provider, Exception e) {
String clientAddress = "Unknown";
String browser = "Unknown";
String os = "Unknown";
@ -278,38 +283,38 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
browser += " " + userAgent.userAgent.major;
if (userAgent.userAgent.minor != null) {
browser += "." + userAgent.userAgent.minor;
if (userAgent.userAgent.patch != null) {
browser += "." + userAgent.userAgent.patch;
}
if (userAgent.userAgent.patch != null) {
browser += "." + userAgent.userAgent.patch;
}
}
}
if (userAgent.os != null) {
os = userAgent.os.family;
if (userAgent.os.major != null) {
os += " " + userAgent.os.major;
if (userAgent.os.minor != null) {
os += "." + userAgent.os.minor;
if (userAgent.os.patch != null) {
os += "." + userAgent.os.patch;
if (userAgent.os.patchMinor != null) {
os += "." + userAgent.os.patchMinor;
}
}
if (userAgent.os != null) {
os = userAgent.os.family;
if (userAgent.os.major != null) {
os += " " + userAgent.os.major;
if (userAgent.os.minor != null) {
os += "." + userAgent.os.minor;
if (userAgent.os.patch != null) {
os += "." + userAgent.os.patch;
if (userAgent.os.patchMinor != null) {
os += "." + userAgent.os.patchMinor;
}
}
}
}
if (userAgent.device != null) {
device = userAgent.device.family;
}
}
if (userAgent.device != null) {
device = userAgent.device.family;
}
}
}
if (actionType == ActionType.LOGIN && e == null) {
userService.setLastLoginTs(user.getTenantId(), user.getId());
}
auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device);
user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device, provider);
}
private static boolean isPositiveInteger(Integer val) {

1
application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java

@ -44,4 +44,5 @@ public interface SystemSecurityService {
void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e);
void logLoginAction(User user, Object authenticationDetails, ActionType actionType, String provider, Exception e);
}

24
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -24,6 +24,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -121,7 +122,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
new EntityKey(EntityKeyType.TIME_SERIES, INACTIVITY_TIMEOUT),
new EntityKey(EntityKeyType.TIME_SERIES, ACTIVITY_STATE),
new EntityKey(EntityKeyType.TIME_SERIES, LAST_CONNECT_TIME),
new EntityKey(EntityKeyType.TIME_SERIES, LAST_DISCONNECT_TIME));
new EntityKey(EntityKeyType.TIME_SERIES, LAST_DISCONNECT_TIME),
new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT));
private static final List<EntityKey> PERSISTENT_ATTRIBUTE_KEYS = Arrays.asList(
new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_ACTIVITY_TIME),
@ -152,14 +154,21 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
@Value("${state.defaultInactivityTimeoutInSec}")
@Getter
@Setter
private long defaultInactivityTimeoutInSec;
@Value("#{${state.defaultInactivityTimeoutInSec} * 1000}")
@Getter
@Setter
private long defaultInactivityTimeoutMs;
@Value("${state.defaultStateCheckIntervalInSec}")
@Getter
private int defaultStateCheckIntervalInSec;
@Value("${state.persistToTelemetry:false}")
@Getter
@Setter
private boolean persistToTelemetry;
@Value("${state.initFetchPackSize:50000}")
@ -540,7 +549,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private ListenableFuture<DeviceStateData> transformInactivityTimeout(ListenableFuture<DeviceStateData> future) {
return Futures.transformAsync(future, deviceStateData -> {
if (!persistToTelemetry || deviceStateData.getState().getInactivityTimeout() != TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec)) {
if (!persistToTelemetry || deviceStateData.getState().getInactivityTimeout() != defaultInactivityTimeoutMs) {
return future; //fail fast
}
var attributesFuture = attributesService.find(TenantId.SYS_TENANT_ID, deviceStateData.getDeviceId(), SERVER_SCOPE, INACTIVITY_TIMEOUT);
@ -563,7 +572,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
try {
long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L);
long inactivityTimeout = getEntryValue(data, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec));
long inactivityTimeout = getEntryValue(data, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs);
//Actual active state by wall-clock will updated outside this method. This method is only for fetch persistent state
final boolean active = getEntryValue(data, ACTIVITY_STATE, false);
DeviceState deviceState = DeviceState.builder()
@ -634,10 +643,15 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
private DeviceStateData toDeviceStateData(EntityData ed, DeviceIdInfo deviceIdInfo) {
DeviceStateData toDeviceStateData(EntityData ed, DeviceIdInfo deviceIdInfo) {
long lastActivityTime = getEntryValue(ed, getKeyType(), LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getEntryValue(ed, getKeyType(), INACTIVITY_ALARM_TIME, 0L);
long inactivityTimeout = getEntryValue(ed, getKeyType(), INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec));
long inactivityTimeout = getEntryValue(ed, getKeyType(), INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs);
if (persistToTelemetry && inactivityTimeout == defaultInactivityTimeoutMs) {
log.trace("[{}] default value for inactivity timeout fetched {}, going to fetch inactivity timeout from attributes",
deviceIdInfo.getDeviceId(), inactivityTimeout);
inactivityTimeout = getEntryValue(ed, EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs);
}
//Actual active state by wall-clock will updated outside this method. This method is only for fetch persistent state
final boolean active = getEntryValue(ed, getKeyType(), ACTIVITY_STATE, false);
DeviceState deviceState = DeviceState.builder()

10
application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java

@ -374,7 +374,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
}
@Override
public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, TbCallback callback) {
public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, TbCallback callback) {
onLocalTelemetrySubUpdate(entityId,
s -> {
if (TbSubscriptionType.ATTRIBUTES.equals(s.getType())) {
@ -397,7 +397,13 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
return subscriptionUpdate;
}, false);
if (entityId.getEntityType() == EntityType.DEVICE) {
deleteDeviceInactivityTimeout(tenantId, entityId, keys);
if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)
|| TbAttributeSubscriptionScope.ANY_SCOPE.name().equalsIgnoreCase(scope)) {
deleteDeviceInactivityTimeout(tenantId, entityId, keys);
} else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId,
new DeviceId(entityId.getId()), scope, keys), null);
}
}
callback.onSuccess();
}

15
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -561,8 +562,18 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
if (queryResults != null) {
for (ReadTsKvQueryResult queryResult : queryResults) {
String queryKey = queriesKeys.get(queryResult.getQueryId());
entityData.getTimeseries().put(queryKey, queryResult.toTsValues());
lastTsMap.put(queryKey, queryResult.getLastEntryTs());
if (queryKey != null) {
TsValue[] tsValues = entityData.getTimeseries().get(queryKey);
if (tsValues == null) {
tsValues = queryResult.toTsValues();
} else {
tsValues = ArrayUtils.addAll(tsValues, queryResult.toTsValues());
}
entityData.getTimeseries().put(queryKey, tsValues);
} else {
log.warn("ReadTsKvQueryResult for {} {} has queryId not matching the initial query",
entityData.getEntityId().getEntityType(), entityData.getEntityId());
}
}
}
// Populate with empty values if no data found.

2
application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java

@ -41,7 +41,7 @@ public interface SubscriptionManagerService extends ApplicationListener<Partitio
void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, TbCallback callback);
void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, TbCallback empty);
void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, TbCallback empty);
void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List<String> keys, TbCallback callback);

5
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.subscription;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AbstractDataQuery;
import org.thingsboard.server.common.data.query.EntityData;
@ -197,6 +198,9 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
entityData.getTimeseries().forEach((k, v) -> {
long ts = Arrays.stream(v).map(TsValue::getTs).max(Long::compareTo).orElse(0L);
log.trace("[{}][{}] Updating key: {} with ts: {}", serviceId, cmdId, k, ts);
if (!Aggregation.NONE.equals(getCurrentAggregation()) && ts < endTs) {
ts = endTs;
}
keyStates.put(k, ts);
});
}
@ -247,4 +251,5 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
abstract void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues);
protected abstract Aggregation getCurrentAggregation();
}

6
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataPageLink;
@ -205,6 +206,11 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
}
}
@Override
protected Aggregation getCurrentAggregation() {
return Aggregation.NONE;
}
private void sendWsMsg(String sessionId, AlarmSubscriptionUpdate subscriptionUpdate) {
Alarm alarm = subscriptionUpdate.getAlarm();
AlarmId alarmId = alarm.getId();

6
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java

@ -19,6 +19,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -86,6 +87,11 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
}
}
@Override
protected Aggregation getCurrentAggregation() {
return (this.curTsCmd == null || this.curTsCmd.getAgg() == null) ? Aggregation.NONE : this.curTsCmd.getAgg();
}
private void sendLatestWsMsg(EntityId entityId, String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType) {
Map<String, TsValue> latestUpdate = new HashMap<>();
subscriptionUpdate.getData().forEach((k, v) -> {

3
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java

@ -276,7 +276,7 @@ public class TbSubscriptionUtils {
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build();
}
public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List<String> keys) {
public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice) {
TbAttributeDeleteProto.Builder builder = TbAttributeDeleteProto.newBuilder();
builder.setEntityType(entityId.getEntityType().name());
builder.setEntityIdMSB(entityId.getId().getMostSignificantBits());
@ -285,6 +285,7 @@ public class TbSubscriptionUtils {
builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits());
builder.setScope(scope);
builder.addAllKeys(keys);
builder.setNotifyDevice(notifyDevice);
SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder();
msgBuilder.setAttrDelete(builder);

5
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -126,6 +126,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return alarmService.findAlarmByIdAsync(tenantId, alarmId);
}
@Override
public Alarm findAlarmById(TenantId tenantId, AlarmId alarmId) {
return alarmService.findAlarmById(tenantId, alarmId);
}
@Override
public ListenableFuture<AlarmInfo> findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) {
return alarmService.findAlarmInfoByIdAsync(tenantId, alarmId);

18
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -269,14 +269,20 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override
public void deleteAndNotify(TenantId tenantId, EntityId entityId, String scope, List<String> keys, FutureCallback<Void> callback) {
checkInternalEntity(entityId);
deleteAndNotifyInternal(tenantId, entityId, scope, keys, callback);
deleteAndNotifyInternal(tenantId, entityId, scope, keys, false, callback);
}
@Override
public void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<String> keys, FutureCallback<Void> callback) {
public void deleteAndNotify(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, FutureCallback<Void> callback) {
checkInternalEntity(entityId);
deleteAndNotifyInternal(tenantId, entityId, scope, keys, notifyDevice, callback);
}
@Override
public void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, FutureCallback<Void> callback) {
ListenableFuture<List<String>> deleteFuture = attrService.removeAll(tenantId, entityId, scope, keys);
addVoidCallback(deleteFuture, callback);
addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, scope, keys));
addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice));
}
@Override
@ -374,11 +380,11 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
});
}
private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys) {
private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice) {
forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, TbCallback.EMPTY);
subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice, TbCallback.EMPTY);
}, () -> {
return TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys);
return TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys, notifyDevice);
});
}

2
application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java

@ -37,7 +37,7 @@ public interface InternalTelemetryService extends RuleEngineTelemetryService {
void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback);
void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<String> keys, FutureCallback<Void> callback);
void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, FutureCallback<Void> callback);
void deleteLatestInternal(TenantId tenantId, EntityId entityId, List<String> keys, FutureCallback<Void> callback);

22
application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java

@ -17,15 +17,22 @@ package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_COLUMN_FAMILY_NAME;
@TbCoreComponent
@Slf4j
@Service
@ConditionalOnExpression("${sql.ttl.edge_events.enabled:true} && ${sql.ttl.edge_events.edge_event_ttl:0} > 0")
public class EdgeEventsCleanUpService extends AbstractCleanUpService {
public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION =
@ -34,20 +41,29 @@ public class EdgeEventsCleanUpService extends AbstractCleanUpService {
@Value("${sql.ttl.edge_events.edge_events_ttl}")
private long ttl;
@Value("${sql.ttl.edge_events.enabled}")
@Value("${sql.edge_events.partition_size:168}")
private int partitionSizeInHours;
@Value("${sql.ttl.edge_events.enabled:true}")
private boolean ttlTaskExecutionEnabled;
private final EdgeEventService edgeEventService;
public EdgeEventsCleanUpService(PartitionService partitionService, EdgeEventService edgeEventService) {
private final SqlPartitioningRepository partitioningRepository;
public EdgeEventsCleanUpService(PartitionService partitionService, EdgeEventService edgeEventService, SqlPartitioningRepository partitioningRepository) {
super(partitionService);
this.edgeEventService = edgeEventService;
this.partitioningRepository = partitioningRepository;
}
@Scheduled(initialDelayString = RANDOM_DELAY_INTERVAL_MS_EXPRESSION, fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}")
public void cleanUp() {
long edgeEventsExpTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttl);
if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) {
edgeEventService.cleanupEvents(ttl);
edgeEventService.cleanupEvents(edgeEventsExpTime);
} else {
partitioningRepository.cleanupPartitionsCache(EDGE_EVENT_COLUMN_FAMILY_NAME, edgeEventsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
}

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

@ -107,11 +107,11 @@ plugins:
# Security parameters
security:
# JWT Token parameters
jwt:
jwt: # Since 3.4.2 values are persisted to the database during install or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN
tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours)
refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week)
refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week).
tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Base64 encoded
# Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator
user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}"
# Enable/disable case-sensitive username login
@ -148,7 +148,7 @@ ui:
# Help parameters
help:
# Base url for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.4.1}"
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.4.2}"
database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records
@ -200,6 +200,14 @@ cassandra:
username: "${CASSANDRA_USERNAME:}"
# Specify your password
password: "${CASSANDRA_PASSWORD:}"
# Astra DB connect https://astra.datastax.com/
cloud:
# /etc/thingsboard/astra/secure-connect-thingsboard.zip
secure_connect_bundle_path: "${CASSANDRA_CLOUD_SECURE_BUNDLE_PATH:}"
# DucitQPHMzPCBOZqFYexAfKk
client_id: "${CASSANDRA_CLOUD_CLIENT_ID:}"
# ZnF7FpuHp43FP5BzM+KY8wGmSb4Ql6BhT4Z7sOU13ze+gXQ-n7OkFpNuB,oACUIQObQnK0g4bSPoZhK5ejkcF9F.j6f64j71Sr.tiRe0Fsq2hPS1ZCGSfAaIgg63IydG
client_secret: "${CASSANDRA_CLOUD_CLIENT_SECRET:}"
# Cassandra cluster connection socket parameters #
socket:
@ -219,6 +227,7 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}"
ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}"
ts_key_value_ttl: "${TS_KV_TTL:0}"
buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
@ -265,6 +274,7 @@ sql:
batch_size: "${SQL_EDGE_EVENTS_BATCH_SIZE:1000}"
batch_max_delay: "${SQL_EDGE_EVENTS_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_EDGE_EVENTS_BATCH_STATS_PRINT_MS:10000}"
partition_size: "${SQL_EDGE_EVENTS_PARTITION_SIZE_HOURS:168}" # Number of hours to partition the events. The current value corresponds to one week.
audit_logs:
partition_size: "${SQL_AUDIT_LOGS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week
notifications:
@ -422,8 +432,9 @@ cache:
attributes:
timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}"
maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}"
usersUpdateTime:
timeToLiveInMinutes: "${CACHE_SPECS_USERS_UPDATE_TIME_TTL:20000}"
userSessionsInvalidation:
# The value of this TTL is ignored and replaced by JWT refresh token expiration time
timeToLiveInMinutes: "0"
maxSize: "${CACHE_SPECS_USERS_UPDATE_TIME_MAX_SIZE:10000}"
otaPackages:
timeToLiveInMinutes: "${CACHE_SPECS_OTA_PACKAGES_TTL:60}"
@ -535,6 +546,7 @@ spring.servlet.multipart.max-file-size: "50MB"
spring.servlet.multipart.max-request-size: "50MB"
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true"
# Note: as for current Spring JPA version, custom NullHandling for the Sort.Order is ignored and this parameter is used
spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PROPERTIES_HIBERNATE_ORDER_BY_DEFAULT_NULL_ORDERING:last}"
# SQL DAO Configuration
@ -601,24 +613,25 @@ state:
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}"
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
mvel:
enabled: "${MVEL_ENABLED:true}"
max_total_args_size: "${MVEL_MAX_TOTAL_ARGS_SIZE:100000}"
max_result_size: "${MVEL_MAX_RESULT_SIZE:300000}"
max_script_body_size: "${MVEL_MAX_SCRIPT_BODY_SIZE:50000}"
# Maximum allowed MVEL script execution memory
max_memory_limit_mb: "${MVEL_MAX_MEMORY_LIMIT_MB: 8}"
# Maximum allowed MVEL script execution errors before it will be blacklisted
max_errors: "${MVEL_MAX_ERRORS:3}"
# MVEL Eval max request timeout in milliseconds. 0 - no timeout
max_requests_timeout: "${MVEL_MAX_REQUEST_TIMEOUT:500}"
tbel:
enabled: "${TBEL_ENABLED:true}"
max_total_args_size: "${TBEL_MAX_TOTAL_ARGS_SIZE:100000}"
max_result_size: "${TBEL_MAX_RESULT_SIZE:300000}"
max_script_body_size: "${TBEL_MAX_SCRIPT_BODY_SIZE:50000}"
# Maximum allowed TBEL script execution memory
max_memory_limit_mb: "${TBEL_MAX_MEMORY_LIMIT_MB: 8}"
# Maximum allowed TBEL script execution errors before it will be blacklisted
max_errors: "${TBEL_MAX_ERRORS:3}"
# TBEL Eval max request timeout in milliseconds. 0 - no timeout
max_requests_timeout: "${TBEL_MAX_REQUEST_TIMEOUT:500}"
# Maximum time in seconds for black listed function to stay in the list.
max_black_list_duration_sec: "${MVEL_MAX_BLACKLIST_DURATION_SEC:60}"
max_black_list_duration_sec: "${TBEL_MAX_BLACKLIST_DURATION_SEC:60}"
# Specify thread pool size for javascript executor service
thread_pool_size: "${MVEL_THREAD_POOL_SIZE:50}"
thread_pool_size: "${TBEL_THREAD_POOL_SIZE:50}"
compiled_scripts_cache_size: "${TBEL_COMPILED_SCRIPTS_CACHE_SIZE:1000}"
stats:
enabled: "${TB_MVEL_STATS_ENABLED:false}"
print_interval_ms: "${TB_MVEL_STATS_PRINT_INTERVAL_MS:10000}"
enabled: "${TB_TBEL_STATS_ENABLED:false}"
print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}"
js:
evaluator: "${JS_EVALUATOR:local}" # local/remote
@ -919,7 +932,7 @@ edges:
storage:
max_read_records_count: "${EDGES_STORAGE_MAX_READ_RECORDS_COUNT:50}"
no_read_records_sleep: "${EDGES_NO_READ_RECORDS_SLEEP:1000}"
sleep_between_batches: "${EDGES_SLEEP_BETWEEN_BATCHES:1000}"
sleep_between_batches: "${EDGES_SLEEP_BETWEEN_BATCHES:10000}"
scheduler_pool_size: "${EDGES_SCHEDULER_POOL_SIZE:1}"
send_scheduler_pool_size: "${EDGES_SEND_SCHEDULER_POOL_SIZE:1}"
grpc_callback_thread_pool_size: "${EDGES_GRPC_CALLBACK_POOL_SIZE:1}"

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

@ -88,6 +88,7 @@ import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import java.io.IOException;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
@ -234,7 +235,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
customerUser = createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD);
customerUserId = customerUser.getId();
logout();
resetTokens();
log.info("Executed web test setup");
}
@ -336,7 +337,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Assert.assertNotNull(savedDifferentCustomer);
differentCustomerId = savedDifferentCustomer.getId();
logout();
resetTokens();
}
protected void deleteDifferentTenant() throws Exception {
@ -350,7 +351,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected User createUserAndLogin(User user, String password) throws Exception {
User savedUser = doPost("/api/user", user, User.class);
logout();
resetTokens();
JsonNode activateRequest = getActivateRequest(password);
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, user.getEmail());
@ -375,7 +376,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
protected void login(String username, String password) throws Exception {
logout();
resetTokens();
JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, username);
}
@ -411,12 +412,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Assert.assertEquals(username, subject);
}
protected void logout() throws Exception {
protected void resetTokens() throws Exception {
this.token = null;
this.refreshToken = null;
this.username = null;
}
protected void logout() throws Exception {
doPost("/api/auth/logout").andExpect(status().isOk());
}
protected void setJwtToken(MockHttpServletRequestBuilder request) {
if (this.token != null) {
request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);
@ -764,4 +769,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
throw new AssertionError("Unexpected status " + mvcResult.getResponse().getStatus());
}
protected <T> T getFieldValue(Object target, String fieldName) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (T) field.get(target);
}
}

122
application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java

@ -17,14 +17,21 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.mail.DefaultMailService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
@ -32,8 +39,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
public abstract class BaseAdminControllerTest extends AbstractControllerTest {
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey");
@Autowired
MailService mailService;
@ -45,67 +53,67 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
public void testFindAdminSettingsByKey() throws Exception {
loginSysAdmin();
doGet("/api/admin/settings/general")
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("general")))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080")));
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("general")))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080")));
doGet("/api/admin/settings/mail")
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("mail")))
.andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp")))
.andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost")))
.andExpect(jsonPath("$.jsonValue.smtpPort", is("25")));
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("mail")))
.andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp")))
.andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost")))
.andExpect(jsonPath("$.jsonValue.smtpPort", is("25")));
doGet("/api/admin/settings/unknown")
.andExpect(status().isNotFound());
.andExpect(status().isNotFound());
}
@Test
public void testSaveAdminSettings() throws Exception {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class);
AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class);
JsonNode jsonValue = adminSettings.getJsonValue();
((ObjectNode) jsonValue).put("baseUrl", "http://myhost.org");
adminSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
doGet("/api/admin/settings/general")
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org")));
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org")));
((ObjectNode) jsonValue).put("baseUrl", "http://localhost:8080");
adminSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", adminSettings)
.andExpect(status().isOk());
.andExpect(status().isOk());
}
@Test
public void testSaveAdminSettingsWithEmptyKey() throws Exception {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
adminSettings.setKey(null);
doPost("/api/admin/settings", adminSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Key should be specified")));
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Key should be specified")));
}
@Test
public void testChangeAdminSettingsKey() throws Exception {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
adminSettings.setKey("newKey");
doPost("/api/admin/settings", adminSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("is prohibited")));
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("is prohibited")));
}
@Test
@ -113,7 +121,7 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
doPost("/api/admin/settings/testMail", adminSettings)
.andExpect(status().isOk());
.andExpect(status().isOk());
}
@Test
@ -139,4 +147,48 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError());
Mockito.doNothing().when(mailService).sendTestMail(Mockito.any(), Mockito.any());
}
void resetJwtSettingsToDefault() throws Exception {
loginSysAdmin();
doPost("/api/admin/jwtSettings", defaultJwtSettings).andExpect(status().isOk()); // jwt test scenarios are always started from
loginTenantAdmin();
}
@Test
public void testGetAndSaveDefaultJwtSettings() throws Exception {
JwtSettings jwtSettings;
loginSysAdmin();
jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());
jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
resetJwtSettingsToDefault();
}
@Test
public void testCreateJwtSettings() throws Exception {
loginSysAdmin();
JwtSettings jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8)));
doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());
doGet("/api/admin/jwtSettings").andExpect(status().isUnauthorized()); //the old JWT token does not work after signing key was changed!
loginSysAdmin();
JwtSettings newJwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(newJwtSettings);
resetJwtSettingsToDefault();
}
}

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

@ -77,7 +77,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
device.setCustomerId(customerId);
customerDevice = doPost("/api/device", device, Device.class);
logout();
resetTokens();
}
@After
@ -423,7 +423,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
testNotifyEntityNeverMsgToEdgeServiceOneTime(alarm, alarm.getId(), tenantId, ActionType.ADDED);
logout();
resetTokens();
JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}");
JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class);

16
application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java

@ -44,6 +44,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@ -202,6 +203,21 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest
});
}
@Test
public void whenSavingAuditLogAndPartitionSaveErrorOccurred_thenSaveAuditLogAnyway() throws Exception {
// creating partition bigger than sql.audit_logs.partition_size
partitioningRepository.createPartitionIfNotExists("audit_log", System.currentTimeMillis(), TimeUnit.DAYS.toMillis(7));
List<Long> partitions = partitioningRepository.fetchPartitions("audit_log");
assertThat(partitions).size().isOne();
partitioningRepository.cleanupPartitionsCache("audit_log", System.currentTimeMillis(), 0);
assertDoesNotThrow(() -> {
// expecting partition overlap error on partition save
createAuditLog(ActionType.LOGIN, tenantAdminUserId);
});
assertThat(partitioningRepository.fetchPartitions("audit_log")).isEqualTo(partitions);
}
private AuditLog createAuditLog(ActionType actionType, EntityId entityId) {
AuditLog auditLog = new AuditLog();
auditLog.setTenantId(tenantId);

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

@ -15,13 +15,15 @@
*/
package org.thingsboard.server.controller;
import org.junit.Test;
import org.thingsboard.server.common.data.security.Authority;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.thingsboard.server.common.data.security.Authority;
import org.junit.Test;
public abstract class BaseAuthControllerTest extends AbstractControllerTest {
@Test
@ -57,9 +59,13 @@ public abstract class BaseAuthControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed
logout();
doGet("/api/auth/user")
.andExpect(status().isUnauthorized());
resetTokens();
}
@Test

69
application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java

@ -22,6 +22,9 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.Tenant;
@ -29,16 +32,27 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.edge.EdgeEventDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.service.ttl.EdgeEventsCleanUpService;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@TestPropertySource(properties = {
@ -50,6 +64,18 @@ public abstract class BaseEdgeEventControllerTest extends AbstractControllerTest
private Tenant savedTenant;
private User tenantAdmin;
@Autowired
private EdgeEventDao edgeEventDao;
@SpyBean
private SqlPartitioningRepository partitioningRepository;
@Autowired
private EdgeEventsCleanUpService edgeEventsCleanUpService;
@Value("#{${sql.edge_events.partition_size} * 60 * 60 * 1000}")
private long partitionDurationInMs;
@Value("${sql.ttl.edge_events.edge_event_ttl}")
private long edgeEventTtlInSec;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -114,6 +140,34 @@ public abstract class BaseEdgeEventControllerTest extends AbstractControllerTest
Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.RELATION.equals(ee.getType())));
}
@Test
public void saveEdgeEvent_thenCreatePartitionIfNotExist() {
reset(partitioningRepository);
EdgeEvent edgeEvent = createEdgeEvent();
verify(partitioningRepository).createPartitionIfNotExists(eq("edge_event"), eq(edgeEvent.getCreatedTime()), eq(partitionDurationInMs));
List<Long> partitions = partitioningRepository.fetchPartitions("edge_event");
assertThat(partitions).singleElement().satisfies(partitionStartTs -> {
assertThat(partitionStartTs).isEqualTo(partitioningRepository.calculatePartitionStartTime(edgeEvent.getCreatedTime(), partitionDurationInMs));
});
}
@Test
public void cleanUpEdgeEventByTtl_dropOldPartitions() {
long oldEdgeEventTs = LocalDate.of(2020, 10, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
long partitionStartTs = partitioningRepository.calculatePartitionStartTime(oldEdgeEventTs, partitionDurationInMs);
partitioningRepository.createPartitionIfNotExists("edge_event", oldEdgeEventTs, partitionDurationInMs);
List<Long> partitions = partitioningRepository.fetchPartitions("edge_event");
assertThat(partitions).contains(partitionStartTs);
edgeEventsCleanUpService.cleanUp();
partitions = partitioningRepository.fetchPartitions("edge_event");
assertThat(partitions).doesNotContain(partitionStartTs);
assertThat(partitions).allSatisfy(partitionsStart -> {
long partitionEndTs = partitionsStart + partitionDurationInMs;
assertThat(partitionEndTs).isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(edgeEventTtlInSec));
});
}
private List<EdgeEvent> findEdgeEvents(EdgeId edgeId) throws Exception {
return doGetTypedWithTimePageLink("/api/edge/" + edgeId.toString() + "/events?",
new TypeReference<PageData<EdgeEvent>>() {
@ -134,4 +188,19 @@ public abstract class BaseEdgeEventControllerTest extends AbstractControllerTest
return asset;
}
private EdgeEvent createEdgeEvent() {
EdgeEvent edgeEvent = new EdgeEvent();
edgeEvent.setCreatedTime(System.currentTimeMillis());
edgeEvent.setTenantId(tenantId);
edgeEvent.setAction(EdgeEventActionType.ADDED);
edgeEvent.setEntityId(tenantAdmin.getUuidId());
edgeEvent.setType(EdgeEventType.ALARM);
try {
edgeEventDao.saveAsync(edgeEvent).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
return edgeEvent;
}
}

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

@ -22,6 +22,7 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
@ -55,6 +56,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class BaseEntityQueryControllerTest extends AbstractControllerTest {
@ -381,4 +383,24 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
Assert.assertEquals("1" + i, alarmActiveTime);
}
}
@Test
public void givenInvalidEntityDataPageLink_thenReturnError() throws Exception {
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceNameFilter("");
String invalidSortProperty = "created(Time)";
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, invalidSortProperty), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List<EntityKey> latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
ResultActions result = doPost("/api/entitiesQuery/find", query).andExpect(status().isBadRequest());
assertThat(getErrorMessage(result)).contains("Invalid").contains("sort property");
}
}

20
application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java

@ -28,6 +28,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
@ -106,7 +107,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
ActionType.ADDED, ActionType.ADDED, 1, 1, 1);
Mockito.reset(tbClusterService, auditLogService);
logout();
resetTokens();
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
@ -123,7 +124,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email", is(email)));
logout();
resetTokens();
login(email, "testPassword");
@ -218,7 +219,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
user.setLastName("Downs");
User savedUser = createUserAndLogin(user, "testPassword1");
logout();
resetTokens();
JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode()
.put("email", email);
@ -244,7 +245,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email", is(email)));
logout();
resetTokens();
login(email, "testPassword2");
doGet("/api/auth/user")
@ -729,6 +730,17 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(userDao, tenantId, userId, "/api/user/" + userId);
}
@Test
public void givenInvalidPageLink_thenReturnError() throws Exception {
loginTenantAdmin();
String invalidSortProperty = "abc(abc)";
ResultActions result = doGet("/api/users?page={page}&pageSize={pageSize}&sortProperty={sortProperty}", 0, 100, invalidSortProperty)
.andExpect(status().isBadRequest());
assertThat(getErrorMessage(result)).containsIgnoringCase("invalid sort property");
}
private User createUser() throws Exception {
loginSysAdmin();
String email = "tenant2@thingsboard.org";

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

@ -51,7 +51,7 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import java.time.Duration;
import java.util.Arrays;
@ -396,7 +396,7 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
private void logInWithPreVerificationToken(String username, String password) throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password);
JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class);
JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class);
assertThat(response.getToken()).isNotNull();
assertThat(response.getRefreshToken()).isNull();
assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);

19
application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java

@ -439,9 +439,9 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getMessageFromTail(2);
Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg);
DeviceUpdateMsg latestDeviceUpdateMsg = (DeviceUpdateMsg) latestMessage;
Optional<DeviceUpdateMsg> deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class);
Assert.assertTrue(deviceUpdateMsgOpt.isPresent());
DeviceUpdateMsg latestDeviceUpdateMsg = deviceUpdateMsgOpt.get();
Assert.assertNotEquals(deviceOnCloudName, latestDeviceUpdateMsg.getName());
Assert.assertEquals(deviceOnCloudName, latestDeviceUpdateMsg.getConflictName());
@ -453,9 +453,9 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
Assert.assertNotNull(device);
Assert.assertNotEquals(deviceOnCloudName, device.getName());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DeviceCredentialsRequestMsg);
DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = (DeviceCredentialsRequestMsg) latestMessage;
Optional<DeviceCredentialsRequestMsg> deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsRequestMsg.class);
Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent());
DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = deviceCredentialsUpdateMsgOpt.get();
Assert.assertEquals(uuid.getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB());
Assert.assertEquals(uuid.getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB());
@ -511,8 +511,11 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
body.put("expirationTime", System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10));
body.put("method", "test_method");
body.put("params", "{\"param1\":\"value1\"}");
body.put("persisted", true);
body.put("retries", 2);
EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL, device.getId().getId(), EdgeEventType.DEVICE, body);
EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL,
device.getId().getId(), EdgeEventType.DEVICE, body);
edgeImitator.expectMessageAmount(1);
edgeEventService.saveAsync(edgeEvent).get();
clusterService.onEdgeEventUpdate(tenantId, edge.getId());
@ -522,6 +525,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(latestMessage instanceof DeviceRpcCallMsg);
DeviceRpcCallMsg latestDeviceRpcCallMsg = (DeviceRpcCallMsg) latestMessage;
Assert.assertEquals("test_method", latestDeviceRpcCallMsg.getRequestMsg().getMethod());
Assert.assertTrue(latestDeviceRpcCallMsg.getPersisted());
Assert.assertEquals(2, latestDeviceRpcCallMsg.getRetries());
}
private void sendAttributesRequestAndVerify(Device device, String scope, String attributesDataStr, String expectedKey,

2
application/src/test/java/org/thingsboard/server/edge/BaseTelemetryEdgeTest.java

@ -34,7 +34,7 @@ abstract public class BaseTelemetryEdgeTest extends AbstractEdgeTest {
@Test
public void testTimeseriesWithFailures() throws Exception {
int numberOfTimeseriesToSend = 1000;
int numberOfTimeseriesToSend = 333;
Device device = findDeviceByName("Edge Device 1");

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

@ -365,11 +365,7 @@ public class EdgeImitator {
}
public AbstractMessage getLatestMessage() {
return getMessageFromTail(1);
}
public AbstractMessage getMessageFromTail(int offset) {
return downlinkMsgs.get(downlinkMsgs.size() - offset);
return downlinkMsgs.get(downlinkMsgs.size() - 1);
}
public void ignoreType(Class<? extends AbstractMessage> type) {

247
application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java

@ -0,0 +1,247 @@
/**
* Copyright © 2016-2022 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.queue;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.NotificationsTopicService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = DefaultTbClusterService.class)
public class DefaultTbClusterServiceTest {
public static final String MONOLITH = "monolith";
public static final String CORE = "core";
public static final String RULE_ENGINE = "rule_engine";
public static final String TRANSPORT = "transport";
@MockBean
protected DataDecodingEncodingService encodingService;
@MockBean
protected TbDeviceProfileCache deviceProfileCache;
@MockBean
protected TbAssetProfileCache assetProfileCache;
@MockBean
protected GatewayNotificationsService gatewayNotificationsService;
@MockBean
protected PartitionService partitionService;
@MockBean
protected TbQueueProducerProvider producerProvider;
@SpyBean
protected NotificationsTopicService notificationsTopicService;
@SpyBean
protected TbClusterService clusterService;
@Test
public void testOnQueueChangeSingleMonolith() {
when(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)).thenReturn(Sets.newHashSet(MONOLITH));
when(partitionService.getAllServiceIds(ServiceType.TB_CORE)).thenReturn(Sets.newHashSet(MONOLITH));
when(partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT)).thenReturn(Sets.newHashSet(MONOLITH));
TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> tbQueueProducer = mock(TbQueueProducer.class);
when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbQueueProducer);
clusterService.onQueueChange(createTestQueue());
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH);
verify(notificationsTopicService, never()).getNotificationsTopic(eq(ServiceType.TB_CORE), any());
verify(notificationsTopicService, never()).getNotificationsTopic(eq(ServiceType.TB_TRANSPORT), any());
verify(tbQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH)), any(TbProtoQueueMsg.class), isNull());
verify(producerProvider, never()).getTbCoreNotificationsMsgProducer();
verify(producerProvider, never()).getTransportNotificationsMsgProducer();
}
@Test
public void testOnQueueChangeMultipleMonoliths() {
String monolith1 = MONOLITH + 1;
String monolith2 = MONOLITH + 2;
when(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)).thenReturn(Sets.newHashSet(monolith1, monolith2));
when(partitionService.getAllServiceIds(ServiceType.TB_CORE)).thenReturn(Sets.newHashSet(monolith1, monolith2));
when(partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT)).thenReturn(Sets.newHashSet(monolith1, monolith2));
TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> tbQueueProducer = mock(TbQueueProducer.class);
when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbQueueProducer);
clusterService.onQueueChange(createTestQueue());
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2);
verify(notificationsTopicService, never()).getNotificationsTopic(eq(ServiceType.TB_CORE), any());
verify(notificationsTopicService, never()).getNotificationsTopic(eq(ServiceType.TB_TRANSPORT), any());
verify(tbQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1)), any(TbProtoQueueMsg.class), isNull());
verify(tbQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2)), any(TbProtoQueueMsg.class), isNull());
verify(producerProvider, never()).getTbCoreNotificationsMsgProducer();
verify(producerProvider, never()).getTransportNotificationsMsgProducer();
}
@Test
public void testOnQueueChangeSingleMonolithAndSingleRemoteTransport() {
when(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)).thenReturn(Sets.newHashSet(MONOLITH));
when(partitionService.getAllServiceIds(ServiceType.TB_CORE)).thenReturn(Sets.newHashSet(MONOLITH));
when(partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT)).thenReturn(Sets.newHashSet(MONOLITH, TRANSPORT));
TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> tbREQueueProducer = mock(TbQueueProducer.class);
TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToTransportMsg>> tbTransportQueueProducer = mock(TbQueueProducer.class);
when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbREQueueProducer);
when(producerProvider.getTransportNotificationsMsgProducer()).thenReturn(tbTransportQueueProducer);
clusterService.onQueueChange(createTestQueue());
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_TRANSPORT, TRANSPORT);
verify(notificationsTopicService, never()).getNotificationsTopic(eq(ServiceType.TB_CORE), any());
verify(tbREQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH)), any(TbProtoQueueMsg.class), isNull());
verify(tbTransportQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, TRANSPORT)), any(TbProtoQueueMsg.class), isNull());
verify(tbTransportQueueProducer, never())
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, MONOLITH)), any(TbProtoQueueMsg.class), isNull());
verify(tbTransportQueueProducer, never())
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, MONOLITH)), any(TbProtoQueueMsg.class), isNull());
verify(producerProvider, never()).getTbCoreNotificationsMsgProducer();
}
@Test
public void testOnQueueChangeMultipleMicroservices() {
String monolith1 = MONOLITH + 1;
String monolith2 = MONOLITH + 2;
String core1 = CORE + 1;
String core2 = CORE + 2;
String ruleEngine1 = RULE_ENGINE + 1;
String ruleEngine2 = RULE_ENGINE + 2;
String transport1 = TRANSPORT + 1;
String transport2 = TRANSPORT + 2;
when(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)).thenReturn(Sets.newHashSet(monolith1, monolith2, ruleEngine1, ruleEngine2));
when(partitionService.getAllServiceIds(ServiceType.TB_CORE)).thenReturn(Sets.newHashSet(monolith1, monolith2, core1, core2));
when(partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT)).thenReturn(Sets.newHashSet(monolith1, monolith2, transport1, transport2));
TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> tbREQueueProducer = mock(TbQueueProducer.class);
TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToCoreNotificationMsg>> tbCoreQueueProducer = mock(TbQueueProducer.class);
TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToTransportMsg>> tbTransportQueueProducer = mock(TbQueueProducer.class);
when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbREQueueProducer);
when(producerProvider.getTbCoreNotificationsMsgProducer()).thenReturn(tbCoreQueueProducer);
when(producerProvider.getTransportNotificationsMsgProducer()).thenReturn(tbTransportQueueProducer);
clusterService.onQueueChange(createTestQueue());
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngine1);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngine2);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_CORE, core1);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_CORE, core2);
verify(notificationsTopicService, never()).getNotificationsTopic(ServiceType.TB_CORE, monolith1);
verify(notificationsTopicService, never()).getNotificationsTopic(ServiceType.TB_CORE, monolith2);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_TRANSPORT, transport1);
verify(notificationsTopicService, times(1)).getNotificationsTopic(ServiceType.TB_TRANSPORT, transport2);
verify(notificationsTopicService, never()).getNotificationsTopic(ServiceType.TB_TRANSPORT, monolith1);
verify(notificationsTopicService, never()).getNotificationsTopic(ServiceType.TB_TRANSPORT, monolith2);
verify(tbREQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1)), any(TbProtoQueueMsg.class), isNull());
verify(tbREQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2)), any(TbProtoQueueMsg.class), isNull());
verify(tbREQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngine1)), any(TbProtoQueueMsg.class), isNull());
verify(tbREQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngine2)), any(TbProtoQueueMsg.class), isNull());
verify(tbCoreQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, core1)), any(TbProtoQueueMsg.class), isNull());
verify(tbCoreQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, core2)), any(TbProtoQueueMsg.class), isNull());
verify(tbCoreQueueProducer, never())
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, monolith1)), any(TbProtoQueueMsg.class), isNull());
verify(tbCoreQueueProducer, never())
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, monolith2)), any(TbProtoQueueMsg.class), isNull());
verify(tbTransportQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transport1)), any(TbProtoQueueMsg.class), isNull());
verify(tbTransportQueueProducer, times(1))
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transport2)), any(TbProtoQueueMsg.class), isNull());
verify(tbTransportQueueProducer, never())
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, monolith1)), any(TbProtoQueueMsg.class), isNull());
verify(tbTransportQueueProducer, never())
.send(eq(notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, monolith2)), any(TbProtoQueueMsg.class), isNull());
}
protected Queue createTestQueue() {
TenantId tenantId = TenantId.SYS_TENANT_ID;
Queue queue = new Queue(new QueueId(UUID.randomUUID()));
queue.setTenantId(tenantId);
queue.setName("Main");
queue.setTopic("main");
queue.setPartitions(10);
return queue;
}
}

2
application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java

@ -70,7 +70,7 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest {
}
long duration = System.currentTimeMillis() - startTs;
System.out.println(iterations + " invocations took: " + duration + "ms");
Assert.assertTrue(duration < TimeUnit.MINUTES.toMillis(3));
Assert.assertTrue(duration < TimeUnit.MINUTES.toMillis(4));
}
@Test

107
application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java → application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.script;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -23,31 +24,38 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.script.api.tbel.TbelScript;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest
@TestPropertySource(properties = {
"mvel.max_script_body_size=100",
"mvel.max_total_args_size=50",
"mvel.max_result_size=50",
"mvel.max_errors=2",
"tbel.max_script_body_size=100",
"tbel.max_total_args_size=50",
"tbel.max_result_size=50",
"tbel.max_errors=2",
"tbel.compiled_scripts_cache_size=100"
})
class MvelInvokeServiceTest extends AbstractControllerTest {
class TbelInvokeServiceTest extends AbstractControllerTest {
@Autowired
private MvelInvokeService invokeService;
private TbelInvokeService invokeService;
@Value("${mvel.max_errors}")
@Value("${tbel.max_errors}")
private int maxJsErrors;
@Test
@ -110,6 +118,89 @@ class MvelInvokeServiceTest extends AbstractControllerTest {
assertThatScriptIsBlocked(scriptId);
}
@Test
void givenScriptsWithSameBody_thenCompileAndCacheOnlyOnce() throws Exception {
String script = "return msg.temperature > 20;";
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 100; i++) {
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
}
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Map<String, TbelScript> scriptMap = getFieldValue(invokeService, "scriptMap");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
String scriptHash = scriptIdToHash.get(scriptsIds.get(0));
assertThat(scriptsIds.stream().map(scriptIdToHash::get)).containsOnly(scriptHash);
assertThat(scriptMap).containsKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNotNull();
}
@Test
public void whenReleasingScript_thenCheckForScriptHashUsages() throws Exception {
String script = "return msg.temperature > 20;";
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 10; i++) {
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
}
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Map<String, TbelScript> scriptMap = getFieldValue(invokeService, "scriptMap");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
String scriptHash = scriptIdToHash.get(scriptsIds.get(0));
for (int i = 0; i < 9; i++) {
UUID scriptId = scriptsIds.get(i);
assertThat(scriptIdToHash).containsKey(scriptId);
invokeService.release(scriptId);
assertThat(scriptIdToHash).doesNotContainKey(scriptId);
}
assertThat(scriptMap).containsKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNotNull();
invokeService.release(scriptsIds.get(9));
assertThat(scriptMap).doesNotContainKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNull();
}
@Test
public void whenCompiledScriptsCacheIsTooBig_thenRemoveRarelyUsedScripts() throws Exception {
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 110; i++) { // tbel.compiled_scripts_cache_size = 100
String script = "return msg.temperature > " + i;
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
for (int j = 0; j < i; j++) {
invokeScript(scriptId, "{ \"temperature\": 12 }"); // so that scriptsIds is ordered by number of invocations
}
}
ConcurrentMap<String, Serializable> cache = compiledScriptsCache.asMap();
for (int i = 0; i < 10; i++) { // iterating rarely used scripts
UUID scriptId = scriptsIds.get(i);
String scriptHash = scriptIdToHash.get(scriptId);
assertThat(cache).doesNotContainKey(scriptHash);
}
for (int i = 10; i < 110; i++) {
UUID scriptId = scriptsIds.get(i);
String scriptHash = scriptIdToHash.get(scriptId);
assertThat(cache).containsKey(scriptHash);
}
UUID scriptRemovedFromCache = scriptsIds.get(0);
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNull();
invokeScript(scriptRemovedFromCache, "{ \"temperature\": 12 }");
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNotNull();
}
private void assertThatScriptIsBlocked(UUID scriptId) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");

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

@ -23,7 +23,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.AccessJwtToken;
@ -36,6 +37,8 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
public class JwtTokenFactoryTest {
@ -50,7 +53,10 @@ public class JwtTokenFactoryTest {
jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2));
jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7));
tokenFactory = new JwtTokenFactory(jwtSettings);
JwtSettingsService jwtSettingsService = mock(JwtSettingsService.class);
willReturn(jwtSettings).given(jwtSettingsService).getJwtSettings();
tokenFactory = new JwtTokenFactory(jwtSettingsService);
}
@Test

167
application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java

@ -15,19 +15,29 @@
*/
package org.thingsboard.server.service.security.auth;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootContextLoader;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.thingsboard.server.common.data.CacheConstants;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
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.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
@ -39,13 +49,9 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
import java.util.UUID;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@ -53,31 +59,37 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ActiveProfiles("test")
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TokenOutdatingTest.class, loader = SpringBootContextLoader.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@ComponentScan({"org.thingsboard.server"})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DaoSqlTest
@TestPropertySource(properties = {
"security.jwt.tokenIssuer=test.io",
"security.jwt.tokenSigningKey=secret",
"security.jwt.tokenExpirationTime=600",
"security.jwt.refreshTokenExpTime=15",
// explicitly set the wrong value to check that it is NOT used.
"cache.specs.userSessionsInvalidation.timeToLiveInMinutes=2"
})
public class TokenOutdatingTest {
private JwtAuthenticationProvider accessTokenAuthenticationProvider;
private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
@Autowired
private TokenOutdatingService tokenOutdatingService;
private ConcurrentMapCacheManager cacheManager;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private JwtTokenFactory tokenFactory;
private JwtSettings jwtSettings;
private SecurityUser securityUser;
private UserId userId;
@BeforeEach
@Before
public void setUp() {
jwtSettings = new JwtSettings();
jwtSettings.setTokenIssuer("test.io");
jwtSettings.setTokenExpirationTime((int) MINUTES.toSeconds(10));
jwtSettings.setRefreshTokenExpTime((int) DAYS.toSeconds(7));
jwtSettings.setTokenSigningKey("secret");
tokenFactory = new JwtTokenFactory(jwtSettings);
cacheManager = new ConcurrentMapCacheManager();
tokenOutdatingService = new TokenOutdatingService(cacheManager, tokenFactory, jwtSettings);
tokenOutdatingService.initCache();
userId = new UserId(UUID.randomUUID());
UserId userId = new UserId(UUID.randomUUID());
securityUser = createMockSecurityUser(userId);
UserService userService = mock(UserService.class);
@ -97,28 +109,29 @@ public class TokenOutdatingTest {
@Test
public void testOutdateOldUserTokens() throws Exception {
JwtToken jwtToken = createAccessJwtToken(userId);
JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser);
SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId));
assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId));
// Token outdatage time is rounded to 1 sec. Need to wait before outdating so that outdatage time is strictly after token issue time
SECONDS.sleep(1);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
SECONDS.sleep(1);
JwtToken newJwtToken = tokenFactory.createAccessJwtToken(createMockSecurityUser(userId));
assertFalse(tokenOutdatingService.isOutdated(newJwtToken, userId));
JwtToken newJwtToken = tokenFactory.createAccessJwtToken(securityUser);
assertFalse(tokenOutdatingService.isOutdated(newJwtToken, securityUser.getId()));
}
@Test
public void testAuthenticateWithOutdatedAccessToken() throws InterruptedException {
RawAccessJwtToken accessJwtToken = getRawJwtToken(createAccessJwtToken(userId));
RawAccessJwtToken accessJwtToken = getRawJwtToken(tokenFactory.createAccessJwtToken(securityUser));
assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken));
});
SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId));
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken));
@ -127,49 +140,94 @@ public class TokenOutdatingTest {
@Test
public void testAuthenticateWithOutdatedRefreshToken() throws InterruptedException {
RawAccessJwtToken refreshJwtToken = getRawJwtToken(createRefreshJwtToken(userId));
RawAccessJwtToken refreshJwtToken = getRawJwtToken(tokenFactory.createRefreshToken(securityUser));
assertDoesNotThrow(() -> {
refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken));
});
SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId));
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertThrows(CredentialsExpiredException.class, () -> {
refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken));
});
}
// This test takes too long to run and is basically testing the cache logic
// @Test
// public void testTokensOutdatageTimeRemovalFromCache() throws Exception {
// JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser);
//
// SECONDS.sleep(1);
// eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
//
// SECONDS.sleep(1);
//
// assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
//
// SECONDS.sleep(30); // refreshTokenExpTime/2
//
// assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
//
// SECONDS.sleep(30 + 1); // refreshTokenExpTime/2 + 1
//
// assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
// }
@Test
public void testTokensOutdatageTimeRemovalFromCache() throws Exception {
JwtToken jwtToken = createAccessJwtToken(userId);
public void testOnlyOneTokenExpired() throws InterruptedException {
JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser);
SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId));
SecurityUser anotherSecurityUser = new SecurityUser(securityUser, securityUser.isEnabled(), securityUser.getUserPrincipal());
JwtToken anotherJwtToken = tokenFactory.createAccessJwtToken(anotherSecurityUser);
int refreshTokenExpirationTime = 3;
jwtSettings.setRefreshTokenExpTime(refreshTokenExpirationTime);
assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken)));
});
SECONDS.sleep(refreshTokenExpirationTime - 2);
SECONDS.sleep(1);
assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId));
assertNotNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString()));
eventPublisher.publishEvent(new UserSessionInvalidationEvent(securityUser.getSessionId()));
SECONDS.sleep(3);
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken)));
});
assertFalse(tokenOutdatingService.isOutdated(jwtToken, userId));
assertNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString()));
assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken)));
});
}
private JwtToken createAccessJwtToken(UserId userId) {
return tokenFactory.createAccessJwtToken(createMockSecurityUser(userId));
}
@Test
public void testResetAllSessions() throws InterruptedException {
JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser);
SecurityUser anotherSecurityUser = new SecurityUser(securityUser, securityUser.isEnabled(), securityUser.getUserPrincipal());
JwtToken anotherJwtToken = tokenFactory.createAccessJwtToken(anotherSecurityUser);
private JwtToken createRefreshJwtToken(UserId userId) {
return tokenFactory.createRefreshToken(createMockSecurityUser(userId));
assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken)));
});
assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken)));
});
SECONDS.sleep(1);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken)));
});
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken)));
});
}
private RawAccessJwtToken getRawJwtToken(JwtToken token) {
return new RawAccessJwtToken(token.getToken());
}
@ -180,6 +238,7 @@ public class TokenOutdatingTest {
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail()));
securityUser.setAuthority(Authority.CUSTOMER_USER);
securityUser.setId(userId);
securityUser.setSessionId(UUID.randomUUID().toString());
return securityUser;
}
}

68
application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandlerTest.java

@ -0,0 +1,68 @@
/**
* Copyright © 2016-2022 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.oauth2;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@DaoSqlTest
public class Oauth2AuthenticationSuccessHandlerTest extends AbstractControllerTest {
@Autowired
private Oauth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler;
@Mock
private JwtTokenFactory jwtTokenFactory;
private SecurityUser securityUser;
@Before
public void before() {
UserId userId = new UserId(UUID.randomUUID());
securityUser = new SecurityUser(userId);
when(jwtTokenFactory.createTokenPair(eq(securityUser))).thenReturn(new JwtPair("testAccessToken", "testRefreshToken"));
}
@Test
public void testGetRedirectUrl() {
JwtPair jwtPair = jwtTokenFactory.createTokenPair(securityUser);
String urlWithoutParams = "http://localhost:8080/dashboardGroups/3fa13530-6597-11ed-bd76-8bd591f0ec3e";
String urlWithParams = "http://localhost:8080/dashboardGroups/3fa13530-6597-11ed-bd76-8bd591f0ec3e?state=someState&page=1";
String redirectUrl = oauth2AuthenticationSuccessHandler.getRedirectUrl(urlWithoutParams, jwtPair);
String expectedUrl = urlWithoutParams + "/?accessToken=" + jwtPair.getToken() + "&refreshToken=" + jwtPair.getRefreshToken();
assertEquals(expectedUrl, redirectUrl);
redirectUrl = oauth2AuthenticationSuccessHandler.getRedirectUrl(urlWithParams, jwtPair);
expectedUrl = urlWithParams + "&accessToken=" + jwtPair.getToken() + "&refreshToken=" + jwtPair.getRefreshToken();
assertEquals(expectedUrl, redirectUrl);
}
}

48
application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java

@ -15,27 +15,37 @@
*/
package org.thingsboard.server.service.state;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import java.util.Map;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_TIMEOUT;
@RunWith(MockitoJUnitRunner.class)
public class DefaultDeviceStateServiceTest {
@ -83,4 +93,40 @@ public class DefaultDeviceStateServiceTest {
Mockito.verify(service, times(1)).fetchDeviceStateDataUsingEntityDataQuery(deviceId);
}
@Test
public void givenPersistToTelemetryAndDefaultInactivityTimeoutFetched_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() {
var defaultInactivityTimeoutInSec = 60L;
var latest =
Map.of(
EntityKeyType.TIME_SERIES, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(defaultInactivityTimeoutInSec * 1000))),
EntityKeyType.SERVER_ATTRIBUTE, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(5000L)))
);
process(latest, defaultInactivityTimeoutInSec);
}
@Test
public void givenPersistToTelemetryAndNoInactivityTimeoutFetchedFromTimeSeries_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() {
var defaultInactivityTimeoutInSec = 60L;
var latest =
Map.of(
EntityKeyType.SERVER_ATTRIBUTE, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(5000L)))
);
process(latest, defaultInactivityTimeoutInSec);
}
private void process(Map<EntityKeyType, Map<String, TsValue>> latest, long defaultInactivityTimeoutInSec) {
service.setDefaultInactivityTimeoutInSec(defaultInactivityTimeoutInSec);
service.setDefaultInactivityTimeoutMs(defaultInactivityTimeoutInSec * 1000);
service.setPersistToTelemetry(true);
var deviceUuid = UUID.randomUUID();
var deviceId = new DeviceId(deviceUuid);
DeviceStateData deviceStateData = service.toDeviceStateData(new EntityData(deviceId, latest, Map.of()), new DeviceIdInfo(TenantId.SYS_TENANT_ID.getId(), UUID.randomUUID(), deviceUuid));
Assert.assertEquals(5000L, deviceStateData.getState().getInactivityTimeout());
}
}

12
application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java

@ -63,15 +63,15 @@ public abstract class BaseRestApiLimitsTest extends AbstractControllerTest {
public void before() throws Exception {
loginSysAdmin();
tenantProfile = getDefaultTenantProfile();
logout();
resetTokens();
}
@After
public void after() throws Exception {
logout();
resetTokens();
loginSysAdmin();
saveTenantProfileWitConfiguration(tenantProfile, new DefaultTenantProfileConfiguration());
logout();
resetTokens();
service.shutdown();
}
@ -85,7 +85,7 @@ public abstract class BaseRestApiLimitsTest extends AbstractControllerTest {
saveTenantProfileWitConfiguration(tenantProfile, configurationWithCustomerRestLimits);
logout();
resetTokens();
loginCustomerUser();
@ -105,7 +105,7 @@ public abstract class BaseRestApiLimitsTest extends AbstractControllerTest {
saveTenantProfileWitConfiguration(tenantProfile, configurationWithTenantRestLimits);
logout();
resetTokens();
loginCustomerUser();
@ -125,7 +125,7 @@ public abstract class BaseRestApiLimitsTest extends AbstractControllerTest {
saveTenantProfileWitConfiguration(tenantProfile, configurationWithTenantRestLimits);
logout();
resetTokens();
loginTenantAdmin();

1
application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java

@ -35,6 +35,7 @@ public class TransportNoSqlTestSuite {
public static CustomCassandraCQLUnit cassandraUnit =
new CustomCassandraCQLUnit(
Arrays.asList(
new ClassPathCQLDataSet("cassandra/schema-keyspace.cql", false, false),
new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false),
new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false)
),

5
application/src/test/resources/application-test.properties

@ -59,4 +59,7 @@ queue.rule-engine.queues[2].processing-strategy.max-pause-between-retries=0
usage.stats.report.enabled=false
sql.audit_logs.partition_size=24
sql.ttl.audit_logs.ttl=2592000
sql.ttl.audit_logs.ttl=2592000
sql.edge_events.partition_size=168
sql.ttl.edge_events.edge_event_ttl=2592000

2
application/src/test/resources/logback-test.xml

@ -16,6 +16,8 @@
<logger name="org.cassandraunit" level="INFO"/>
<logger name="org.eclipse.leshan" level="INFO"/>
<!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors -->
<logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/>
<root level="WARN">
<appender-ref ref="console"/>

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.4.2-SNAPSHOT</version>
<version>3.4.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.4.2-SNAPSHOT</version>
<version>3.4.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

16
common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java

@ -17,9 +17,12 @@ package org.thingsboard.server.cache;
import lombok.Data;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.thingsboard.server.common.data.CacheConstants;
import javax.annotation.PostConstruct;
import java.util.Map;
@Configuration
@ -27,7 +30,20 @@ import java.util.Map;
@Data
public class CacheSpecsMap {
@Value("${security.jwt.refreshTokenExpTime:604800}")
private int refreshTokenExpTime;
@Getter
private Map<String, CacheSpecs> specs;
@PostConstruct
public void replaceTheJWTTokenRefreshExpTime() {
if (specs != null) {
var cacheSpecs = specs.get(CacheConstants.USERS_SESSION_INVALIDATION_CACHE);
if (cacheSpecs != null) {
cacheSpecs.setTimeToLiveInMinutes((refreshTokenExpTime / 60) + 1);
}
}
}
}

2
common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java

@ -20,6 +20,7 @@ import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.Ticker;
import com.github.benmanes.caffeine.cache.Weigher;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
@ -71,6 +72,7 @@ public class TbCaffeineCacheConfiguration {
}
private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) {
final Caffeine<Object, Object> caffeineBuilder
= Caffeine.newBuilder()
.weigher(collectionSafeWeigher())

34
common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2022 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.cache.usersUpdateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("UsersSessionInvalidation")
public class UsersSessionInvalidationCaffeineCache extends CaffeineTbTransactionalCache<String, Long> {
@Autowired
public UsersSessionInvalidationCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.USERS_SESSION_INVALIDATION_CACHE);
}
}

36
common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2022 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.cache.usersUpdateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbFSTRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("UsersSessionInvalidation")
public class UsersSessionInvalidationRedisCache extends RedisTbTransactionalCache<String, Long> {
@Autowired
public UsersSessionInvalidationRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.USERS_SESSION_INVALIDATION_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

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

Loading…
Cancel
Save