Browse Source

merge with develop/3.5

pull/8661/head
ShvaykaD 3 years ago
parent
commit
6266f6763e
  1. 77
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  2. 21
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  3. 7
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 114
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  5. 1
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  6. 137
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  7. 28
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  8. 71
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  9. 2
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  10. 6
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  11. 32
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  12. 2
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  13. 69
      application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java
  14. 3
      application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java
  15. 5
      application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java
  16. 1
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  17. 51
      application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java
  18. 123
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  19. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  20. 58
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  21. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java
  22. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java
  23. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
  24. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  25. 3
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  26. 5
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  27. 39
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  28. 13
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  29. 9
      application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
  30. 2
      application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java
  31. 10
      application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
  32. 6
      application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java
  33. 12
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  34. 11
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
  35. 11
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  36. 8
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  37. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java
  38. 15
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
  39. 73
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
  40. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  41. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
  42. 6
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  43. 23
      application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java
  44. 4
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  45. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  46. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  47. 46
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  48. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  49. 6
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  50. 44
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  51. 13
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  52. 46
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  53. 3
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  54. 24
      application/src/main/resources/thingsboard.yml
  55. 19
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java
  56. 72
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
  57. 22
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java
  58. 48
      application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java
  59. 56
      application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java
  60. 17
      application/src/test/java/org/thingsboard/server/controller/sql/TelemetryControllerSqlTest.java
  61. 1
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  62. 266
      application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java
  63. 31
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  64. 7
      application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java
  65. 98
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  66. 309
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  67. 25
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
  68. 10
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java
  69. 13
      application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java
  70. 55
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  71. 211
      application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java
  72. 36
      application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java
  73. 28
      application/src/test/resources/provision/x509ChainProvisionTest.pem
  74. 5
      common/cluster-api/src/main/proto/queue.proto
  75. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  76. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java
  77. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java
  78. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java
  79. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java
  80. 4
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  81. 3
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java
  82. 4
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  83. 6
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  84. 39
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java
  85. 4
      common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
  86. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java
  87. 19
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java
  88. 36
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java
  89. 4
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java
  90. 31
      common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantNotFoundException.java
  91. 2
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java
  92. 2
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java
  93. 1
      common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java
  94. 2
      common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java
  95. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  96. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java
  97. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java
  98. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntityActionNotificationInfo.java
  99. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
  100. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java

77
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

File diff suppressed because one or more lines are too long

21
application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -47,11 +47,15 @@ UPDATE alarm SET acknowledged = true, cleared = false WHERE status = 'ACTIVE_ACK
UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK'; UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK';
UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK'; UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK';
-- Drop index by 'status' column and replace with new one that has only active alarms; -- Drop index by 'status' column and replace with new indexes that has only active alarms;
DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active; DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active;
CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active
ON alarm USING btree (originator_id, type) WHERE cleared = false; ON alarm USING btree (originator_id, type) WHERE cleared = false;
DROP INDEX IF EXISTS idx_alarm_tenant_alarm_type_active;
CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_active
ON alarm USING btree (tenant_id, type) WHERE cleared = false;
-- Cover index by alarm type to optimize propagated alarm queries; -- Cover index by alarm type to optimize propagated alarm queries;
DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id; DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id;
CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm
@ -609,4 +613,17 @@ BEGIN
END END
$$; $$;
-- TTL DROP PARTITIONS FUNCTIONS UPDATE END -- TTL DROP PARTITIONS FUNCTIONS UPDATE END
-- RULE NODE SINGLETON MODE SUPPORT
ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS singleton_mode bool DEFAULT false;
UPDATE rule_node SET singleton_mode = true WHERE type IN ('org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode', 'org.thingsboard.rule.engine.mqtt.TbMqttNode');
ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS clustering_mode varchar(255) DEFAULT 'ENABLED';
UPDATE component_descriptor SET clustering_mode = 'USER_PREFERENCE' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.TbMqttNode';
UPDATE component_descriptor SET clustering_mode = 'SINGLETON' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode';

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

@ -87,9 +87,10 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
@ -179,6 +180,10 @@ public class ActorSystemContext {
@Setter @Setter
private ComponentDiscoveryService componentService; private ComponentDiscoveryService componentService;
@Autowired
@Getter
private DiscoveryService discoveryService;
@Autowired @Autowired
@Getter @Getter
private DataDecodingEncodingService encodingService; private DataDecodingEncodingService encodingService;

114
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -31,7 +31,10 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.gen.transport.TransportProtos;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -39,11 +42,10 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient;
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> { public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
private final String ruleChainName; private final String ruleChainName;
private final TbActorRef self;
private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private final DefaultTbContext defaultCtx;
private RuleNode ruleNode; private RuleNode ruleNode;
private TbNode tbNode; private TbNode tbNode;
private DefaultTbContext defaultCtx;
private RuleNodeInfo info; private RuleNodeInfo info;
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
@ -51,7 +53,6 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
super(systemContext, tenantId, ruleNodeId); super(systemContext, tenantId, ruleNodeId);
this.apiUsageClient = systemContext.getApiUsageClient(); this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChainName; this.ruleChainName = ruleChainName;
this.self = self;
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode)); this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode));
this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown"); this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown");
@ -59,29 +60,36 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override @Override
public void start(TbActorCtx context) throws Exception { public void start(TbActorCtx context) throws Exception {
tbNode = initComponent(ruleNode); if (isMyNodePartition()) {
if (tbNode != null) { tbNode = initComponent(ruleNode);
state = ComponentLifecycleState.ACTIVE; if (tbNode != null) {
state = ComponentLifecycleState.ACTIVE;
}
} }
} }
@Override @Override
public void onUpdate(TbActorCtx context) throws Exception { public void onUpdate(TbActorCtx context) throws Exception {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); if (isMyNodePartition()) {
this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown"); RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
boolean restartRequired = state != ComponentLifecycleState.ACTIVE || this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown");
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration())); boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
this.ruleNode = newRuleNode; !(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.defaultCtx.updateSelf(newRuleNode); this.ruleNode = newRuleNode;
if (restartRequired) { this.defaultCtx.updateSelf(newRuleNode);
if (tbNode != null) { if (restartRequired) {
tbNode.destroy(); if (tbNode != null) {
} tbNode.destroy();
try { }
start(context); try {
} catch (Exception e) { start(context);
throw new TbRuleNodeUpdateException("Failed to update rule node", e); } catch (Exception e) {
throw new TbRuleNodeUpdateException("Failed to update rule node", e);
}
} }
} else if (tbNode != null) {
stop(null);
tbNode = null;
} }
} }
@ -94,9 +102,16 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
@Override @Override
public void onPartitionChangeMsg(PartitionChangeMsg msg) { public void onPartitionChangeMsg(PartitionChangeMsg msg) throws Exception {
if (tbNode != null) { if (tbNode != null) {
tbNode.onPartitionChangeMsg(defaultCtx, msg); if (!isMyNodePartition()) {
stop(null);
tbNode = null;
} else {
tbNode.onPartitionChangeMsg(defaultCtx, msg);
}
} else if (isMyNodePartition()) {
start(null);
} }
} }
@ -121,23 +136,27 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception { void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
msg.getMsg().getCallback().onProcessingStart(info); if (!isMyNodePartition()) {
checkComponentStateActive(msg.getMsg()); putToNodePartition(msg.getMsg());
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
msg.getCtx().tellFailure(msg.getMsg(), e);
}
} else { } else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode)); msg.getMsg().getCallback().onProcessingStart(info);
checkComponentStateActive(msg.getMsg());
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
msg.getCtx().tellFailure(msg.getMsg(), e);
}
} else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
}
} }
} }
@ -160,4 +179,23 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
protected RuleNodeException getInactiveException() { protected RuleNodeException getInactiveException() {
return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode); return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode);
} }
private boolean isMyNodePartition() {
return !ruleNode.isSingletonMode()
|| systemContext.getDiscoveryService().isMonolith()
|| defaultCtx.isLocalEntity(ruleNode.getId());
}
//Message will return after processing. See RuleChainActorMessageProcessor.pushToTarget.
private void putToNodePartition(TbMsg source) {
TbMsg tbMsg = TbMsg.newMsg(source, source.getQueueName(), source.getRuleChainId(), entityId);
TopicPartitionInfo tpi = systemContext.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, ruleNode.getId());
TransportProtos.ToRuleEngineMsg toQueueMsg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg))
.build();
systemContext.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), toQueueMsg, null);
defaultCtx.ack(source);
}
} }

1
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -96,6 +96,7 @@ public class TenantActor extends RuleChainManagerActor {
log.info("[{}] Skip init of the rule chains due to API limits", tenantId); log.info("[{}] Skip init of the rule chains due to API limits", tenantId);
} }
} catch (Exception e) { } catch (Exception e) {
log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e);
cantFindTenant = true; cantFindTenant = true;
} }
} }

137
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -52,6 +53,10 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -85,9 +90,15 @@ public class AlarmController extends BaseController {
private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " + private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. "; "If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. ";
private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value"; private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value";
private static final String ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSearchStatus enumeration value";
private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK";
private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value";
private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK";
private static final String ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSeverity enumeration value";
private static final String ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES = "CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE";
private static final String ALARM_QUERY_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing alarm types";
private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status"; private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status";
private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'.";
@ -331,6 +342,132 @@ public class AlarmController extends BaseController {
} }
} }
@ApiOperation(value = "Get Alarms (getAlarmsV2)",
notes = "Returns a page of alarms for the selected entity. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAlarmsV2(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable(ENTITY_TYPE) String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_ID) String strEntityId,
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] statusList,
@ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] severityList,
@ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION)
@RequestParam(required = false) String[] typeList,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException {
checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
checkEntityId(entityId, Operation.READ);
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) {
for (String strStatus : statusList) {
if (!StringUtils.isEmpty(strStatus)) {
alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus));
}
}
}
List<AlarmSeverity> alarmSeverityList = new ArrayList<>();
if (severityList != null) {
for (String strSeverity : severityList) {
if (!StringUtils.isEmpty(strSeverity)) {
alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity));
}
}
}
List<String> alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList();
UserId assigneeUserId = null;
if (assigneeId != null) {
assigneeUserId = new UserId(UUID.fromString(assigneeId));
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
}
@ApiOperation(value = "Get All Alarms (getAllAlarmsV2)",
notes = "Returns a page of alarms that belongs to the current user owner. " +
"If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " +
"If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarms", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAllAlarmsV2(
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] statusList,
@ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] severityList,
@ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION)
@RequestParam(required = false) String[] typeList,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException {
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) {
for (String strStatus : statusList) {
if (!StringUtils.isEmpty(strStatus)) {
alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus));
}
}
}
List<AlarmSeverity> alarmSeverityList = new ArrayList<>();
if (severityList != null) {
for (String strSeverity : severityList) {
if (!StringUtils.isEmpty(strSeverity)) {
alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity));
}
}
}
List<String> alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList();
UserId assigneeUserId = null;
if (assigneeId != null) {
assigneeUserId = new UserId(UUID.fromString(assigneeId));
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
} else {
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
}
}
@ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)",
notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " +
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH

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

@ -103,7 +103,6 @@ import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.settings.UserDashboardAction;
import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
@ -130,12 +129,12 @@ import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserSettingsService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
@ -164,7 +163,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.mail.MessagingException; import javax.mail.MessagingException;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -395,16 +396,19 @@ public abstract class BaseController {
* Handles validation error for controller method arguments annotated with @{@link javax.validation.Valid} * Handles validation error for controller method arguments annotated with @{@link javax.validation.Valid}
* */ * */
@ExceptionHandler(MethodArgumentNotValidException.class) @ExceptionHandler(MethodArgumentNotValidException.class)
public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) { public void handleValidationError(MethodArgumentNotValidException validationError, HttpServletResponse response) {
String errorMessage = "Validation error: " + e.getFieldErrors().stream() List<ConstraintViolation<Object>> constraintsViolations = validationError.getFieldErrors().stream()
.map(fieldError -> { .map(fieldError -> {
String property = fieldError.getField(); try {
if (property.equals("valid") || StringUtils.endsWith(property, ".valid")) { // when custom @AssertTrue is used return (ConstraintViolation<Object>) fieldError.unwrap(ConstraintViolation.class);
property = ""; } catch (Exception e) {
log.warn("FieldError source is not of type ConstraintViolation");
return null; // should not happen
} }
return (!property.isEmpty() ? (property + " ") : "") + fieldError.getDefaultMessage();
}) })
.collect(Collectors.joining(", ")); .filter(Objects::nonNull)
.collect(Collectors.toList());
String errorMessage = "Validation error: " + ConstraintValidator.getErrorMessage(constraintsViolations);
ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS); ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
handleControllerException(thingsboardException, response); handleControllerException(thingsboardException, response);
} }
@ -793,8 +797,10 @@ public abstract class BaseController {
protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) { protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) {
EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType); EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType);
entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, if (!user.isSystemAdmin()) {
user.getCustomerId(), actionType, e); entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity,
user.getCustomerId(), actionType, e);
}
} }
protected <E extends HasName & HasId<? extends EntityId>> E doSaveAndLog(EntityType entityType, E entity, BiFunction<TenantId, E, E> savingFunction) throws Exception { protected <E extends HasName & HasId<? extends EntityId>> E doSaveAndLog(EntityType entityType, E entity, BiFunction<TenantId, E, E> savingFunction) throws Exception {

71
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -47,11 +47,13 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationSettingsService;
@ -64,7 +66,9 @@ import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -201,9 +205,6 @@ public class NotificationController extends BaseController {
public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request, public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request,
@RequestParam(defaultValue = "20") int recipientsPreviewSize, @RequestParam(defaultValue = "20") int recipientsPreviewSize,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
NotificationRequestPreview preview = new NotificationRequestPreview();
request.setOriginatorEntityId(user.getId());
NotificationTemplate template; NotificationTemplate template;
if (request.getTemplateId() != null) { if (request.getTemplateId() != null) {
template = checkEntityId(request.getTemplateId(), notificationTemplateService::findNotificationTemplateById, Operation.READ); template = checkEntityId(request.getTemplateId(), notificationTemplateService::findNotificationTemplateById, Operation.READ);
@ -213,56 +214,70 @@ public class NotificationController extends BaseController {
if (template == null) { if (template == null) {
throw new IllegalArgumentException("Template is missing"); throw new IllegalArgumentException("Template is missing");
} }
NotificationProcessingContext tmpProcessingCtx = NotificationProcessingContext.builder() request.setOriginatorEntityId(user.getId());
.tenantId(user.getTenantId()) List<NotificationTarget> targets = request.getTargets().stream()
.request(request) .map(NotificationTargetId::new)
.template(template) .map(targetId -> notificationTargetService.findNotificationTargetById(user.getTenantId(), targetId))
.settings(null) .sorted(Comparator.comparing(target -> target.getConfiguration().getType()))
.build(); .collect(Collectors.toList());
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = tmpProcessingCtx.getDeliveryMethods().stream() NotificationRequestPreview preview = new NotificationRequestPreview();
.collect(Collectors.toMap(m -> m, deliveryMethod -> {
NotificationRecipient recipient = null;
if (NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods().contains(deliveryMethod)) {
recipient = userService.findUserById(user.getTenantId(), user.getId());
}
return tmpProcessingCtx.getProcessedTemplate(deliveryMethod, recipient);
}));
preview.setProcessedTemplates(processedTemplates);
// generic permission
Set<String> recipientsPreview = new LinkedHashSet<>(); Set<String> recipientsPreview = new LinkedHashSet<>();
Map<String, Integer> recipientsCountByTarget = new HashMap<>(); Map<String, Integer> recipientsCountByTarget = new LinkedHashMap<>();
Map<NotificationTargetType, NotificationRecipient> firstRecipient = new HashMap<>();
List<NotificationTarget> targets = notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(),
request.getTargets().stream().map(NotificationTargetId::new).collect(Collectors.toList()));
for (NotificationTarget target : targets) { for (NotificationTarget target : targets) {
int recipientsCount; int recipientsCount;
List<NotificationRecipient> recipientsPart; List<NotificationRecipient> recipientsPart;
if (target.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) { NotificationTargetType targetType = target.getConfiguration().getType();
if (targetType == NotificationTargetType.PLATFORM_USERS) {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(),
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize)); (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null,
new SortOrder("createdTime", SortOrder.Direction.DESC)));
recipientsCount = (int) recipients.getTotalElements(); recipientsCount = (int) recipients.getTotalElements();
recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList());
} else { } else {
recipientsCount = 1; recipientsCount = 1;
recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation()); recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation());
} }
firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null);
for (NotificationRecipient recipient : recipientsPart) { for (NotificationRecipient recipient : recipientsPart) {
if (recipientsPreview.size() < recipientsPreviewSize) { if (recipientsPreview.size() < recipientsPreviewSize) {
recipientsPreview.add(recipient.getTitle()); String title = recipient.getTitle();
if (recipient instanceof SlackConversation) {
title = ((SlackConversation) recipient).getPointer() + title;
} else if (recipient instanceof User) {
if (!title.equals(recipient.getEmail())) {
title += " (" + recipient.getEmail() + ")";
}
}
recipientsPreview.add(title);
} else { } else {
break; break;
} }
} }
recipientsCountByTarget.put(target.getName(), recipientsCount); recipientsCountByTarget.put(target.getName(), recipientsCount);
} }
preview.setRecipientsPreview(recipientsPreview); preview.setRecipientsPreview(recipientsPreview);
preview.setRecipientsCountByTarget(recipientsCountByTarget); preview.setRecipientsCountByTarget(recipientsCountByTarget);
preview.setTotalRecipientsCount(recipientsCountByTarget.values().stream().mapToInt(Integer::intValue).sum()); preview.setTotalRecipientsCount(recipientsCountByTarget.values().stream().mapToInt(Integer::intValue).sum());
Set<NotificationDeliveryMethod> deliveryMethods = template.getConfiguration().getDeliveryMethodsTemplates().entrySet()
.stream().filter(entry -> entry.getValue().isEnabled()).map(Map.Entry::getKey).collect(Collectors.toSet());
NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(user.getTenantId())
.request(request)
.deliveryMethods(deliveryMethods)
.template(template)
.settings(null)
.build();
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = ctx.getDeliveryMethods().stream()
.collect(Collectors.toMap(m -> m, deliveryMethod -> {
NotificationTargetType targetType = NotificationTargetType.forDeliveryMethod(deliveryMethod);
return ctx.getProcessedTemplate(deliveryMethod, firstRecipient.get(targetType));
}));
preview.setProcessedTemplates(processedTemplates);
return preview; return preview;
} }

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

@ -71,7 +71,7 @@ public class NotificationTargetController extends BaseController {
private final NotificationTargetService notificationTargetService; private final NotificationTargetService notificationTargetService;
@ApiOperation(value = "Save notification target (saveNotificationTarget)", @ApiOperation(value = "Save notification target (saveNotificationTarget)",
notes = "Create or update notification target.\n\n" + notes = "Create or update notification target." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/target") @PostMapping("/target")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")

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

@ -75,16 +75,15 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.exception.InvalidParametersException;
import org.thingsboard.server.exception.UncheckedApiException;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.AttributeData; import org.thingsboard.server.service.telemetry.AttributeData;
import org.thingsboard.server.service.telemetry.TsData; import org.thingsboard.server.service.telemetry.TsData;
import org.thingsboard.server.exception.InvalidParametersException;
import org.thingsboard.server.exception.UncheckedApiException;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -624,7 +623,6 @@ public class TelemetryController extends BaseController {
} }
if (json.isObject()) { if (json.isObject()) {
List<AttributeKvEntry> attributes = extractRequestAttributes(json); List<AttributeKvEntry> attributes = extractRequestAttributes(json);
attributes.forEach(ConstraintValidator::validateFields);
if (attributes.isEmpty()) { if (attributes.isEmpty()) {
return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
} }

32
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -41,7 +41,10 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -53,6 +56,7 @@ import java.util.stream.Collectors;
public class EntityActionService { public class EntityActionService {
private final TbClusterService tbClusterService; private final TbClusterService tbClusterService;
private final AuditLogService auditLogService; private final AuditLogService auditLogService;
private final NotificationRuleProcessor notificationRuleProcessor;
private static final ObjectMapper json = new ObjectMapper(); private static final ObjectMapper json = new ObjectMapper();
@ -87,11 +91,11 @@ public class EntityActionService {
case ALARM_CLEAR: case ALARM_CLEAR:
msgType = DataConstants.ALARM_CLEAR; msgType = DataConstants.ALARM_CLEAR;
break; break;
case ALARM_ASSIGN: case ALARM_ASSIGNED:
msgType = DataConstants.ALARM_ASSIGN; msgType = DataConstants.ALARM_ASSIGNED;
break; break;
case ALARM_UNASSIGN: case ALARM_UNASSIGNED:
msgType = DataConstants.ALARM_UNASSIGN; msgType = DataConstants.ALARM_UNASSIGNED;
break; break;
case ALARM_DELETE: case ALARM_DELETE:
msgType = DataConstants.ALARM_DELETE; msgType = DataConstants.ALARM_DELETE;
@ -233,12 +237,28 @@ public class EntityActionService {
entityNode = json.valueToTree(extractParameter(EntityRelation.class, 0, additionalInfo)); entityNode = json.valueToTree(extractParameter(EntityRelation.class, 0, additionalInfo));
} }
} }
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode));
if (tenantId == null || tenantId.isNullUid()) { if (tenantId == null || tenantId.isNullUid()) {
if (entity instanceof HasTenantId) { if (entity instanceof HasTenantId) {
tenantId = ((HasTenantId) entity).getTenantId(); tenantId = ((HasTenantId) entity).getTenantId();
} }
} }
if (tenantId != null && !tenantId.isSysTenantId()) {
if (actionType == ActionType.ADDED) {
notificationRuleProcessor.process(EntitiesLimitTrigger.builder()
.tenantId(tenantId)
.entityType(entityId.getEntityType())
.build());
}
notificationRuleProcessor.process(EntityActionTrigger.builder()
.tenantId(tenantId)
.entityId(entityId)
.entity(entity)
.actionType(actionType)
.user(user)
.build());
}
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e); log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e);
@ -247,7 +267,7 @@ public class EntityActionService {
} }
public <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId, public <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) { ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) { if (customerId == null || customerId.isNullUid()) {
customerId = user.getCustomerId(); customerId = user.getCustomerId();
} }

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

@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;

69
application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java

@ -15,54 +15,81 @@
*/ */
package org.thingsboard.server.service.apiusage.limits; package org.thingsboard.server.service.apiusage.limits;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.Map; import javax.annotation.PostConstruct;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class DefaultRateLimitService implements RateLimitService { public class DefaultRateLimitService implements RateLimitService {
private final TbTenantProfileCache tenantProfileCache; private final TbTenantProfileCache tenantProfileCache;
@Value("${cache.rateLimits.timeToLiveInMinutes:60}")
private int rateLimitsTtl;
@Value("${cache.rateLimits.maxSize:100000}")
private int rateLimitsCacheMaxSize;
private final Map<LimitedApi, Map<TenantId, TbRateLimits>> rateLimits = new ConcurrentHashMap<>(); private Cache<RateLimitKey, TbRateLimits> rateLimits;
@PostConstruct
private void init() {
rateLimits = Caffeine.newBuilder()
.expireAfterAccess(rateLimitsTtl, TimeUnit.MINUTES)
.maximumSize(rateLimitsCacheMaxSize)
.build();
}
@Override
public boolean checkRateLimit(LimitedApi api, TenantId tenantId) {
return checkRateLimit(api, tenantId, tenantId);
}
@Override @Override
public boolean checkRateLimit(TenantId tenantId, LimitedApi api) { public boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId) {
if (tenantId.isSysTenantId()) { if (tenantId.isSysTenantId()) {
return true; return true;
} }
RateLimitKey key = new RateLimitKey(api, entityId);
String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration() String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration()
.map(api::getLimitConfig).orElse(null); .map(api::getLimitConfig).orElse(null);
Map<TenantId, TbRateLimits> rateLimits = this.rateLimits.get(api);
if (StringUtils.isEmpty(rateLimitConfig)) { if (StringUtils.isEmpty(rateLimitConfig)) {
if (rateLimits != null) { rateLimits.invalidate(key);
rateLimits.remove(tenantId);
if (rateLimits.isEmpty()) {
this.rateLimits.remove(api);
}
}
return true; return true;
} }
log.trace("[{}] Checking rate limit for {} ({})", entityId, api, rateLimitConfig);
if (rateLimits == null) { TbRateLimits rateLimit = rateLimits.asMap().compute(key, (k, limit) -> {
rateLimits = new ConcurrentHashMap<>(); if (limit == null || !limit.getConfiguration().equals(rateLimitConfig)) {
this.rateLimits.put(api, rateLimits); limit = new TbRateLimits(rateLimitConfig);
} log.trace("[{}] Created new rate limit bucket for {} ({})", entityId, api, rateLimitConfig);
TbRateLimits rateLimit = rateLimits.get(tenantId); }
if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) { return limit;
rateLimit = new TbRateLimits(rateLimitConfig); });
rateLimits.put(tenantId, rateLimit); boolean success = rateLimit.tryConsume();
if (!success) {
log.debug("[{}] Rate limit exceeded for {} ({})", entityId, api, rateLimitConfig);
} }
return success;
}
return rateLimit.tryConsume(); @Data(staticConstructor = "of")
private static class RateLimitKey {
private final LimitedApi api;
private final EntityId entityId;
} }
} }

3
application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java

@ -25,7 +25,8 @@ public enum LimitedApi {
ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit), ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit),
ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit), ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit),
NOTIFICATION_REQUEST(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit); NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit),
NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit);
private final Function<DefaultTenantProfileConfiguration, String> configExtractor; private final Function<DefaultTenantProfileConfiguration, String> configExtractor;

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

@ -15,10 +15,13 @@
*/ */
package org.thingsboard.server.service.apiusage.limits; package org.thingsboard.server.service.apiusage.limits;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
public interface RateLimitService { public interface RateLimitService {
boolean checkRateLimit(TenantId tenantId, LimitedApi api); boolean checkRateLimit(LimitedApi api, TenantId tenantId);
boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId);
} }

1
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java

@ -155,6 +155,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope()); scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
ObjectNode configurationDescriptor = mapper.createObjectNode(); ObjectNode configurationDescriptor = mapper.createObjectNode();
JsonNode node = mapper.valueToTree(nodeDefinition); JsonNode node = mapper.valueToTree(nodeDefinition);

51
application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java

@ -90,35 +90,32 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
@Override @Override
public ListenableFuture<Void> registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs) { public ListenableFuture<Void> registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs) {
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); Device device = deviceService.findDeviceById(tenantId, deviceId);
return Futures.transformAsync(deviceFuture, device -> { Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE);
Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); List<Object> key = constructCacheKey(device.getId());
List<Object> key = constructCacheKey(device.getId()); if (isAllowedClaimingByDefault) {
if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
if (isAllowedClaimingByDefault) { persistInCache(secretKey, durationMs, cache, key);
if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { return Futures.immediateFuture(null);
persistInCache(secretKey, durationMs, cache, key); }
return Futures.immediateFuture(null); log.warn("The device [{}] has been already claimed!", device.getName());
return Futures.immediateFailedFuture(new IllegalArgumentException());
} else {
ListenableFuture<List<AttributeKvEntry>> claimingAllowedFuture = attributesService.find(tenantId, device.getId(),
DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME));
return Futures.transform(claimingAllowedFuture, list -> {
if (list != null && !list.isEmpty()) {
Optional<Boolean> claimingAllowedOptional = list.get(0).getBooleanValue();
if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get()
&& device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
persistInCache(secretKey, durationMs, cache, key);
return null;
}
} }
log.warn("The device [{}] has been already claimed!", device.getName()); log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName());
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} else { }, MoreExecutors.directExecutor());
ListenableFuture<List<AttributeKvEntry>> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), }
DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME));
return Futures.transform(claimingAllowedFuture, list -> {
if (list != null && !list.isEmpty()) {
Optional<Boolean> claimingAllowedOptional = list.get(0).getBooleanValue();
if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get()
&& device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
persistInCache(secretKey, durationMs, cache, key);
return null;
}
}
log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName());
throw new IllegalArgumentException();
}, MoreExecutors.directExecutor());
}
}, MoreExecutors.directExecutor());
} }
private ListenableFuture<ClaimDataInfo> getClaimData(Cache cache, Device device) { private ListenableFuture<ClaimDataInfo> getClaimData(Cache cache, Device device) {

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

@ -20,15 +20,16 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
@ -36,15 +37,16 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.transport.util.SslUtil;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProfileDao;
import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
@ -59,12 +61,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service @Service
@ -77,35 +80,56 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
private static final String DEVICE_PROVISION_STATE = "provisionState"; private static final String DEVICE_PROVISION_STATE = "provisionState";
private static final String PROVISIONED_STATE = "provisioned"; private static final String PROVISIONED_STATE = "provisioned";
@Autowired private final TbClusterService clusterService;
TbClusterService clusterService; private final DeviceProfileService deviceProfileService;
private final DeviceService deviceService;
private final DeviceCredentialsService deviceCredentialsService;
private final AttributesService attributesService;
private final AuditLogService auditLogService;
private final PartitionService partitionService;
@Autowired public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider, TbClusterService clusterService, DeviceProfileService deviceProfileService, DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, AttributesService attributesService, AuditLogService auditLogService, PartitionService partitionService) {
DeviceDao deviceDao;
@Autowired
DeviceProfileDao deviceProfileDao;
@Autowired
DeviceService deviceService;
@Autowired
DeviceCredentialsService deviceCredentialsService;
@Autowired
AttributesService attributesService;
@Autowired
DeviceStateService deviceStateService;
@Autowired
AuditLogService auditLogService;
@Autowired
PartitionService partitionService;
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) {
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
this.clusterService = clusterService;
this.deviceProfileService = deviceProfileService;
this.deviceService = deviceService;
this.deviceCredentialsService = deviceCredentialsService;
this.attributesService = attributesService;
this.auditLogService = auditLogService;
this.partitionService = partitionService;
}
@Override
public ProvisionResponse provisionDeviceViaX509Chain(DeviceProfile targetProfile, ProvisionRequest provisionRequest) throws ProvisionFailedException {
if (targetProfile == null) {
throw new ProvisionFailedException("Device profile is not specified!");
}
if (!DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(targetProfile.getProfileData().getProvisionConfiguration().getType())) {
throw new ProvisionFailedException("Device profile provision strategy is not X509_CERTIFICATE_CHAIN!");
}
X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration();
String certificateValue = provisionRequest.getCredentialsData().getX509CertHash();
String certificateRegEx = configuration.getCertificateRegExPattern();
String commonName = getCNFromX509Certificate(targetProfile, certificateValue);
String deviceName = extractDeviceNameFromCNByRegEx(targetProfile, commonName, certificateRegEx);
provisionRequest.setDeviceName(deviceName);
Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName());
X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration();
if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) {
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(targetDevice.getTenantId(), targetDevice.getId());
if (DeviceCredentialsType.X509_CERTIFICATE.equals(deviceCredentials.getCredentialsType())) {
String updatedDeviceCertificateValue = provisionRequest.getCredentialsData().getX509CertHash();
deviceCredentials = updateDeviceCredentials(targetDevice.getTenantId(), deviceCredentials,
updatedDeviceCertificateValue, DeviceCredentialsType.X509_CERTIFICATE);
}
return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS);
} else if (x509Configuration.isAllowCreateNewDevicesByX509Certificate()) {
return createDevice(provisionRequest, targetProfile);
} else {
log.warn("[{}][{}] Device with name {} doesn't exist and cannot be created due incorrect configuration for X509CertificateChainProvisionConfiguration",
targetProfile.getTenantId(), targetProfile.getId(), provisionRequest.getDeviceName());
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
} }
@Override @Override
@ -124,14 +148,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
} }
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey); DeviceProfile targetProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequestKey);
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null || if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) { targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
} }
Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null); Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName());
switch (targetProfile.getProvisionType()) { switch (targetProfile.getProvisionType()) {
case ALLOW_CREATE_NEW_DEVICES: case ALLOW_CREATE_NEW_DEVICES:
@ -155,6 +179,8 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
} }
} }
break; break;
case X509_CERTIFICATE_CHAIN:
throw new ProvisionFailedException("Invalid provision strategy type!");
} }
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
} }
@ -209,6 +235,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
} }
} }
private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue,
DeviceCredentialsType credentialsType) {
log.trace("Updating device credentials [{}] with certificate value [{}]", deviceCredentials, certificateValue);
deviceCredentials.setCredentialsValue(certificateValue);
deviceCredentials.setCredentialsType(credentialsType);
return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials);
}
private ListenableFuture<List<String>> saveProvisionStateAttribute(Device device) { private ListenableFuture<List<String>> saveProvisionStateAttribute(Device device) {
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
@ -257,4 +291,27 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE; ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest); auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
} }
private String getCNFromX509Certificate(DeviceProfile profile, String x509Value) {
try {
return SslUtil.parseCommonName(SslUtil.readCertFile(x509Value));
} catch (Exception e) {
log.trace("[{}][{}] Failed to parse CN from X509 certificate {}", profile.getTenantId(), profile.getId(), x509Value);
return null;
}
}
public String extractDeviceNameFromCNByRegEx(DeviceProfile profile, String commonName, String regex) throws ProvisionFailedException {
try {
log.trace("Extract device name from CN [{}] by regex pattern [{}]", commonName, regex);
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(commonName);
if (matcher.find()) {
return matcher.group(1);
}
} catch (Exception ignored) {}
log.trace("[{}][{}] Failed to match device name using [{}] from CN: [{}]", profile.getTenantId(), profile.getId(), regex, commonName);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
} }

12
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -98,11 +98,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private String privateKeyResource; private String privateKeyResource;
@Value("${edges.state.persistToTelemetry:false}") @Value("${edges.state.persistToTelemetry:false}")
private boolean persistToTelemetry; private boolean persistToTelemetry;
@Value("${edges.rpc.client_max_keep_alive_time_sec}") @Value("${edges.rpc.client_max_keep_alive_time_sec:1}")
private int clientMaxKeepAliveTimeSec; private int clientMaxKeepAliveTimeSec;
@Value("${edges.rpc.max_inbound_message_size:4194304}") @Value("${edges.rpc.max_inbound_message_size:4194304}")
private int maxInboundMessageSize; private int maxInboundMessageSize;
@Value("${edges.rpc.keep_alive_time_sec:10}")
private int keepAliveTimeSec;
@Value("${edges.rpc.keep_alive_timeout_sec:5}")
private int keepAliveTimeoutSec;
@Value("${edges.scheduler_pool_size}") @Value("${edges.scheduler_pool_size}")
private int schedulerPoolSize; private int schedulerPoolSize;
@ -131,6 +134,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.info("Initializing Edge RPC service!"); log.info("Initializing Edge RPC service!");
NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort) NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort)
.permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS)
.keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS)
.keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS)
.permitKeepAliveWithoutCalls(true)
.maxInboundMessageSize(maxInboundMessageSize) .maxInboundMessageSize(maxInboundMessageSize)
.addService(this); .addService(this);
if (sslEnabled) { if (sslEnabled) {
@ -282,7 +288,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
if (session != null) { if (session != null) {
boolean success = false; boolean success = false;
if (session.isConnected()) { if (session.isConnected()) {
session.startSyncProcess(tenantId, edgeId, true); session.startSyncProcess(true);
success = true; success = true;
} }
clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success)); clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success));

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

@ -148,7 +148,7 @@ public final class EdgeGrpcSession implements Closeable {
if (requestMsg.getSyncRequestMsg().hasFullSync()) { if (requestMsg.getSyncRequestMsg().hasFullSync()) {
fullSync = requestMsg.getSyncRequestMsg().getFullSync(); fullSync = requestMsg.getSyncRequestMsg().getFullSync();
} }
startSyncProcess(edge.getTenantId(), edge.getId(), fullSync); startSyncProcess(fullSync);
} else { } else {
syncCompleted = true; syncCompleted = true;
} }
@ -192,10 +192,10 @@ public final class EdgeGrpcSession implements Closeable {
}; };
} }
public void startSyncProcess(TenantId tenantId, EdgeId edgeId, boolean fullSync) { public void startSyncProcess(boolean fullSync) {
log.trace("[{}][{}] Staring edge sync process", tenantId, edgeId); log.trace("[{}][{}][{}] Staring edge sync process", edge.getTenantId(), edge.getId(), this.sessionId);
syncCompleted = false; syncCompleted = false;
interruptGeneralProcessingOnSync(tenantId, edgeId); interruptGeneralProcessingOnSync();
doSync(new EdgeSyncCursor(ctx, edge, fullSync)); doSync(new EdgeSyncCursor(ctx, edge, fullSync));
} }
@ -221,9 +221,9 @@ public final class EdgeGrpcSession implements Closeable {
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build())
.build(); .build();
Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<Void>() { Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() {
@Override @Override
public void onSuccess(Void result) { public void onSuccess(Boolean isInterrupted) {
syncCompleted = true; syncCompleted = true;
ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId()); ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId());
} }
@ -272,7 +272,7 @@ public final class EdgeGrpcSession implements Closeable {
} }
if (sessionState.getPendingMsgsMap().isEmpty()) { if (sessionState.getPendingMsgsMap().isEmpty()) {
log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey()); log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey());
stopCurrentSendDownlinkMsgsTask(null); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e); log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e);
@ -367,14 +367,19 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && !pageData.getData().isEmpty()) { if (isConnected() && !pageData.getData().isEmpty()) {
log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size()); log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData());
Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<Void>() { Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable Void tmp) { public void onSuccess(@Nullable Boolean isInterrupted) {
if (isConnected() && pageData.hasNext()) { if (Boolean.TRUE.equals(isInterrupted)) {
processEdgeEvents(fetcher, pageLink.nextPageLink(), result); log.debug("[{}][{}][{}] Send downlink messages task was interrupted", edge.getTenantId(), edge.getId(), sessionId);
result.set(null);
} else { } else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); if (isConnected() && pageData.hasNext()) {
result.set(ifOffset); processEdgeEvents(fetcher, pageLink.nextPageLink(), result);
} else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId();
result.set(ifOffset);
}
} }
} }
@ -394,7 +399,7 @@ public final class EdgeGrpcSession implements Closeable {
} }
} }
private ListenableFuture<Void> sendDownlinkMsgsPack(List<DownlinkMsg> downlinkMsgsPack) { private ListenableFuture<Boolean> sendDownlinkMsgsPack(List<DownlinkMsg> downlinkMsgsPack) {
interruptPreviousSendDownlinkMsgsTask(); interruptPreviousSendDownlinkMsgsTask();
sessionState.setSendDownlinkMsgsFuture(SettableFuture.create()); sessionState.setSendDownlinkMsgsFuture(SettableFuture.create());
@ -433,14 +438,14 @@ public final class EdgeGrpcSession implements Closeable {
} else { } else {
log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}",
this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy);
stopCurrentSendDownlinkMsgsTask(null); stopCurrentSendDownlinkMsgsTask(false);
} }
} else { } else {
stopCurrentSendDownlinkMsgsTask(null); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to send downlink msgs. Error msg {}", this.sessionId, e.getMessage(), e); log.warn("[{}] Failed to send downlink msgs. Error msg {}", this.sessionId, e.getMessage(), e);
stopCurrentSendDownlinkMsgsTask(e); stopCurrentSendDownlinkMsgsTask(true);
} }
}; };
@ -688,23 +693,18 @@ public final class EdgeGrpcSession implements Closeable {
} }
private void interruptPreviousSendDownlinkMsgsTask() { private void interruptPreviousSendDownlinkMsgsTask() {
String msg = String.format("[%s] Previous send downlink future was not properly completed, stopping it now!", this.sessionId); log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); stopCurrentSendDownlinkMsgsTask(true);
} }
private void interruptGeneralProcessingOnSync(TenantId tenantId, EdgeId edgeId) { private void interruptGeneralProcessingOnSync() {
String msg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId); log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); stopCurrentSendDownlinkMsgsTask(true);
} }
public void stopCurrentSendDownlinkMsgsTask(Exception e) { public void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) {
if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) { if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) {
if (e != null) { sessionState.getSendDownlinkMsgsFuture().set(isInterrupted);
log.debug(e.getMessage());
sessionState.getSendDownlinkMsgsFuture().setException(e);
} else {
sessionState.getSendDownlinkMsgsFuture().set(null);
}
} }
if (sessionState.getScheduledSendDownlinkTask() != null) { if (sessionState.getScheduledSendDownlinkTask() != null) {
sessionState.getScheduledSendDownlinkTask().cancel(true); sessionState.getScheduledSendDownlinkTask().cancel(true);

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

@ -28,6 +28,6 @@ import java.util.concurrent.ScheduledFuture;
public class EdgeSessionState { public class EdgeSessionState {
private final Map<Integer, DownlinkMsg> pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); private final Map<Integer, DownlinkMsg> pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>());
private SettableFuture<Void> sendDownlinkMsgsFuture; private SettableFuture<Boolean> sendDownlinkMsgsFuture;
private ScheduledFuture<?> scheduledSendDownlinkTask; private ScheduledFuture<?> scheduledSendDownlinkTask;
} }

7
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java

@ -16,7 +16,6 @@
package org.thingsboard.server.service.edge.rpc.processor.device; package org.thingsboard.server.service.edge.rpc.processor.device;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -108,8 +107,8 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
public ListenableFuture<Void> processDeviceCredentialsMsg(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { public ListenableFuture<Void> processDeviceCredentialsMsg(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) {
log.debug("[{}] Executing processDeviceCredentialsMsg, deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentialsUpdateMsg); log.debug("[{}] Executing processDeviceCredentialsMsg, deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentialsUpdateMsg);
DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB())); DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB()));
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); return dbCallbackExecutorService.submit(() -> {
return Futures.transform(deviceFuture, device -> { Device device = deviceService.findDeviceById(tenantId, deviceId);
if (device != null) { if (device != null) {
log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]",
device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue());
@ -129,6 +128,6 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
log.warn("Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", deviceId, deviceCredentialsUpdateMsg); log.warn("Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", deviceId, deviceCredentialsUpdateMsg);
} }
return null; return null;
}, dbCallbackExecutorService); });
} }
} }

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

@ -309,10 +309,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
return EdgeEventActionType.ALARM_ACK; return EdgeEventActionType.ALARM_ACK;
case ALARM_CLEAR: case ALARM_CLEAR:
return EdgeEventActionType.ALARM_CLEAR; return EdgeEventActionType.ALARM_CLEAR;
case ALARM_ASSIGN: case ALARM_ASSIGNED:
return EdgeEventActionType.ALARM_ASSIGN; return EdgeEventActionType.ALARM_ASSIGNED;
case ALARM_UNASSIGN: case ALARM_UNASSIGNED:
return EdgeEventActionType.ALARM_UNASSIGN; return EdgeEventActionType.ALARM_UNASSIGNED;
case DELETED: case DELETED:
return EdgeEventActionType.DELETED; return EdgeEventActionType.DELETED;
case RELATION_ADD_OR_UPDATE: case RELATION_ADD_OR_UPDATE:

4
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -175,7 +175,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
} catch (ThingsboardException e) { } catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e); log.error("Failed to save alarm comment", e);
} }
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGN, user); notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGNED, user);
} else { } else {
throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
@ -203,7 +203,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
} catch (ThingsboardException e) { } catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e); log.error("Failed to save alarm comment", e);
} }
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGN, user); notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user);
} else { } else {
throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }

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

@ -82,6 +82,9 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ..."); log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ...");
clearCacheByName("repositorySettings"); clearCacheByName("repositorySettings");
break; break;
case "3.4.4":
log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0");
clearCacheByName("deviceProfiles");
default: default:
//Do nothing, since cache cleanup is optional. //Do nothing, since cache cleanup is optional.
} }

5
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -364,6 +364,11 @@ public class DefaultMailService implements MailService {
mailSender.testConnection(); mailSender.testConnection();
} }
@Override
public boolean isConfigured(TenantId tenantId) {
return mailSender != null;
}
private String toEnabledValueLabel(ApiFeature apiFeature) { private String toEnabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) { switch (apiFeature) {
case DB: case DB:

39
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -107,8 +107,10 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
@Override @Override
public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest request, Consumer<NotificationRequestStats> callback) { public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest request, Consumer<NotificationRequestStats> callback) {
if (!rateLimitService.checkRateLimit(tenantId, LimitedApi.NOTIFICATION_REQUEST)) { if (request.getRuleId() == null) {
throw new TbRateLimitsException(EntityType.TENANT); if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS, tenantId)) {
throw new TbRateLimitsException(EntityType.TENANT);
}
} }
NotificationTemplate notificationTemplate; NotificationTemplate notificationTemplate;
@ -119,21 +121,33 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
} }
if (notificationTemplate == null) throw new IllegalArgumentException("Template is missing"); if (notificationTemplate == null) throw new IllegalArgumentException("Template is missing");
Set<NotificationDeliveryMethod> deliveryMethods = new HashSet<>();
List<NotificationTarget> targets = request.getTargets().stream().map(NotificationTargetId::new) List<NotificationTarget> targets = request.getTargets().stream().map(NotificationTargetId::new)
.map(id -> notificationTargetService.findNotificationTargetById(tenantId, id)).collect(Collectors.toList()); .map(id -> notificationTargetService.findNotificationTargetById(tenantId, id))
.collect(Collectors.toList());
NotificationRuleId ruleId = request.getRuleId(); NotificationRuleId ruleId = request.getRuleId();
notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> { notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> {
if (!template.isEnabled()) return; if (!template.isEnabled()) return;
if (!channels.get(deliveryMethod).check(tenantId)) { try {
throw new IllegalArgumentException("Unable to send notification via " + deliveryMethod.getName() + ": not configured or not working"); channels.get(deliveryMethod).check(tenantId);
} catch (Exception e) {
if (ruleId == null) {
throw new IllegalArgumentException(e.getMessage());
} else {
return; // if originated by rule - just ignore delivery method
}
} }
if (ruleId == null) { if (ruleId == null) {
if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) { if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) {
throw new IllegalArgumentException("Target for " + deliveryMethod.getName() + " delivery method is missing"); throw new IllegalArgumentException("Recipients for " + deliveryMethod.getName() + " delivery method not chosen");
} }
} }
deliveryMethods.add(deliveryMethod);
}); });
if (deliveryMethods.isEmpty()) {
throw new IllegalArgumentException("No delivery methods to send notification with");
}
if (request.getAdditionalConfig() != null) { if (request.getAdditionalConfig() != null) {
NotificationRequestConfig config = request.getAdditionalConfig(); NotificationRequestConfig config = request.getAdditionalConfig();
@ -153,6 +167,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
NotificationProcessingContext ctx = NotificationProcessingContext.builder() NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(tenantId) .tenantId(tenantId)
.request(request) .request(request)
.deliveryMethods(deliveryMethods)
.template(notificationTemplate) .template(notificationTemplate)
.settings(settings) .settings(settings)
.build(); .build();
@ -341,14 +356,20 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
@Override @Override
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(TenantId tenantId) { public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(TenantId tenantId) {
return channels.values().stream() return channels.values().stream()
.filter(channel -> channel.check(tenantId)) .filter(channel -> {
try {
channel.check(tenantId);
return true;
} catch (Exception e) {
return false;
}
})
.map(NotificationChannel::getDeliveryMethod) .map(NotificationChannel::getDeliveryMethod)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@Override @Override
public boolean check(TenantId tenantId) { public void check(TenantId tenantId) throws Exception {
return true;
} }
@Override @Override

13
application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java

@ -48,20 +48,21 @@ public class NotificationProcessingContext {
private final NotificationSettings settings; private final NotificationSettings settings;
@Getter @Getter
private final NotificationRequest request; private final NotificationRequest request;
@Getter
private final Set<NotificationDeliveryMethod> deliveryMethods;
@Getter @Getter
private final NotificationTemplate notificationTemplate; private final NotificationTemplate notificationTemplate;
private final Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates; private final Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates;
@Getter @Getter
private Set<NotificationDeliveryMethod> deliveryMethods;
@Getter
private final NotificationRequestStats stats; private final NotificationRequestStats stats;
@Builder @Builder
public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, NotificationTemplate template, NotificationSettings settings) { public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, Set<NotificationDeliveryMethod> deliveryMethods,
NotificationTemplate template, NotificationSettings settings) {
this.tenantId = tenantId; this.tenantId = tenantId;
this.request = request; this.request = request;
this.deliveryMethods = deliveryMethods;
this.settings = settings; this.settings = settings;
this.notificationTemplate = template; this.notificationTemplate = template;
this.templates = new EnumMap<>(NotificationDeliveryMethod.class); this.templates = new EnumMap<>(NotificationDeliveryMethod.class);
@ -77,7 +78,6 @@ public class NotificationProcessingContext {
templates.put(deliveryMethod, template); templates.put(deliveryMethod, template);
} }
}); });
deliveryMethods = templates.keySet();
} }
public <C extends NotificationDeliveryMethodConfig> C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) { public <C extends NotificationDeliveryMethodConfig> C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) {
@ -128,6 +128,7 @@ public class NotificationProcessingContext {
private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) { private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) {
return Map.of( return Map.of(
"recipientTitle", recipient.getTitle(),
"recipientEmail", Strings.nullToEmpty(recipient.getEmail()), "recipientEmail", Strings.nullToEmpty(recipient.getEmail()),
"recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()), "recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()),
"recipientLastName", Strings.nullToEmpty(recipient.getLastName()) "recipientLastName", Strings.nullToEmpty(recipient.getLastName())

9
application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java

@ -48,12 +48,9 @@ public class EmailNotificationChannel implements NotificationChannel<User, Email
} }
@Override @Override
public boolean check(TenantId tenantId) { public void check(TenantId tenantId) throws Exception {
try { if (!mailService.isConfigured(tenantId)) {
mailService.testConnection(tenantId); throw new RuntimeException("Mail server is not configured");
return true;
} catch (Exception e) {
return false;
} }
} }

2
application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java

@ -26,7 +26,7 @@ public interface NotificationChannel<R extends NotificationRecipient, T extends
ListenableFuture<Void> sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx); ListenableFuture<Void> sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx);
boolean check(TenantId tenantId); void check(TenantId tenantId) throws Exception;
NotificationDeliveryMethod getDeliveryMethod(); NotificationDeliveryMethod getDeliveryMethod();

10
application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java

@ -22,12 +22,12 @@ import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@ -47,9 +47,11 @@ public class SlackNotificationChannel implements NotificationChannel<SlackConver
} }
@Override @Override
public boolean check(TenantId tenantId) { public void check(TenantId tenantId) throws Exception {
NotificationSettings notificationSettings = notificationSettingsService.findNotificationSettings(tenantId); NotificationSettings notificationSettings = notificationSettingsService.findNotificationSettings(tenantId);
return notificationSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.SLACK); if (!notificationSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.SLACK)) {
throw new RuntimeException("Slack API token is not configured");
}
} }
@Override @Override

6
application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java

@ -49,8 +49,10 @@ public class SmsNotificationChannel implements NotificationChannel<User, SmsDeli
} }
@Override @Override
public boolean check(TenantId tenantId) { public void check(TenantId tenantId) throws Exception {
return smsService.isConfigured(tenantId); if (!smsService.isConfigured(tenantId)) {
throw new RuntimeException("SMS provider is not configured");
}
} }
@Override @Override

12
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java

@ -35,15 +35,17 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.service.notification.rule.cache.NotificationRulesCache;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.service.apiusage.limits.LimitedApi;
import org.thingsboard.server.service.apiusage.limits.RateLimitService;
import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.notification.rule.cache.NotificationRulesCache;
import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor; import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor;
import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor; import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor;
@ -65,6 +67,7 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
private final NotificationRulesCache notificationRulesCache; private final NotificationRulesCache notificationRulesCache;
private final NotificationRequestService notificationRequestService; private final NotificationRequestService notificationRequestService;
private final PartitionService partitionService; private final PartitionService partitionService;
private final RateLimitService rateLimitService;
@Autowired @Lazy @Autowired @Lazy
private NotificationCenter notificationCenter; private NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor; private final NotificationExecutorService notificationExecutor;
@ -119,6 +122,11 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
} }
if (matchesFilter(trigger, triggerConfig)) { if (matchesFilter(trigger, triggerConfig)) {
if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, rule.getTenantId(), rule.getId())) {
log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName());
return;
}
NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig); NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig);
rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> { rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> {
submitNotificationRequest(targets, rule, trigger.getOriginatorEntityId(), notificationInfo, delay); submitNotificationRequest(targets, rule, trigger.getOriginatorEntityId(), notificationInfo, delay);

11
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java

@ -32,19 +32,20 @@ import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigg
import java.util.Set; import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> { public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> {
@Override @Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) {
Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? Action.ASSIGNED : Action.UNASSIGNED; Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? Action.ASSIGNED : Action.UNASSIGNED;
if (!triggerConfig.getNotifyOn().contains(action)) { if (!triggerConfig.getNotifyOn().contains(action)) {
return false; return false;
} }
Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class); Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) && emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
} }
@ -53,7 +54,7 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio
AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class); AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class);
AlarmAssignee assignee = alarmInfo.getAssignee(); AlarmAssignee assignee = alarmInfo.getAssignee();
return AlarmAssignmentNotificationInfo.builder() return AlarmAssignmentNotificationInfo.builder()
.action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? "assigned" : "unassigned") .action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? "assigned" : "unassigned")
.assigneeFirstName(assignee != null ? assignee.getFirstName() : null) .assigneeFirstName(assignee != null ? assignee.getFirstName() : null)
.assigneeLastName(assignee != null ? assignee.getLastName() : null) .assigneeLastName(assignee != null ? assignee.getLastName() : null)
.assigneeEmail(assignee != null ? assignee.getEmail() : null) .assigneeEmail(assignee != null ? assignee.getEmail() : null)
@ -78,7 +79,7 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio
@Override @Override
public Set<String> getSupportedMsgTypes() { public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ALARM_ASSIGN, DataConstants.ALARM_UNASSIGN); return Set.of(DataConstants.ALARM_ASSIGNED, DataConstants.ALARM_UNASSIGNED);
} }
} }

11
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigg
import java.util.Set; import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> { public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> {
@ -53,8 +54,8 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
} }
} }
Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class); Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) && emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
} }
@ -66,9 +67,9 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
return AlarmCommentNotificationInfo.builder() return AlarmCommentNotificationInfo.builder()
.comment(comment.getComment().get("text").asText()) .comment(comment.getComment().get("text").asText())
.action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated") .action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated")
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail")) .userEmail(msg.getMetaData().getValue("userEmail"))
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName")) .userFirstName(msg.getMetaData().getValue("userFirstName"))
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName")) .userLastName(msg.getMetaData().getValue("userLastName"))
.alarmId(alarmInfo.getUuidId()) .alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType()) .alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator()) .alarmOriginator(alarmInfo.getOriginator())

8
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
@ -25,11 +26,10 @@ import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotific
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger; import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> { public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> {
@ -85,11 +85,11 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
} }
private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
return isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity()); return emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity());
} }
private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
return isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType()); return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType());
} }
@Override @Override

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.Notification
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -34,8 +34,8 @@ public class ApiUsageLimitTriggerProcessor implements NotificationRuleTriggerPro
@Override @Override
public boolean matchesFilter(ApiUsageLimitTrigger trigger, ApiUsageLimitNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(ApiUsageLimitTrigger trigger, ApiUsageLimitNotificationRuleTriggerConfig triggerConfig) {
return (isEmpty(triggerConfig.getApiFeatures()) || triggerConfig.getApiFeatures().contains(trigger.getState().getApiFeature())) && return emptyOrContains(triggerConfig.getApiFeatures(), trigger.getState().getApiFeature()) &&
(isEmpty(triggerConfig.getNotifyOn()) || triggerConfig.getNotifyOn().contains(trigger.getStatus())); emptyOrContains(triggerConfig.getNotifyOn(), trigger.getStatus());
} }
@Override @Override

15
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java

@ -21,7 +21,10 @@ import org.thingsboard.server.common.data.notification.info.EntitiesLimitNotific
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
@ -30,6 +33,8 @@ import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
@RequiredArgsConstructor @RequiredArgsConstructor
public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor<EntitiesLimitTrigger, EntitiesLimitNotificationRuleTriggerConfig> { public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor<EntitiesLimitTrigger, EntitiesLimitNotificationRuleTriggerConfig> {
private final EntityCountService entityCountService;
private final TbTenantProfileCache tenantProfileCache;
private final TenantService tenantService; private final TenantService tenantService;
@Override @Override
@ -37,7 +42,15 @@ public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerPro
if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) { if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) {
return false; return false;
} }
return (int) (trigger.getLimit() * triggerConfig.getThreshold()) == trigger.getCurrentCount(); // strict comparing not to send notification on each new entity DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(trigger.getTenantId()).getDefaultProfileConfiguration();
long limit = profileConfiguration.getEntitiesLimit(trigger.getEntityType());
if (limit <= 0) {
return false;
}
long currentCount = entityCountService.countByTenantIdAndEntityType(trigger.getTenantId(), trigger.getEntityType());
trigger.setLimit(limit);
trigger.setCurrentCount(currentCount);
return (int) (limit * triggerConfig.getThreshold()) == currentCount; // strict comparing not to send notification on each new entity
} }
@Override @Override

73
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java

@ -16,78 +16,47 @@
package org.thingsboard.server.service.notification.rule.trigger; package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import java.util.Optional; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
import java.util.Set;
import java.util.UUID;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
@Service @Service
public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<EntityActionNotificationRuleTriggerConfig> { public class EntityActionTriggerProcessor implements NotificationRuleTriggerProcessor<EntityActionTrigger, EntityActionNotificationRuleTriggerConfig> {
@Override @Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(EntityActionTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) {
String msgType = trigger.getMsg().getType(); return ((trigger.getActionType() == ActionType.ADDED && triggerConfig.isCreated())
if (msgType.equals(DataConstants.ENTITY_CREATED)) { || (trigger.getActionType() == ActionType.UPDATED && triggerConfig.isUpdated())
if (!triggerConfig.isCreated()) { || (trigger.getActionType() == ActionType.DELETED && triggerConfig.isDeleted()))
return false; && emptyOrContains(triggerConfig.getEntityTypes(), trigger.getEntityId().getEntityType());
}
} else if (msgType.equals(DataConstants.ENTITY_UPDATED)) {
if (!triggerConfig.isUpdated()) {
return false;
}
} else if (msgType.equals(DataConstants.ENTITY_DELETED)) {
if (!triggerConfig.isDeleted()) {
return false;
}
} else {
return false;
}
return isEmpty(triggerConfig.getEntityTypes()) || triggerConfig.getEntityTypes().contains(getEntityType(trigger.getMsg()));
} }
@Override @Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { public RuleOriginatedNotificationInfo constructNotificationInfo(EntityActionTrigger trigger) {
TbMsg msg = trigger.getMsg();
String msgType = msg.getType();
ActionType actionType = msgType.equals(DataConstants.ENTITY_CREATED) ? ActionType.ADDED :
msgType.equals(DataConstants.ENTITY_UPDATED) ? ActionType.UPDATED :
msgType.equals(DataConstants.ENTITY_DELETED) ? ActionType.DELETED : null;
return EntityActionNotificationInfo.builder() return EntityActionNotificationInfo.builder()
.entityId(msg.getOriginator()) .entityId(trigger.getEntityId())
.entityName(msg.getMetaData().getValue("entityName")) .entityName(trigger.getEntity().getName())
.actionType(actionType) .actionType(trigger.getActionType())
.userId(UUID.fromString(msg.getMetaData().getValue("userId"))) .userId(trigger.getUser().getUuidId())
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail")) .userTitle(trigger.getUser().getTitle())
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName")) .userEmail(trigger.getUser().getEmail())
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName")) .userFirstName(trigger.getUser().getFirstName())
.entityCustomerId(msg.getCustomerId()) .userLastName(trigger.getUser().getLastName())
.entityCustomerId(trigger.getEntity() instanceof HasCustomerId ?
((HasCustomerId) trigger.getEntity()).getCustomerId() :
trigger.getUser().getCustomerId())
.build(); .build();
} }
private static EntityType getEntityType(TbMsg msg) {
return Optional.ofNullable(msg.getMetaData().getValue("entityType"))
.map(EntityType::valueOf).orElse(null);
}
@Override @Override
public NotificationRuleTriggerType getTriggerType() { public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ENTITY_ACTION; return NotificationRuleTriggerType.ENTITY_ACTION;
} }
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ENTITY_CREATED, DataConstants.ENTITY_UPDATED, DataConstants.ENTITY_DELETED);
}
} }

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

@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;

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

@ -18,13 +18,13 @@ package org.thingsboard.server.service.queue;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.TenantRoutingInfo; import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
@Slf4j @Slf4j
@Service @Service
@ -46,7 +46,7 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService
if (tenantProfile != null) { if (tenantProfile != null) {
return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbRuleEngine()); return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbRuleEngine());
} else { } else {
throw new RuntimeException("Tenant not found!"); throw new TenantNotFoundException(tenantId);
} }
} }
} }

6
application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java

@ -67,6 +67,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
@ -263,8 +264,7 @@ public class AccessValidator {
if (currentUser.isSystemAdmin()) { if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
} else { } else {
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(currentUser.getTenantId(), new DeviceId(entityId.getId())); Futures.addCallback(Futures.immediateFuture(deviceService.findDeviceById(currentUser.getTenantId(), new DeviceId(entityId.getId()))), getCallback(callback, device -> {
Futures.addCallback(deviceFuture, getCallback(callback, device -> {
if (device == null) { if (device == null) {
return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND); return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND);
} else { } else {
@ -575,7 +575,7 @@ public class AccessValidator {
ResponseEntity responseEntity; ResponseEntity responseEntity;
if (e instanceof ToErrorResponseEntity) { if (e instanceof ToErrorResponseEntity) {
responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
} else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException) { } else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException || e instanceof DataValidationException) {
responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} else { } else {
responseEntity = new ResponseEntity<>(defaultErrorStatus); responseEntity = new ResponseEntity<>(defaultErrorStatus);

23
application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java

@ -28,7 +28,6 @@ import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.methods.response.users.UsersListResponse; import com.slack.api.methods.response.users.UsersListResponse;
import com.slack.api.model.ConversationType; import com.slack.api.model.ConversationType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -44,8 +43,6 @@ import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class DefaultSlackService implements SlackService { public class DefaultSlackService implements SlackService {
@ -81,15 +78,11 @@ public class DefaultSlackService implements SlackService {
.filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot()) .filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot())
.map(user -> { .map(user -> {
SlackConversation conversation = new SlackConversation(); SlackConversation conversation = new SlackConversation();
conversation.setType(conversationType);
conversation.setId(user.getId()); conversation.setId(user.getId());
conversation.setShortName(user.getName()); conversation.setName(user.getName());
conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName()); conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName());
conversation.setEmail(user.getProfile() != null ? user.getProfile().getEmail() : null); conversation.setEmail(user.getProfile() != null ? user.getProfile().getEmail() : null);
String title = "@" + conversation.getShortName();
if (isNotEmpty(conversation.getWholeName()) && !conversation.getWholeName().equals(conversation.getShortName())) {
title += " (" + conversation.getWholeName() + ")";
}
conversation.setTitle(title);
return conversation; return conversation;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -107,10 +100,10 @@ public class DefaultSlackService implements SlackService {
.filter(channel -> !channel.isArchived()) .filter(channel -> !channel.isArchived())
.map(channel -> { .map(channel -> {
SlackConversation conversation = new SlackConversation(); SlackConversation conversation = new SlackConversation();
conversation.setType(conversationType);
conversation.setId(channel.getId()); conversation.setId(channel.getId());
conversation.setShortName(channel.getName()); conversation.setName(channel.getName());
conversation.setWholeName(channel.getNameNormalized()); conversation.setWholeName(channel.getNameNormalized());
conversation.setTitle("#" + channel.getName());
return conversation; return conversation;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -118,14 +111,6 @@ public class DefaultSlackService implements SlackService {
}); });
} }
@Override
public SlackConversation findConversation(TenantId tenantId, String token, SlackConversationType conversationType, String namePattern) {
List<SlackConversation> conversations = listConversations(tenantId, token, conversationType);
return conversations.stream()
.filter(conversation -> StringUtils.containsIgnoreCase(conversation.getTitle(), namePattern))
.findFirst().orElse(null);
}
@Override @Override
public String getToken(TenantId tenantId) { public String getToken(TenantId tenantId) {
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);

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

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -453,6 +454,9 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
updateInactivityStateIfExpired(ts, deviceId, stateData); updateInactivityStateIfExpired(ts, deviceId, stateData);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to update inactivity state", deviceId, e); log.warn("[{}] Failed to update inactivity state", deviceId, e);
if (e instanceof TenantNotFoundException) {
continue;
}
} }
Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(), Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(),
tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger())); tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger()));

4
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -73,7 +73,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
@Override @Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException { public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
if (!rateLimitService.checkRateLimit(ctx.getTenantId(), LimitedApi.ENTITY_EXPORT)) { if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) {
throw new ThingsboardException("Rate limit for entities export is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); throw new ThingsboardException("Rate limit for entities export is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
} }
@ -85,7 +85,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
@Override @Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(EntitiesImportCtx ctx, EntityExportData<E> exportData) throws ThingsboardException { public <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(EntitiesImportCtx ctx, EntityExportData<E> exportData) throws ThingsboardException {
if (!rateLimitService.checkRateLimit(ctx.getTenantId(), LimitedApi.ENTITY_IMPORT)) { if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_IMPORT, ctx.getTenantId())) {
throw new ThingsboardException("Rate limit for entities import is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); throw new ThingsboardException("Rate limit for entities import is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
} }
if (exportData.getEntity() == null || exportData.getEntity().getId() == null) { if (exportData.getEntity() == null || exportData.getEntity().getId() == null) {

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

@ -113,8 +113,9 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
ImportedEntityInfo<E> importedEntityInfo = saveEntity(entityData.getFields(), user); ImportedEntityInfo<E> importedEntityInfo = saveEntity(entityData.getFields(), user);
E entity = importedEntityInfo.getEntity(); E entity = importedEntityInfo.getEntity();
saveKvs(user, entity, entityData.getKvs()); if (request.getMapping().getUpdate() || !importedEntityInfo.isUpdated()) {
saveKvs(user, entity, entityData.getKvs());
}
return importedEntityInfo; return importedEntityInfo;
}, },
importedEntityInfo -> { importedEntityInfo -> {

46
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -87,7 +86,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
@Autowired @Autowired
protected TbNotificationEntityService entityNotificationService; protected TbNotificationEntityService entityNotificationService;
@Transactional(rollbackFor = Exception.class)
@Override @Override
public EntityImportResult<E> importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException { public EntityImportResult<E> importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException {
EntityImportResult<E> importResult = new EntityImportResult<>(); EntityImportResult<E> importResult = new EntityImportResult<>();
@ -336,27 +334,37 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty(); if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty();
for (EntityType entityType : EntityType.values()) { for (EntityType entityType : EntityType.values()) {
Optional<EntityId> externalIdOpt = buildEntityId(entityType, externalUuid); Optional<EntityId> externalId = buildEntityId(entityType, externalUuid);
if (!externalIdOpt.isPresent()) { if (externalId.isEmpty()) {
continue; continue;
} }
EntityId internalId = ctx.getInternalId(externalIdOpt.get()); EntityId internalId = ctx.getInternalId(externalId.get());
if (internalId != null) { if (internalId != null) {
return Optional.of(internalId); return Optional.of(internalId);
} }
} }
if (fetchAllUUIDs) { if (fetchAllUUIDs) {
for (EntityType entityType : hints) { Set<EntityType> processLast = Set.of(EntityType.TENANT);
Optional<EntityId> internalId = lookupInDb(externalUuid, entityType); List<EntityType> entityTypes = new ArrayList<>(hints);
if (internalId.isPresent()) return internalId;
}
for (EntityType entityType : EntityType.values()) { for (EntityType entityType : EntityType.values()) {
if (hints.contains(entityType)) { if (!hints.contains(entityType) && !processLast.contains(entityType)) {
entityTypes.add(entityType);
}
}
entityTypes.addAll(processLast);
for (EntityType entityType : entityTypes) {
Optional<EntityId> externalId = buildEntityId(entityType, externalUuid);
if (externalId.isEmpty() || ctx.isNotFound(externalId.get())) {
continue; continue;
} }
Optional<EntityId> internalId = lookupInDb(externalUuid, entityType); EntityId internalId = getInternalId(externalId.get(), false);
if (internalId.isPresent()) return internalId; if (internalId != null) {
return Optional.of(internalId);
} else {
ctx.registerNotFound(externalId.get());
}
} }
} }
@ -364,20 +372,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
return Optional.empty(); return Optional.empty();
} }
private Optional<EntityId> lookupInDb(UUID externalUuid, EntityType entityType) {
Optional<EntityId> externalIdOpt = buildEntityId(entityType, externalUuid);
if (externalIdOpt.isEmpty() || ctx.isNotFound(externalIdOpt.get())) {
return Optional.empty();
}
EntityId internalId = getInternalId(externalIdOpt.get(), false);
if (internalId != null) {
return Optional.of(internalId);
} else {
ctx.registerNotFound(externalIdOpt.get());
}
return Optional.empty();
}
private Optional<EntityId> buildEntityId(EntityType entityType, UUID externalUuid) { private Optional<EntityId> buildEntityId(EntityType entityType, UUID externalUuid) {
try { try {
return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid)); return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid));

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java

@ -65,10 +65,10 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
@Override @Override
protected Dashboard prepare(EntitiesImportCtx ctx, Dashboard dashboard, Dashboard old, EntityExportData<Dashboard> exportData, IdProvider idProvider) { protected Dashboard prepare(EntitiesImportCtx ctx, Dashboard dashboard, Dashboard old, EntityExportData<Dashboard> exportData, IdProvider idProvider) {
for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) { for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) {
replaceIdsRecursively(ctx, idProvider, entityAlias, Collections.emptySet(), HINTS); replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), HINTS);
} }
for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"), HINTS); replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), HINTS);
} }
return dashboard; return dashboard;
} }

6
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
@ -69,6 +68,7 @@ import org.thingsboard.server.common.data.sync.vc.request.load.EntityTypeVersion
import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest; import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest;
import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig;
import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
@ -259,7 +259,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
return gitServiceQueue.listEntitiesAtVersion(tenantId, versionId); return gitServiceQueue.listEntitiesAtVersion(tenantId, versionId);
} }
@SuppressWarnings({"UnstableApiUsage", "rawtypes"}) @SuppressWarnings({"rawtypes"})
@Override @Override
public UUID loadEntitiesVersion(User user, VersionLoadRequest request) throws Exception { public UUID loadEntitiesVersion(User user, VersionLoadRequest request) throws Exception {
EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId()); EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId());
@ -402,7 +402,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>()) ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>())
.add(importResult.getSavedEntity().getId()); .add(importResult.getSavedEntity().getId());
} }
log.debug("Imported {} pack for tenant {}", entityType, ctx.getTenantId()); log.debug("Imported {} pack ({}) for tenant {}", entityType, entityDataList.size(), ctx.getTenantId());
offset += limit; offset += limit;
} while (entityDataList.size() == limit); } while (entityDataList.size() == limit);
} }

44
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.system; package org.thingsboard.server.service.system;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ProtocolStringList; import com.google.protobuf.ProtocolStringList;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -23,10 +22,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.FeaturesInfo; import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo; import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.SystemInfoData; import org.thingsboard.server.common.data.SystemInfoData;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo;
import org.thingsboard.server.queue.discovery.DiscoveryService; import org.thingsboard.server.queue.discovery.DiscoveryService;
@ -59,10 +58,10 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.SystemUtil.getCpuCount;
import static org.thingsboard.common.util.SystemUtil.getCpuUsage; import static org.thingsboard.common.util.SystemUtil.getCpuUsage;
import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
import static org.thingsboard.common.util.SystemUtil.getDiscSpaceUsage; import static org.thingsboard.common.util.SystemUtil.getDiscSpaceUsage;
import static org.thingsboard.common.util.SystemUtil.getCpuCount; import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace; import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace;
import static org.thingsboard.common.util.SystemUtil.getTotalMemory; import static org.thingsboard.common.util.SystemUtil.getTotalMemory;
@ -90,6 +89,8 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private final TbApiUsageStateClient apiUsageStateClient; private final TbApiUsageStateClient apiUsageStateClient;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final OAuth2Service oAuth2Service; private final OAuth2Service oAuth2Service;
private final MailService mailService;
private final SmsService smsService;
private volatile ScheduledExecutorService scheduler; private volatile ScheduledExecutorService scheduler;
@Override @Override
@ -135,18 +136,37 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
public FeaturesInfo getFeaturesInfo() { public FeaturesInfo getFeaturesInfo() {
FeaturesInfo featuresInfo = new FeaturesInfo(); FeaturesInfo featuresInfo = new FeaturesInfo();
featuresInfo.setEmailEnabled(isEmailEnabled()); featuresInfo.setEmailEnabled(isEmailEnabled());
featuresInfo.setSmsEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "sms") != null); featuresInfo.setSmsEnabled(smsService.isConfigured(TenantId.SYS_TENANT_ID));
featuresInfo.setOauthEnabled(oAuth2Service.findOAuth2Info().isEnabled()); featuresInfo.setOauthEnabled(oAuth2Service.findOAuth2Info().isEnabled());
featuresInfo.setTwoFaEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings") != null); featuresInfo.setTwoFaEnabled(isTwoFaEnabled());
featuresInfo.setNotificationEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications") != null); featuresInfo.setNotificationEnabled(isSlackEnabled());
return featuresInfo; return featuresInfo;
} }
private boolean isEmailEnabled() { private boolean isEmailEnabled() {
AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); try {
if (mailSettings != null) { mailService.testConnection(TenantId.SYS_TENANT_ID);
JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom"); return true;
return StringUtils.isNotEmpty(mailFrom.asText()) && !"ThingsBoard <sysadmin@localhost.localdomain>".equalsIgnoreCase(mailFrom.asText()); } catch (Exception e) {
return false;
}
}
private boolean isTwoFaEnabled() {
AdminSettings twoFaSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings");
if (twoFaSettings != null) {
var providers = twoFaSettings.getJsonValue().get("providers");
if (providers != null) {
return providers.size() > 0;
}
}
return false;
}
private boolean isSlackEnabled() {
AdminSettings notifications = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications");
if (notifications != null) {
return notifications.getJsonValue().get("deliveryMethodsConfigs").has("SLACK");
} }
return false; return false;
} }

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

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -51,7 +52,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
@ -189,6 +190,16 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return alarmService.findCustomerAlarms(tenantId, customerId, query); return alarmService.findCustomerAlarms(tenantId, customerId, query);
} }
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return alarmService.findAlarmsV2(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return alarmService.findCustomerAlarmsV2(tenantId, customerId, query);
}
@Override @Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) {
return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId); return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId);

46
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
@ -66,11 +67,13 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
@ -108,6 +111,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH; import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH;
@ -124,10 +128,13 @@ public class DefaultTransportApiService implements TransportApiService {
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
private static final Pattern X509_CERTIFICATE_TRIM_CHAIN_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----");
private final TbDeviceProfileCache deviceProfileCache; private final TbDeviceProfileCache deviceProfileCache;
private final TbTenantProfileCache tenantProfileCache; private final TbTenantProfileCache tenantProfileCache;
private final TbApiUsageStateService apiUsageStateService; private final TbApiUsageStateService apiUsageStateService;
private final DeviceService deviceService; private final DeviceService deviceService;
private final DeviceProfileService deviceProfileService;
private final RelationService relationService; private final RelationService relationService;
private final DeviceCredentialsService deviceCredentialsService; private final DeviceCredentialsService deviceCredentialsService;
private final DbCallbackExecutorService dbCallbackExecutorService; private final DbCallbackExecutorService dbCallbackExecutorService;
@ -159,6 +166,9 @@ public class DefaultTransportApiService implements TransportApiService {
} else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE); result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE);
} else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) {
TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg();
result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain());
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg());
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
@ -226,6 +236,35 @@ public class DefaultTransportApiService implements TransportApiService {
} }
} }
protected ListenableFuture<TransportApiResponseMsg> validateOrCreateDeviceX509Certificate(String certificateChain) {
List<String> chain = X509_CERTIFICATE_TRIM_CHAIN_PATTERN.matcher(certificateChain).results().map(match ->
EncryptionUtil.certTrimNewLines(match.group())).collect(Collectors.toList());
for (String certificateValue : chain) {
String certificateHash = EncryptionUtil.getSha3Hash(certificateValue);
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash);
if (credentials != null && DeviceCredentialsType.X509_CERTIFICATE.equals(credentials.getCredentialsType())) {
return getDeviceInfo(credentials);
}
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(certificateHash);
if (deviceProfile != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) {
String updatedDeviceProvisionSecret = chain.get(0);
ProvisionRequest provisionRequest = createProvisionRequest(updatedDeviceProvisionSecret);
try {
ProvisionResponse provisionResponse = deviceProvisionService.provisionDeviceViaX509Chain(deviceProfile, provisionRequest);
if (ProvisionResponseStatus.SUCCESS.equals(provisionResponse.getResponseStatus())) {
return getDeviceInfo(provisionResponse.getDeviceCredentials());
}
} catch (ProvisionFailedException e) {
log.debug("[{}][{}] Failed to provision device with cert chain: {}", deviceProfile.getTenantId(), deviceProfile.getId(), provisionRequest, e);
return getEmptyTransportApiResponseFuture();
}
} else if (deviceProfile != null) {
log.warn("[{}][{}] Device Profile provision configuration mismatched: expected {}, actual {}", deviceProfile.getTenantId(), deviceProfile.getId(), DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN, deviceProfile.getProvisionType());
}
}
return getEmptyTransportApiResponseFuture();
}
private ListenableFuture<TransportApiResponseMsg> validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { private ListenableFuture<TransportApiResponseMsg> validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
if (credentials != null) { if (credentials != null) {
@ -665,4 +704,11 @@ public class DefaultTransportApiService implements TransportApiService {
private Long checkLong(Long l) { private Long checkLong(Long l) {
return l != null ? l : 0; return l != null ? l : 0;
} }
private ProvisionRequest createProvisionRequest(String certificateValue) {
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE,
new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue),
null);
}
} }

3
application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java

@ -29,11 +29,10 @@ import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger; import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;

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

@ -131,6 +131,9 @@ security:
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}" loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
githubMapper: githubMapper:
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}" emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
java_cacerts:
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}${file.separator}lib${file.separator}security${file.separator}cacerts}"
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
# Usage statistics parameters # Usage statistics parameters
usage: usage:
@ -479,10 +482,14 @@ cache:
entityCount: entityCount:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}" timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}"
maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}" maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}"
# deliberately placed outside 'specs' group above # deliberately placed outside 'specs' group above
notificationRules: notificationRules:
timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_RULES_TTL:30}" timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_RULES_TTL:30}"
maxSize: "${CACHE_SPECS_NOTIFICATION_RULES_MAX_SIZE:1000}" maxSize: "${CACHE_SPECS_NOTIFICATION_RULES_MAX_SIZE:1000}"
rateLimits:
timeToLiveInMinutes: "${CACHE_SPECS_RATE_LIMITS_TTL:60}"
maxSize: "${CACHE_SPECS_RATE_LIMITS_MAX_SIZE:100000}"
#Disable this because it is not required. #Disable this because it is not required.
spring.data.redis.repositories.enabled: false spring.data.redis.repositories.enabled: false
@ -949,7 +956,9 @@ edges:
enabled: "${EDGES_ENABLED:true}" enabled: "${EDGES_ENABLED:true}"
rpc: rpc:
port: "${EDGES_RPC_PORT:7070}" port: "${EDGES_RPC_PORT:7070}"
client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:300}" client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:1}"
keep_alive_time_sec: "${EDGES_RPC_KEEP_ALIVE_TIME_SEC:10}"
keep_alive_timeout_sec: "${EDGES_RPC_KEEP_ALIVE_TIMEOUT_SEC:5}"
ssl: ssl:
# Enable/disable SSL support # Enable/disable SSL support
enabled: "${EDGES_RPC_SSL_ENABLED:false}" enabled: "${EDGES_RPC_SSL_ENABLED:false}"
@ -1009,6 +1018,8 @@ queue:
max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}"
max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}"
fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}"
request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms
session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms
use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}"
confluent: confluent:
ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}"
@ -1027,11 +1038,12 @@ queue:
# tb_rule_engine.sq: # tb_rule_engine.sq:
# - key: max.poll.records # - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}"
other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000"
- key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside
value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms
- key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds)
value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms
# value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds)
topic-properties: topic-properties:
rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"

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

@ -29,7 +29,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -180,7 +179,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
alarm = updatedAlarm; alarm = updatedAlarm;
alarm.setAssigneeId(null); alarm.setAssigneeId(null);
@ -192,7 +191,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED);
} }
@Test @Test
@ -414,7 +413,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
} }
@Test @Test
@ -445,7 +444,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
logout(); logout();
@ -462,7 +461,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGNED);
} }
@Test @Test
@ -480,7 +479,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
beforeAssignmentTs = System.currentTimeMillis(); beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2); Thread.sleep(2);
@ -491,7 +490,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED);
} }
@Test @Test
@ -509,7 +508,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
logout(); logout();
loginCustomerUser(); loginCustomerUser();
@ -525,7 +524,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGN); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED);
} }
@Test @Test

72
application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
@ -33,11 +34,13 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest;
@ -68,6 +71,8 @@ import org.thingsboard.server.service.gateway_device.GatewayNotificationsService
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -1392,6 +1397,73 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedCredentials, updatedCredentials); Assert.assertEquals(savedCredentials, updatedCredentials);
} }
@Test
public void testBulkImportDeviceWithUpdateFalse() throws Exception {
String deviceName = "firstDevice";
String attributeValue = "testValue";
BulkImportRequest request = new BulkImportRequest();
request.setFile(String.format("NAME,TYPE,DATA\n%s,%s,%s", deviceName, "thermostat", attributeValue));
BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping();
BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping();
name.setType(BulkImportColumnType.NAME);
BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping();
type.setType(BulkImportColumnType.TYPE);
BulkImportRequest.ColumnMapping attribute = new BulkImportRequest.ColumnMapping();
attribute.setType(BulkImportColumnType.SERVER_ATTRIBUTE);
attribute.setKey("DATA");
List<BulkImportRequest.ColumnMapping> columns = new ArrayList<>();
columns.add(name);
columns.add(type);
columns.add(attribute);
mapping.setColumns(columns);
mapping.setDelimiter(',');
mapping.setUpdate(true);
mapping.setHeader(true);
request.setMapping(mapping);
//import device
doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {});
Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
//check server attribute value
List<Map<String, Object>> values = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId() +
"/values/attributes/SERVER_SCOPE", new TypeReference<>() {
});
Map<String, Object> serverAttribute = values.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get();
Assert.assertEquals(attributeValue, serverAttribute.get("value"));
//update server attribute value
String newAttributeValue = "testValue2";
JsonNode content = JacksonUtil.toJsonNode("{\"DATA\": \"" + newAttributeValue + "\"}");
doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + savedDevice.getUuidId() + "/SERVER_SCOPE", content)
.andExpect(status().isOk());
//reimport devices
String deviceName2 = "secondDevice";
String attributeValue2 = "testValue3";
request.setFile(String.format("NAME,TYPE,DATA\n%s,%s,%s\n%s,%s,%s", deviceName, "thermostat", attributeValue,
deviceName2, "thermostat", attributeValue2));
mapping.setUpdate(false);
doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {});
Device savedDevice2 = doGet("/api/tenant/devices?deviceName=" + deviceName2, Device.class);
//check attribute value was not changed after reimport
List<Map<String, Object>> values2 = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId() +
"/values/attributes/SERVER_SCOPE", new TypeReference<>() {
});
Map<String, Object> retrievedServerAttribute2 = values2.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get();
Assert.assertEquals(newAttributeValue, retrievedServerAttribute2.get("value"));
//check attribute for second device
List<Map<String, Object>> values3 = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice2.getId() +
"/values/attributes/SERVER_SCOPE", new TypeReference<>() {
});
Map<String, Object> retrievedServerAttribute3 = values3.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get();
Assert.assertEquals(attributeValue2, retrievedServerAttribute3.get("value"));
}
private Device createDevice(String name) { private Device createDevice(String name) {
Device device = new Device(); Device device = new Device();
device.setName(name); device.setName(name);

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

@ -299,6 +299,28 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
} }
@Test
public void testSaveDeviceProfileWithSameCertificateHash() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
deviceProfile.setProvisionDeviceKey("Certificate hash");
doPost("/api/deviceProfile", deviceProfile)
.andExpect(status().isOk());
DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2");
deviceProfile2.setProvisionDeviceKey("Certificate hash");
Mockito.reset(tbClusterService, auditLogService);
String msgError = "Device profile with such provision device key already exists!";
doPost("/api/deviceProfile", deviceProfile2)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));
testNotifyEntityEqualsOneTimeServiceNeverError(deviceProfile, savedTenant.getId(),
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
}
@Test @Test
public void testChangeDeviceProfileTypeNull() throws Exception { public void testChangeDeviceProfileTypeNull() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");

48
application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java

@ -21,8 +21,12 @@ import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
@ -53,6 +57,7 @@ import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
@ -76,6 +81,15 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
@Autowired @Autowired
private TbTenantProfileCache tenantProfileCache; private TbTenantProfileCache tenantProfileCache;
@Autowired
private AdminSettingsService adminSettingsService;
@MockBean
private MailService mailService;
@MockBean
private SmsService smsService;
//For system administrator //For system administrator
@Test @Test
public void testTenantsCountWsCmd() throws Exception { public void testTenantsCountWsCmd() throws Exception {
@ -266,6 +280,19 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
@Test @Test
public void testGetFeaturesInfo() throws Exception { public void testGetFeaturesInfo() throws Exception {
String mail = "test@thingsboard.org";
Mockito.doAnswer(invocation -> {
AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom");
if (!mailFrom.asText().equals(mail)) {
throw new Exception();
}
return null;
}).when(mailService).testConnection(TenantId.SYS_TENANT_ID);
Mockito.when(smsService.isConfigured(TenantId.SYS_TENANT_ID))
.then(a -> adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "sms") != null);
loginSysAdmin(); loginSysAdmin();
FeaturesInfo featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); FeaturesInfo featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
@ -279,7 +306,7 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
AdminSettings mailSettings = doGet("/api/admin/settings/mail", AdminSettings.class); AdminSettings mailSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
JsonNode jsonValue = mailSettings.getJsonValue(); JsonNode jsonValue = mailSettings.getJsonValue();
((ObjectNode) jsonValue).put("mailFrom", "test@thingsboard.org"); ((ObjectNode) jsonValue).put("mailFrom", mail);
mailSettings.setJsonValue(jsonValue); mailSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", mailSettings).andExpect(status().isOk()); doPost("/api/admin/settings", mailSettings).andExpect(status().isOk());
@ -305,7 +332,12 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
AdminSettings twoFaSettingsSettings = new AdminSettings(); AdminSettings twoFaSettingsSettings = new AdminSettings();
twoFaSettingsSettings.setKey("twoFaSettings"); twoFaSettingsSettings.setKey("twoFaSettings");
twoFaSettingsSettings.setJsonValue(JacksonUtil.newObjectNode());
var twoFaSettings = JacksonUtil.newObjectNode();
var providers = JacksonUtil.newArrayNode();
providers.add(JacksonUtil.newObjectNode());
twoFaSettings.set("providers", providers);
twoFaSettingsSettings.setJsonValue(twoFaSettings);
doPost("/api/admin/settings", twoFaSettingsSettings).andExpect(status().isOk()); doPost("/api/admin/settings", twoFaSettingsSettings).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
@ -317,7 +349,13 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
AdminSettings notificationsSettings = new AdminSettings(); AdminSettings notificationsSettings = new AdminSettings();
notificationsSettings.setKey("notifications"); notificationsSettings.setKey("notifications");
notificationsSettings.setJsonValue(JacksonUtil.newObjectNode());
var notificationSettings = JacksonUtil.newObjectNode();
var deliveryMethodsConfigs = JacksonUtil.newObjectNode();
deliveryMethodsConfigs.set("SLACK", JacksonUtil.newObjectNode());
notificationSettings.set("deliveryMethodsConfigs", deliveryMethodsConfigs);
notificationsSettings.setJsonValue(notificationSettings);
doPost("/api/admin/settings", notificationsSettings).andExpect(status().isOk()); doPost("/api/admin/settings", notificationsSettings).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
@ -338,6 +376,10 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
Assert.assertTrue(featuresInfo.isTwoFaEnabled()); Assert.assertTrue(featuresInfo.isTwoFaEnabled());
Assert.assertTrue(featuresInfo.isNotificationEnabled()); Assert.assertTrue(featuresInfo.isNotificationEnabled());
Assert.assertTrue(featuresInfo.isOauthEnabled()); Assert.assertTrue(featuresInfo.isOauthEnabled());
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "notifications");
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "twoFaSettings");
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "sms");
} }
@Test @Test

56
application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2023 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.controller;
import org.junit.Test;
import org.stringtemplate.v4.ST;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class BaseTelemetryControllerTest extends AbstractControllerTest {
@Test
public void testConstraintValidator() throws Exception {
loginTenantAdmin();
Device device = createDevice();
String correctRequestBody = "{\"data\": \"value\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", correctRequestBody, String.class, status().isOk());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", correctRequestBody, String.class, status().isOk());
String invalidRequestBody = "{\"<object data=\\\"data:text/html,<script>alert(document)</script>\\\"></object>\": \"data\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
}
private Device createDevice() throws Exception {
String testToken = "TEST_TOKEN";
Device device = new Device();
device.setName("My device");
device.setType("default");
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
deviceCredentials.setCredentialsId(testToken);
SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials);
return readResponse(doPost("/api/device-with-credentials", saveRequest).andExpect(status().isOk()), Device.class);
}
}

17
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationText.java → application/src/test/java/org/thingsboard/server/controller/sql/TelemetryControllerSqlTest.java

@ -13,18 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.common.data.notification.template; package org.thingsboard.server.controller.sql;
import lombok.AllArgsConstructor; import org.thingsboard.server.controller.BaseTelemetryControllerTest;
import lombok.Data; import org.thingsboard.server.dao.service.DaoSqlTest;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NotificationText {
private String body;
private String subject;
@DaoSqlTest
public class TelemetryControllerSqlTest extends BaseTelemetryControllerTest {
} }

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

@ -108,6 +108,7 @@ public class EdgeImitator {
updateEdgeClientFields("rpcPort", port); updateEdgeClientFields("rpcPort", port);
updateEdgeClientFields("timeoutSecs", 3); updateEdgeClientFields("timeoutSecs", 3);
updateEdgeClientFields("keepAliveTimeSec", 300); updateEdgeClientFields("keepAliveTimeSec", 300);
updateEdgeClientFields("keepAliveTimeoutSec", 5);
updateEdgeClientFields("maxInboundMessageSize", 4194304); updateEdgeClientFields("maxInboundMessageSize", 4194304);
} }

266
application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java

@ -0,0 +1,266 @@
/**
* Copyright © 2016-2023 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.device.provision;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.Before;
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.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.transport.util.SslUtil;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
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.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.service.device.DeviceProvisionServiceImpl;;import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = DeviceProvisionServiceImpl.class)
public class DeviceProvisionServiceTest {
@MockBean
protected TbQueueProducerProvider producerProvider;
@MockBean
protected TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> ruleEngineMsgProducer;
@MockBean
protected TbClusterService clusterService;
@MockBean
protected DeviceProfileService deviceProfileService;
@MockBean
protected DeviceService deviceService;
@MockBean
protected DeviceCredentialsService deviceCredentialsService;
@MockBean
protected AttributesService attributesService;
@MockBean
protected AuditLogService auditLogService;
@MockBean
protected PartitionService partitionService;
@SpyBean
DeviceProvisionServiceImpl service;
private String[] chain;
@Before
public void setUp() {
String filePath = "src/test/resources/provision/x509ChainProvisionTest.pem";
try {
String certificateChain = Files.readString(Paths.get(filePath));
certificateChain = certTrimNewLinesForChainInDeviceProfile(certificateChain);
chain = fetchLeafCertificateFromChain(certificateChain);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
public void provisionDeviceViaX509Certificate() {
var tenant = createTenant();
var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], true);
var device = createDevice(tenant.getId(), deviceProfile.getId());
when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device);
var deviceCredentials = createDeviceCredentials(chain[0], device.getId());
when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials);
when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials);
ProvisionResponse response = service.provisionDeviceViaX509Chain(deviceProfile, createProvisionRequest(chain[0]));
verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any());
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any());
verify(deviceCredentialsService, times(1)).updateDeviceCredentials(any(), any());
Assertions.assertThat(response.getResponseStatus()).isEqualTo(ProvisionResponseStatus.SUCCESS);
Assertions.assertThat(response.getDeviceCredentials()).isEqualTo(deviceCredentials);
}
@Test
public void provisionDeviceWithIncorrectConfiguration() {
var tenant = createTenant();
var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], false);
Assertions.assertThatThrownBy(() ->
service.provisionDeviceViaX509Chain(deviceProfile, createProvisionRequest(chain[0])))
.isInstanceOf(ProvisionFailedException.class);
verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any());
}
@Test
public void matchDeviceNameFromX509CNCertificateByRegex() {
var tenant = createTenant();
var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], true);
X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
String CN = getCNFromX509Certificate(chain[0]);
String deviceName = service.extractDeviceNameFromCNByRegEx(deviceProfile, CN, configuration.getCertificateRegExPattern());
Assertions.assertThat(deviceName).isNotBlank();
Assertions.assertThat(deviceName).isEqualTo("deviceCertificate");
}
@Test
public void matchDeviceNameFromCNByRegex() {
var CN = "DeviceA.company.com";
var regex = "(.*)\\.company.com";
var result = service.extractDeviceNameFromCNByRegEx(null, CN, regex);
Assertions.assertThat(result).isNotBlank();
Assertions.assertThat(result).isEqualTo("DeviceA");
CN = "DeviceA@company.com";
regex = "(.*)@company.com";
result = service.extractDeviceNameFromCNByRegEx(null, CN, regex);
Assertions.assertThat(result).isNotBlank();
Assertions.assertThat(result).isEqualTo("DeviceA");
CN = "prefixDeviceAsuffix@company.com";
regex = "prefix(.*)suffix@company.com";
result = service.extractDeviceNameFromCNByRegEx(null, CN, regex);
Assertions.assertThat(result).isNotBlank();
Assertions.assertThat(result).isEqualTo("DeviceA");
CN = "prefixDeviceAsufix@company.com";
regex = "prefix(.*)sufix@company.com";
result = service.extractDeviceNameFromCNByRegEx(null, CN, regex);
Assertions.assertThat(result).isNotBlank();
Assertions.assertThat(result).isEqualTo("DeviceA");
CN = "region.DeviceA.220423@company.com";
regex = "\\D+\\.(.*)\\.\\d+@company.com";
result = service.extractDeviceNameFromCNByRegEx(null, CN, regex);
Assertions.assertThat(result).isNotBlank();
Assertions.assertThat(result).isEqualTo("DeviceA");
}
private DeviceProfile createDeviceProfile(TenantId tenantId, String certificateValue, boolean isAllowToCreateNewDevices) {
X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration();
provision.setProvisionDeviceSecret(certificateValue);
provision.setCertificateRegExPattern("([^@]+)");
provision.setAllowCreateNewDevicesByX509Certificate(isAllowToCreateNewDevices);
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setProvisionConfiguration(provision);
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setId(new DeviceProfileId(UUID.randomUUID()));
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setProvisionDeviceKey(EncryptionUtil.getSha3Hash(certificateValue));
deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN);
deviceProfile.setTenantId(tenantId);
return deviceProfile;
}
private Device createDevice(TenantId tenantId, DeviceProfileId deviceProfileId) {
Device device = new Device();
device.setTenantId(tenantId);
device.setId(new DeviceId(UUID.randomUUID()));
device.setDeviceProfileId(deviceProfileId);
device.setCustomerId(new CustomerId(UUID.randomUUID()));
return device;
}
private Tenant createTenant() {
Tenant tenant = new Tenant();
tenant.setId(new TenantId(UUID.randomUUID()));
return tenant;
}
private DeviceCredentials createDeviceCredentials(String certificateValue, DeviceId deviceId) {
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setDeviceId(deviceId);
deviceCredentials.setCredentialsValue(certificateValue);
deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(certificateValue));
deviceCredentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
return deviceCredentials;
}
private ProvisionRequest createProvisionRequest(String certificateValue) {
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE,
new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue),
null);
}
public static String certTrimNewLinesForChainInDeviceProfile(String input) {
return input.replaceAll("\n", "")
.replaceAll("\r", "")
.replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n")
.replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n")
.trim();
}
private String[] fetchLeafCertificateFromChain(String value) {
List<String> chain = new ArrayList<>();
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(value);
while (matcher.find()) {
chain.add(matcher.group(0));
}
return chain.toArray(new String[0]);
}
private String getCNFromX509Certificate(String x509Value) {
try {
return SslUtil.parseCommonName(SslUtil.readCertFile(x509Value));
} catch (Exception e) {
return null;
}
}
}

31
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -35,6 +35,10 @@ import org.thingsboard.server.common.data.notification.NotificationRequestConfig
import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
@ -52,6 +56,7 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -195,6 +200,32 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
new PageLink(limit, 0), unreadOnly).getData(); new PageLink(limit, 0), unreadOnly).getData();
} }
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) {
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, NotificationDeliveryMethod.WEB);
NotificationRule rule = new NotificationRule();
rule.setName(triggerConfig.getTriggerType() + " [" + Arrays.toString(targets) + "]");
rule.setTemplateId(template.getId());
rule.setTriggerType(triggerConfig.getTriggerType());
rule.setTriggerConfig(triggerConfig);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTargets(DaoUtil.toUUIDs(List.of(targets)));
rule.setRecipientsConfig(recipientsConfig);
return saveNotificationRule(rule);
}
protected NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}
protected PageData<NotificationRuleInfo> findNotificationRules() throws Exception {
PageLink pageLink = new PageLink(10);
return doGetTypedWithPageLink("/api/notification/rules?", new TypeReference<PageData<NotificationRuleInfo>>() {}, pageLink);
}
@Override @Override
protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort, token); NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort, token);

7
application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java

@ -19,17 +19,14 @@ import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService; import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
@Service @Service
@Primary @Primary
public class MockNotificationSettingsService extends DefaultNotificationSettingsService { public class MockNotificationSettingsService extends DefaultNotificationSettingsService {
public MockNotificationSettingsService(AdminSettingsService adminSettingsService, NotificationTargetService notificationTargetService, NotificationTemplateService notificationTemplateService, NotificationRuleService notificationRuleService) { public MockNotificationSettingsService(AdminSettingsService adminSettingsService) {
super(adminSettingsService, notificationTargetService, notificationTemplateService, notificationRuleService); super(adminSettingsService, null, null);
} }
@Override @Override

98
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.notification.targets.platform.Customer
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
@ -49,7 +50,6 @@ import org.thingsboard.server.common.data.notification.template.SlackDeliveryMet
import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.notification.NotificationDao; import org.thingsboard.server.dao.notification.NotificationDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.DbCallbackExecutorService;
@ -226,13 +226,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText, 5); NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText, 5);
assertThat(notificationRequest.getStatus()).isEqualTo(NotificationRequestStatus.SCHEDULED); assertThat(notificationRequest.getStatus()).isEqualTo(NotificationRequestStatus.SCHEDULED);
await().atLeast(4, TimeUnit.SECONDS) await().atLeast(4, TimeUnit.SECONDS)
.atMost(6, TimeUnit.SECONDS) .atMost(15, TimeUnit.SECONDS)
.until(() -> wsClient.getLastMsg() != null); .until(() -> wsClient.getLastMsg() != null);
Notification delayedNotification = wsClient.getLastDataUpdate().getUpdate(); Notification delayedNotification = wsClient.getLastDataUpdate().getUpdate();
assertThat(delayedNotification).extracting(Notification::getText).isEqualTo(notificationText); assertThat(delayedNotification).extracting(Notification::getText).isEqualTo(notificationText);
assertThat(delayedNotification.getCreatedTime() - notificationRequest.getCreatedTime()) assertThat(delayedNotification.getCreatedTime() - notificationRequest.getCreatedTime())
.isCloseTo(TimeUnit.SECONDS.toMillis(5), Offset.offset(500L)); .isCloseTo(TimeUnit.SECONDS.toMillis(5), Offset.offset(10000L));
assertThat(findNotificationRequest(notificationRequest.getId()).getStatus()).isEqualTo(NotificationRequestStatus.SENT); assertThat(findNotificationRequest(notificationRequest.getId()).getStatus()).isEqualTo(NotificationRequestStatus.SENT);
} }
@ -324,16 +324,17 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test @Test
public void testNotificationRequestPreview() throws Exception { public void testNotificationRequestPreview() throws Exception {
NotificationTarget target1 = new NotificationTarget(); NotificationTarget tenantAdminTarget = new NotificationTarget();
target1.setName("Me"); tenantAdminTarget.setName("Me");
PlatformUsersNotificationTargetConfig target1Config = new PlatformUsersNotificationTargetConfig(); PlatformUsersNotificationTargetConfig tenantAdminTargetConfig = new PlatformUsersNotificationTargetConfig();
UserListFilter userListFilter = new UserListFilter(); UserListFilter userListFilter = new UserListFilter();
userListFilter.setUsersIds(DaoUtil.toUUIDs(List.of(tenantAdminUserId))); userListFilter.setUsersIds(List.of(tenantAdminUserId.getId()));
target1Config.setUsersFilter(userListFilter); tenantAdminTargetConfig.setUsersFilter(userListFilter);
target1.setConfiguration(target1Config); tenantAdminTarget.setConfiguration(tenantAdminTargetConfig);
target1 = saveNotificationTarget(target1); tenantAdminTarget = saveNotificationTarget(tenantAdminTarget);
List<String> recipients = new ArrayList<>(); List<String> recipients = new ArrayList<>();
recipients.add(TENANT_ADMIN_EMAIL); recipients.add(TENANT_ADMIN_EMAIL);
String firstRecipientEmail = TENANT_ADMIN_EMAIL;
createDifferentCustomer(); createDifferentCustomer();
loginTenantAdmin(); loginTenantAdmin();
@ -347,21 +348,33 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
customerUser = createUser(customerUser, "12345678"); customerUser = createUser(customerUser, "12345678");
recipients.add(customerUser.getEmail()); recipients.add(customerUser.getEmail());
} }
NotificationTarget target2 = new NotificationTarget(); NotificationTarget customerUsersTarget = new NotificationTarget();
target2.setName("Other customer users"); customerUsersTarget.setName("Other customer users");
PlatformUsersNotificationTargetConfig target2Config = new PlatformUsersNotificationTargetConfig(); PlatformUsersNotificationTargetConfig customerUsersTargetConfig = new PlatformUsersNotificationTargetConfig();
CustomerUsersFilter customerUsersFilter = new CustomerUsersFilter(); CustomerUsersFilter customerUsersFilter = new CustomerUsersFilter();
customerUsersFilter.setCustomerId(differentCustomerId.getId()); customerUsersFilter.setCustomerId(differentCustomerId.getId());
target2Config.setUsersFilter(customerUsersFilter); customerUsersTargetConfig.setUsersFilter(customerUsersFilter);
target2.setConfiguration(target2Config); customerUsersTarget.setConfiguration(customerUsersTargetConfig);
target2 = saveNotificationTarget(target2); customerUsersTarget = saveNotificationTarget(customerUsersTarget);
NotificationTarget slackTarget = new NotificationTarget();
slackTarget.setName("Slack user");
SlackNotificationTargetConfig slackTargetConfig = new SlackNotificationTargetConfig();
slackTargetConfig.setConversationType(SlackConversationType.DIRECT);
SlackConversation slackConversation = new SlackConversation();
slackConversation.setType(SlackConversationType.DIRECT);
slackConversation.setId("U1234567");
slackConversation.setName("jdoe");
slackConversation.setWholeName("John Doe");
slackTargetConfig.setConversation(slackConversation);
slackTarget.setConfiguration(slackTargetConfig);
slackTarget = saveNotificationTarget(slackTarget);
recipients.add("@" + slackConversation.getWholeName());
NotificationTemplate notificationTemplate = new NotificationTemplate(); NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setNotificationType(NotificationType.GENERAL); notificationTemplate.setNotificationType(NotificationType.GENERAL);
notificationTemplate.setName("Test template"); notificationTemplate.setName("Test template");
String requestorEmail = TENANT_ADMIN_EMAIL;
NotificationTemplateConfig templateConfig = new NotificationTemplateConfig(); NotificationTemplateConfig templateConfig = new NotificationTemplateConfig();
HashMap<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates = new HashMap<>(); HashMap<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates = new HashMap<>();
templateConfig.setDeliveryMethodsTemplates(templates); templateConfig.setDeliveryMethodsTemplates(templates);
@ -369,69 +382,59 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
WebDeliveryMethodNotificationTemplate webNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); WebDeliveryMethodNotificationTemplate webNotificationTemplate = new WebDeliveryMethodNotificationTemplate();
webNotificationTemplate.setEnabled(true); webNotificationTemplate.setEnabled(true);
webNotificationTemplate.setBody("Message for WEB: ${recipientEmail} ${unknownParam}"); webNotificationTemplate.setSubject("WEB SUBJECT: ${recipientEmail}");
webNotificationTemplate.setSubject("Subject for WEB: ${recipientEmail}"); webNotificationTemplate.setBody("WEB: ${recipientEmail} ${unknownParam}");
templates.put(NotificationDeliveryMethod.WEB, webNotificationTemplate); templates.put(NotificationDeliveryMethod.WEB, webNotificationTemplate);
SmsDeliveryMethodNotificationTemplate smsNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); SmsDeliveryMethodNotificationTemplate smsNotificationTemplate = new SmsDeliveryMethodNotificationTemplate();
smsNotificationTemplate.setEnabled(true); smsNotificationTemplate.setEnabled(true);
smsNotificationTemplate.setBody("Message for SMS: ${recipientEmail}"); smsNotificationTemplate.setBody("SMS: ${recipientEmail}");
templates.put(NotificationDeliveryMethod.SMS, smsNotificationTemplate); templates.put(NotificationDeliveryMethod.SMS, smsNotificationTemplate);
EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
emailNotificationTemplate.setEnabled(true); emailNotificationTemplate.setEnabled(true);
emailNotificationTemplate.setSubject("Subject for EMAIL: ${recipientEmail}"); emailNotificationTemplate.setSubject("EMAIL SUBJECT: ${recipientEmail}");
emailNotificationTemplate.setBody("Message for EMAIL: ${recipientEmail}"); emailNotificationTemplate.setBody("EMAIL: ${recipientEmail}");
templates.put(NotificationDeliveryMethod.EMAIL, emailNotificationTemplate); templates.put(NotificationDeliveryMethod.EMAIL, emailNotificationTemplate);
SlackDeliveryMethodNotificationTemplate slackNotificationTemplate = new SlackDeliveryMethodNotificationTemplate(); SlackDeliveryMethodNotificationTemplate slackNotificationTemplate = new SlackDeliveryMethodNotificationTemplate();
slackNotificationTemplate.setEnabled(true); slackNotificationTemplate.setEnabled(true);
slackNotificationTemplate.setBody("Message for SLACK: ${recipientEmail}"); slackNotificationTemplate.setBody("SLACK: ${recipientFirstName} ${recipientLastName}");
templates.put(NotificationDeliveryMethod.SLACK, slackNotificationTemplate); templates.put(NotificationDeliveryMethod.SLACK, slackNotificationTemplate);
notificationTemplate = saveNotificationTemplate(notificationTemplate); notificationTemplate = saveNotificationTemplate(notificationTemplate);
NotificationRequest notificationRequest = new NotificationRequest(); NotificationRequest notificationRequest = new NotificationRequest();
notificationRequest.setTargets(List.of(target1.getUuidId(), target2.getUuidId())); notificationRequest.setTargets(List.of(tenantAdminTarget.getUuidId(), customerUsersTarget.getUuidId(), slackTarget.getUuidId()));
notificationRequest.setTemplateId(notificationTemplate.getId()); notificationRequest.setTemplateId(notificationTemplate.getId());
notificationRequest.setAdditionalConfig(new NotificationRequestConfig()); notificationRequest.setAdditionalConfig(new NotificationRequestConfig());
NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class); NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class);
assertThat(preview.getRecipientsCountByTarget().get(target1.getName())).isEqualTo(1); assertThat(preview.getRecipientsCountByTarget().get(tenantAdminTarget.getName())).isEqualTo(1);
assertThat(preview.getRecipientsCountByTarget().get(target2.getName())).isEqualTo(customerUsersCount); assertThat(preview.getRecipientsCountByTarget().get(customerUsersTarget.getName())).isEqualTo(customerUsersCount);
assertThat(preview.getTotalRecipientsCount()).isEqualTo(1 + customerUsersCount); assertThat(preview.getRecipientsCountByTarget().get(slackTarget.getName())).isEqualTo(1);
assertThat(preview.getTotalRecipientsCount()).isEqualTo(2 + customerUsersCount);
assertThat(preview.getRecipientsPreview()).containsAll(recipients); assertThat(preview.getRecipientsPreview()).containsAll(recipients);
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = preview.getProcessedTemplates(); Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = preview.getProcessedTemplates();
assertThat(processedTemplates.get(NotificationDeliveryMethod.WEB)).asInstanceOf(type(WebDeliveryMethodNotificationTemplate.class)) assertThat(processedTemplates.get(NotificationDeliveryMethod.WEB)).asInstanceOf(type(WebDeliveryMethodNotificationTemplate.class))
.satisfies(template -> { .satisfies(template -> {
assertThat(template.getBody()) assertThat(template.getSubject()).isEqualTo("WEB SUBJECT: " + firstRecipientEmail);
.startsWith("Message for WEB") assertThat(template.getBody()).isEqualTo("WEB: " + firstRecipientEmail + " ${unknownParam}");
.endsWith(requestorEmail + " ${unknownParam}");
assertThat(template.getSubject())
.startsWith("Subject for WEB")
.endsWith(requestorEmail);
}); });
assertThat(processedTemplates.get(NotificationDeliveryMethod.SMS)).asInstanceOf(type(SmsDeliveryMethodNotificationTemplate.class)) assertThat(processedTemplates.get(NotificationDeliveryMethod.SMS)).asInstanceOf(type(SmsDeliveryMethodNotificationTemplate.class))
.satisfies(template -> { .satisfies(template -> {
assertThat(template.getBody()) assertThat(template.getBody()).isEqualTo("SMS: " + firstRecipientEmail);
.startsWith("Message for SMS")
.endsWith(requestorEmail);
}); });
assertThat(processedTemplates.get(NotificationDeliveryMethod.EMAIL)).asInstanceOf(type(EmailDeliveryMethodNotificationTemplate.class)) assertThat(processedTemplates.get(NotificationDeliveryMethod.EMAIL)).asInstanceOf(type(EmailDeliveryMethodNotificationTemplate.class))
.satisfies(template -> { .satisfies(template -> {
assertThat(template.getBody()) assertThat(template.getSubject()).isEqualTo("EMAIL SUBJECT: " + firstRecipientEmail);
.startsWith("Message for EMAIL") assertThat(template.getBody()).isEqualTo("EMAIL: " + firstRecipientEmail);
.endsWith(requestorEmail);
assertThat(template.getSubject())
.startsWith("Subject for EMAIL")
.endsWith(requestorEmail);
}); });
assertThat(processedTemplates.get(NotificationDeliveryMethod.SLACK)).asInstanceOf(type(SlackDeliveryMethodNotificationTemplate.class)) assertThat(processedTemplates.get(NotificationDeliveryMethod.SLACK)).asInstanceOf(type(SlackDeliveryMethodNotificationTemplate.class))
.satisfies(template -> { .satisfies(template -> {
assertThat(template.getBody()) assertThat(template.getBody()).isEqualTo("SLACK: John Doe");
.isEqualTo("Message for SLACK: ${recipientEmail}"); // ${recipientEmail} should not be processed
}); });
} }
@ -498,8 +501,9 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
notificationTarget.setName(conversationName + " in Slack"); notificationTarget.setName(conversationName + " in Slack");
SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig(); SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig();
targetConfig.setConversation(SlackConversation.builder() targetConfig.setConversation(SlackConversation.builder()
.type(SlackConversationType.DIRECT)
.id(conversationId) .id(conversationId)
.title(conversationName) .name(conversationName)
.build()); .build());
notificationTarget.setConfiguration(targetConfig); notificationTarget.setConfiguration(targetConfig);
notificationTarget = saveNotificationTarget(notificationTarget); notificationTarget = saveNotificationTarget(notificationTarget);

309
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -15,27 +15,26 @@
*/ */
package org.thingsboard.server.service.notification; package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.BooleanNode;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair; import org.springframework.data.util.Pair;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmCondition;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
@ -43,8 +42,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
@ -57,27 +55,28 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo; import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.apiusage.limits.LimitedApi;
import org.thingsboard.server.service.apiusage.limits.RateLimitService;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.ArrayList; import java.util.ArrayList;
@ -87,7 +86,10 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset; import static org.assertj.core.api.Assertions.offset;
@ -96,9 +98,6 @@ import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = {
"js.evaluator=local"
})
public class NotificationRuleApiTest extends AbstractNotificationApiTest { public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@SpyBean @SpyBean
@ -106,72 +105,48 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationRequestService notificationRequestService; private NotificationRequestService notificationRequestService;
@Autowired @Autowired
private NotificationRuleService notificationRuleService; private TenantProfileService tenantProfileService;
@Autowired @Autowired
private NotificationTemplateService notificationTemplateService; private TbTenantProfileService tbTenantProfileService;
@Autowired
@SpyBean private RateLimitService rateLimitService;
private AlarmService alarmService; @Autowired
private RuleChainService ruleChainService;
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
notificationRuleService.deleteNotificationRulesByTenantId(tenantId);
notificationTemplateService.deleteNotificationTemplatesByTenantId(tenantId);
} }
@Test @Test
public void testNotificationRuleProcessing_entityActionTrigger() throws Exception { public void testNotificationRuleProcessing_entityActionTrigger() throws Exception {
String notificationSubject = "${actionType}: ${entityType} [${entityId}]";
String notificationText = "User: ${userEmail}";
NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.GENERAL, notificationSubject, notificationText, NotificationDeliveryMethod.WEB);
NotificationRule notificationRule = new NotificationRule();
notificationRule.setName("Web notification when any device is created, updated or deleted");
notificationRule.setTemplateId(notificationTemplate.getId());
notificationRule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE)); triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE));
triggerConfig.setCreated(true); triggerConfig.setCreated(true);
triggerConfig.setUpdated(true); triggerConfig.setUpdated(true);
triggerConfig.setDeleted(true); triggerConfig.setDeleted(true);
createNotificationRule(triggerConfig, "${actionType}: ${entityType} [${entityId}]",
"User: ${userEmail}", createNotificationTarget(tenantAdminUserId).getId());
Device device = checkNotificationAfter(() -> {
return createDevice("DEVICE!!!", "default", "12345");
}, (notification, newDevice) -> {
assertThat(notification.getSubject()).isEqualTo("added: Device [" + newDevice.getId() + "]");
assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL);
});
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); checkNotificationAfter(() -> {
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); device.setName("Updated name");
recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); doPost("/api/device", device, Device.class);
}, notification -> {
notificationRule.setTriggerConfig(triggerConfig); assertThat(notification.getSubject()).isEqualTo("updated: Device [" + device.getId() + "]");
notificationRule.setRecipientsConfig(recipientsConfig); });
notificationRule = saveNotificationRule(notificationRule);
getWsClient().subscribeForUnreadNotifications(10).waitForReply(true);
getWsClient().registerWaitForUpdate();
Device device = createDevice("DEVICE!!!", "default", "12345");
getWsClient().waitForUpdate(true);
Notification notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("added: Device [" + device.getId() + "]");
assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL);
getWsClient().registerWaitForUpdate();
device.setName("Updated name");
device = doPost("/api/device", device, Device.class);
getWsClient().waitForUpdate(true);
notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("updated: Device [" + device.getId() + "]");
getWsClient().registerWaitForUpdate();
doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
getWsClient().waitForUpdate(true);
notification = getWsClient().getLastDataUpdate().getUpdate(); checkNotificationAfter(() -> {
assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]"); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
}, notification -> {
assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]");
});
} }
@Test @Test
@ -208,7 +183,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
String alarmType = "myBoolIsTrue"; String alarmType = "myBoolIsTrue";
DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType); DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType);
Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); Device device = createDevice("Device 1", deviceProfile.getName(), "1234");
clients.values().forEach(wsClient -> { clients.values().forEach(wsClient -> {
@ -230,7 +205,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
clients.forEach((expectedDelay, wsClient) -> { clients.forEach((expectedDelay, wsClient) -> {
Notification notification = wsClient.getLastDataUpdate().getUpdate(); Notification notification = wsClient.getLastDataUpdate().getUpdate();
double actualDelay = (double) (notification.getCreatedTime() - ts) / 1000; double actualDelay = (double) (notification.getCreatedTime() - ts) / 1000;
assertThat(actualDelay).isCloseTo(expectedDelay, offset(0.5)); assertThat(actualDelay).isCloseTo(expectedDelay, offset(2.0));
assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " + assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " +
"severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId()); "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId());
@ -272,7 +247,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM); notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM);
String alarmType = "myBoolIsTrue"; String alarmType = "myBoolIsTrue";
DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType); DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType);
Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); Device device = createDevice("Device 1", deviceProfile.getName(), "1234");
AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig(); AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig();
@ -327,6 +302,79 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty(); assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty();
} }
@Test
public void testNotificationRuleProcessing_entitiesLimit() throws Exception {
TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID);
TenantProfileData profileData = tenantProfile.getProfileData();
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration();
int limit = 5;
profileConfiguration.setMaxDevices(limit);
profileConfiguration.setMaxAssets(limit);
profileConfiguration.setMaxCustomers(limit);
profileConfiguration.setMaxUsers(limit);
profileConfiguration.setMaxDashboards(limit);
profileConfiguration.setMaxRuleChains(limit);
tenantProfile.setProfileData(profileData);
tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null);
EntitiesLimitNotificationRuleTriggerConfig triggerConfig = EntitiesLimitNotificationRuleTriggerConfig.builder()
.entityTypes(null).threshold(0.8f)
.build();
loginSysAdmin();
NotificationRule rule = createNotificationRule(triggerConfig, "${entityType}s limit will be reached soon",
"${entityType}s usage: ${currentCount}/${limit} (${percents}%)", createNotificationTarget(tenantAdminUserId).getId());
int threshold = (int) (limit * 0.8);
loginTenantAdmin();
checkNotificationAfter(() -> {
for (int i = 1; i <= threshold; i++) {
createDevice(i + "", i + "");
}
}, notification -> {
assertThat(notification.getText()).isEqualTo("Devices usage: " + threshold + "/" + limit + " (80%)");
});
checkNotificationAfter(() -> {
for (int i = 1; i <= threshold; i++) {
Asset asset = new Asset();
asset.setType("Test");
asset.setName(i + "");
doPost("/api/asset", asset);
}
}, notification -> {
assertThat(notification.getText()).isEqualTo("Assets usage: " + threshold + "/" + limit + " (80%)");
});
checkNotificationAfter(() -> {
long present = ruleChainService.countByTenantId(tenantId);
for (int i = 1; i <= threshold - present; i++) {
RuleChain ruleChain = new RuleChain();
ruleChain.setName(i + "");
ruleChain.setRoot(false);
ruleChain.setDebugMode(false);
ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChain.getId());
metaData.setNodes(List.of());
doPost("/api/ruleChain/metadata", metaData);
}
}, notification -> {
assertThat(notification.getText()).isEqualTo("Rule chains usage: " + threshold + "/" + limit + " (80%)");
});
triggerConfig.setThreshold(1.0f);
rule.setTriggerConfig(triggerConfig);
loginSysAdmin();
saveNotificationRule(rule);
loginTenantAdmin();
checkNotificationAfter(() -> {
createDevice(limit + "", limit + "");
}, notification -> {
assertThat(notification.getText()).isEqualTo("Devices usage: " + limit + "/" + limit + " (100%)");
});
}
@Test @Test
public void testNotificationRuleInfo() throws Exception { public void testNotificationRuleInfo() throws Exception {
NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL}; NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL};
@ -352,7 +400,80 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(ruleInfo.getDeliveryMethods()).containsOnly(deliveryMethods); assertThat(ruleInfo.getDeliveryMethods()).containsOnly(deliveryMethods);
} }
private DeviceProfile createDeviceProfileWithAlarmRules(NotificationRuleId notificationRuleId, String alarmType) { @Test
public void testNotificationRequestsPerRuleRateLimits() throws Exception {
int notificationRequestsLimit = 10;
TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID);
TenantProfileData profileData = tenantProfile.getProfileData();
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration();
profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(notificationRequestsLimit + ":300");
tenantProfile.setProfileData(profileData);
tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null);
NotificationRule rule = new NotificationRule();
rule.setName("Device created");
rule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
NotificationTemplate template = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Device created", "Device created",
NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.SMS);
rule.setTemplateId(template.getId());
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE));
triggerConfig.setCreated(true);
rule.setTriggerConfig(triggerConfig);
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
recipientsConfig.setTargets(List.of(target.getUuidId()));
rule.setRecipientsConfig(recipientsConfig);
rule = saveNotificationRule(rule);
for (int i = 0; i < notificationRequestsLimit; i++) {
String name = "device " + i;
createDevice(name, name);
}
await().atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit);
});
for (int i = 0; i < 5; i++) {
String name = "device " + (notificationRequestsLimit + i);
createDevice(name, name);
}
boolean rateLimitExceeded = !rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, tenantId, rule.getId());
assertThat(rateLimitExceeded).isTrue();
TimeUnit.SECONDS.sleep(3);
assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit);
}
private <R> R checkNotificationAfter(Callable<R> action, BiConsumer<Notification, R> check) throws Exception {
if (getWsClient().getLastDataUpdate() == null) {
getWsClient().subscribeForUnreadNotifications(10).waitForReply(true);
}
getWsClient().registerWaitForUpdate();
R result = action.call();
getWsClient().waitForUpdate(true);
check.accept(getWsClient().getLastDataUpdate().getUpdate(), result);
return result;
}
private void checkNotificationAfter(ThrowingRunnable action, Consumer<Notification> check) throws Exception {
checkNotificationAfter(() -> {
try {
action.run();
return null;
} catch (Throwable e) {
throw new Exception(e);
}
}, (notification, r) -> check.accept(notification));
}
private PageData<NotificationRequestInfo> findNotificationRequests(EntityType originatorType) {
return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(tenantId, originatorType, new PageLink(100));
}
private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) {
DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); DeviceProfile deviceProfile = createDeviceProfile("For notification rule test");
deviceProfile.setTenantId(tenantId); deviceProfile.setTenantId(tenantId);
@ -387,52 +508,4 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
return deviceProfile; return deviceProfile;
} }
private RuleChain createEmptyRuleChain(String name) {
RuleChain ruleChain = new RuleChain();
ruleChain.setName(name);
ruleChain.setTenantId(tenantId);
ruleChain.setRoot(false);
ruleChain.setDebugMode(false);
ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChain.getId());
metaData.setNodes(List.of());
metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class);
return ruleChain;
}
private RuleNode addRuleNodeWithError(RuleChainId ruleChainId, String name) {
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChainId);
RuleNode generatorNodeWithError = new RuleNode();
generatorNodeWithError.setName(name);
generatorNodeWithError.setType(TbMsgGeneratorNode.class.getName());
TbMsgGeneratorNodeConfiguration generatorNodeConfiguration = new TbMsgGeneratorNodeConfiguration();
generatorNodeConfiguration.setScriptLang(ScriptLanguage.JS);
generatorNodeConfiguration.setPeriodInSeconds(1000);
generatorNodeConfiguration.setMsgCount(1);
generatorNodeConfiguration.setJsScript("[return");
generatorNodeWithError.setConfiguration(mapper.valueToTree(generatorNodeConfiguration));
metaData.setNodes(List.of(generatorNodeWithError));
metaData.setFirstNodeIndex(0);
metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class);
return metaData.getNodes().get(0);
}
private NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}
private PageData<NotificationRuleInfo> findNotificationRules() throws Exception {
PageLink pageLink = new PageLink(10);
return doGetTypedWithPageLink("/api/notification/rules?", new TypeReference<PageData<NotificationRuleInfo>>() {}, pageLink);
}
private PageData<NotificationRequestInfo> findNotificationRequests(EntityType originatorType) {
return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(tenantId, originatorType, new PageLink(100));
}
} }

25
application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java

@ -23,6 +23,8 @@ import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.ResultMatcher;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
@ -30,7 +32,6 @@ import org.thingsboard.server.common.data.notification.targets.platform.Platform
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.notification.NotificationTargetDao; import org.thingsboard.server.dao.notification.NotificationTargetDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -41,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
public class NotificationTargetApiTest extends AbstractControllerTest { public class NotificationTargetApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationTargetDao notificationTargetDao; private NotificationTargetDao notificationTargetDao;
@ -148,6 +149,26 @@ public class NotificationTargetApiTest extends AbstractControllerTest {
assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty(); assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty();
} }
@Test
public void whenDeletingTargetUsedByRule_thenReturnError() throws Exception {
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
createNotificationRule(new EntityActionNotificationRuleTriggerConfig(), "Test", "Test", target.getId());
String error = getErrorMessage(doDelete("/api/notification/target/" + target.getId())
.andExpect(status().isBadRequest()));
assertThat(error).containsIgnoringCase("used in notification rule");
}
@Test
public void whenDeletingTargetUsedByScheduledNotificationRequest_thenReturnError() throws Exception {
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
submitNotificationRequest(target.getId(), "Test", 100, NotificationDeliveryMethod.WEB);
String error = getErrorMessage(doDelete("/api/notification/target/" + target.getId())
.andExpect(status().isBadRequest()));
assertThat(error).containsIgnoringCase("referenced by scheduled notification request");
}
private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception { private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception {
return getErrorMessage(save(notificationTarget, statusMatcher)); return getErrorMessage(save(notificationTarget, statusMatcher));
} }

10
application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.ResultMatcher;
import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.IdBased;
@ -30,8 +29,6 @@ import org.thingsboard.server.common.data.notification.template.NotificationTemp
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List; import java.util.List;
@ -43,16 +40,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest @DaoSqlTest
public class NotificationTemplateApiTest extends AbstractNotificationApiTest { public class NotificationTemplateApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationTemplateService templateService;
@Autowired
private NotificationRuleService notificationRuleService;
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
notificationRuleService.deleteNotificationRulesByTenantId(tenantId);
templateService.deleteNotificationTemplatesByTenantId(tenantId);
} }
@Test @Test

13
application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -35,17 +36,14 @@ import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"usage.stats.report.enabled=true", "usage.stats.report.enabled=true",
"transport.http.enabled=true",
"usage.stats.report.interval=2", "usage.stats.report.interval=2",
"usage.stats.gauge_report_interval=1", "usage.stats.gauge_report_interval=1",
"state.defaultStateCheckIntervalInSec=3", "state.defaultStateCheckIntervalInSec=3",
"state.defaultInactivityTimeoutInSec=10" "state.defaultInactivityTimeoutInSec=10"
}) })
public class DevicesStatisticsTest extends AbstractControllerTest { public class DevicesStatisticsTest extends AbstractControllerTest {
@ -53,6 +51,8 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
private TbApiUsageStateService apiUsageStateService; private TbApiUsageStateService apiUsageStateService;
@Autowired @Autowired
private TimeseriesService timeseriesService; private TimeseriesService timeseriesService;
@Autowired
private DeviceStateService deviceStateService;
private ApiUsageStateId apiUsageStateId; private ApiUsageStateId apiUsageStateId;
@ -86,7 +86,7 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
}); });
for (Device device : activeDevices) { for (Device device : activeDevices) {
postTelemetry(device.getName(), "{\"dp\":1}"); deviceStateService.onDeviceActivity(tenantId, device.getId(), System.currentTimeMillis());
} }
await().atMost(40, TimeUnit.SECONDS) await().atMost(40, TimeUnit.SECONDS)
@ -102,9 +102,4 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
.get().stream().findFirst().flatMap(KvEntry::getLongValue).orElse(null); .get().stream().findFirst().flatMap(KvEntry::getLongValue).orElse(null);
} }
@SneakyThrows
private void postTelemetry(String accessToken, String json) {
doPost("/api/v1/" + accessToken + "/telemetry", json, new String[0]).andExpect(status().isOk());
}
} }

55
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -68,7 +68,6 @@ import org.thingsboard.server.service.ota.OtaPackageStateService;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -254,21 +253,25 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2"); Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2");
Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1"); Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1");
DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing");
String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c";
String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa";
String entityAliases = "{\n" + String entityAliases = "{\n" +
"\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" + "\"" + aliasId + "\": {\n" +
"\t\t\"alias\": \"assets\",\n" + "\"alias\": \"assets\",\n" +
"\t\t\"filter\": {\n" + "\"filter\": {\n" +
"\t\t\t\"entityList\": [\n" + "\"entityList\": [\n" +
"\t\t\t\t\"" + asset1.getId().toString() + "\",\n" + "\"" + asset1.getId().toString() + "\",\n" +
"\t\t\t\t\"" + asset2.getId().toString() + "\"\n" + "\"" + asset2.getId().toString() + "\",\n" +
"\t\t\t],\n" + "\"" + tenantId1.getId().toString() + "\",\n" +
"\t\t\t\"entityType\": \"ASSET\",\n" + "\"" + existingDeviceProfile.getId().toString() + "\",\n" +
"\t\t\t\"resolveMultiple\": true,\n" + "\"" + unknownUuid + "\"\n" +
"\t\t\t\"type\": \"entityList\"\n" + "],\n" +
"\t\t},\n" + "\"resolveMultiple\": true\n" +
"\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" + "},\n" +
"\t}\n" + "\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}"; "}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
@ -287,11 +290,23 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity(); Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity();
Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity(); Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity();
Set<String> entityAliasEntitiesIds = Streams.stream(importedDashboard.getConfiguration() Map.Entry<String, JsonNode> entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next();
.get("entityAliases").elements().next().get("filter").get("entityList").elements()) assertThat(entityAlias.getKey()).isEqualTo(aliasId);
.map(JsonNode::asText).collect(Collectors.toSet()); assertThat(entityAlias.getValue().get("id").asText()).isEqualTo(aliasId);
assertThat(entityAliasEntitiesIds).doesNotContain(asset1.getId().toString(), asset2.getId().toString());
assertThat(entityAliasEntitiesIds).contains(importedAsset1.getId().toString(), importedAsset2.getId().toString()); List<String> aliasEntitiesIds = Streams.stream(entityAlias.getValue().get("filter").get("entityList").elements())
.map(JsonNode::asText).collect(Collectors.toList());
assertThat(aliasEntitiesIds).size().isEqualTo(5);
assertThat(aliasEntitiesIds).element(0).as("external asset 1 was replaced with imported one")
.isEqualTo(importedAsset1.getId().toString());
assertThat(aliasEntitiesIds).element(1).as("external asset 2 was replaced with imported one")
.isEqualTo(importedAsset2.getId().toString());
assertThat(aliasEntitiesIds).element(2).as("external tenant id was replaced with new tenant id")
.isEqualTo(tenantId2.toString());
assertThat(aliasEntitiesIds).element(3).as("existing device profile id was left as is")
.isEqualTo(existingDeviceProfile.getId().toString());
assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id")
.isEqualTo(tenantId2.toString());
} }
@ -469,7 +484,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1");
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId())
.map(entityId -> { .map(entityId -> {
try { try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()

211
application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java

@ -0,0 +1,211 @@
/**
* Copyright © 2016-2023 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.transport;
import com.google.common.util.concurrent.Futures;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
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.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.resource.TbResourceService;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = DefaultTransportApiService.class)
public class DefaultTransportApiServiceTest {
@MockBean
protected TbDeviceProfileCache deviceProfileCache;
@MockBean
protected TbTenantProfileCache tenantProfileCache;
@MockBean
protected TbApiUsageStateService apiUsageStateService;
@MockBean
protected DeviceService deviceService;
@MockBean
protected DeviceProfileService deviceProfileService;
@MockBean
protected RelationService relationService;
@MockBean
protected DeviceCredentialsService deviceCredentialsService;
@MockBean
protected DbCallbackExecutorService dbCallbackExecutorService;
@MockBean
protected TbClusterService tbClusterService;
@MockBean
protected DataDecodingEncodingService dataDecodingEncodingService;
@MockBean
protected DeviceProvisionService deviceProvisionService;
@MockBean
protected TbResourceService resourceService;
@MockBean
protected OtaPackageService otaPackageService;
@MockBean
protected OtaPackageDataCache otaPackageDataCache;
@MockBean
protected QueueService queueService;
@SpyBean
DefaultTransportApiService service;
private String certificateChain;
private String[] chain;
@Before
public void setUp() {
String filePath = "src/test/resources/provision/x509ChainProvisionTest.pem";
try {
certificateChain = Files.readString(Paths.get(filePath));
certificateChain = certTrimNewLinesForChainInDeviceProfile(certificateChain);
chain = fetchLeafCertificateFromChain(certificateChain);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
public void validateExistingDeviceByX509CertificateStrategy() {
var device = createDevice();
when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
var deviceCredentials = createDeviceCredentials(chain[0], device.getId());
when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(deviceCredentials);
service.validateOrCreateDeviceX509Certificate(certificateChain);
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any());
}
@Test
public void provisionDeviceX509Certificate() {
var deviceProfile = createDeviceProfile(chain[1]);
when(deviceProfileService.findDeviceProfileByProvisionDeviceKey(any())).thenReturn(deviceProfile);
var device = createDevice();
when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device);
when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
var deviceCredentials = createDeviceCredentials(chain[0], device.getId());
when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(null);
when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials);
var provisionResponse = createProvisionResponse(deviceCredentials);
when(deviceProvisionService.provisionDeviceViaX509Chain(any(), any())).thenReturn(provisionResponse);
service.validateOrCreateDeviceX509Certificate(certificateChain);
verify(deviceProfileService, times(1)).findDeviceProfileByProvisionDeviceKey(any());
verify(deviceService, times(1)).findDeviceByIdAsync(any(), any());
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any());
verify(deviceProvisionService, times(1)).provisionDeviceViaX509Chain(any(), any());
}
private DeviceProfile createDeviceProfile(String certificateValue) {
X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration();
provision.setProvisionDeviceSecret(certificateValue);
provision.setCertificateRegExPattern("([^@]+)");
provision.setAllowCreateNewDevicesByX509Certificate(true);
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setProvisionConfiguration(provision);
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setProvisionDeviceKey(EncryptionUtil.getSha3Hash(certificateValue));
deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN);
return deviceProfile;
}
private DeviceCredentials createDeviceCredentials(String certificateValue, DeviceId deviceId) {
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setDeviceId(deviceId);
deviceCredentials.setCredentialsValue(certificateValue);
deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(certificateValue));
deviceCredentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
return deviceCredentials;
}
private Device createDevice() {
Device device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
return device;
}
private ProvisionResponse createProvisionResponse(DeviceCredentials deviceCredentials) {
return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS);
}
public static String certTrimNewLinesForChainInDeviceProfile(String input) {
return input.replaceAll("\n", "")
.replaceAll("\r", "")
.replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n")
.replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n")
.trim();
}
private String[] fetchLeafCertificateFromChain(String value) {
List<String> chain = new ArrayList<>();
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(value);
while (matcher.find()) {
chain.add(matcher.group(0));
}
return chain.toArray(new String[0]);
}
}

36
application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2023 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.system;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;
@Slf4j
public class RestTemplateConvertersTest {
@Test
public void testJacksonXmlConverter() {
ClassLoader classLoader = RestTemplate.class.getClassLoader();
boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
Assertions.assertFalse(jackson2XmlPresent, "XmlMapper must not be present in classpath, please, exclude \"jackson-dataformat-xml\" dependency!");
//If this xml mapper will be present in classpath then we will get "Unsupported Media Type" in RestTemplate
}
}

28
application/src/test/resources/provision/x509ChainProvisionTest.pem

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIw
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy
b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1ow
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy
b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qK
QiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaC
kQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8G
A1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8w
CgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZ
uccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIw
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy
b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlow
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy
b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72k
rDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMy
LydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8G
A1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8w
CgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZ
NGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A==
-----END CERTIFICATE-----

5
common/cluster-api/src/main/proto/queue.proto

@ -179,6 +179,10 @@ message ValidateDeviceX509CertRequestMsg {
string hash = 1; string hash = 1;
} }
message ValidateOrCreateDeviceX509CertRequestMsg {
string certificateChain = 1;
}
message ValidateBasicMqttCredRequestMsg { message ValidateBasicMqttCredRequestMsg {
string clientId = 1; string clientId = 1;
string userName = 2; string userName = 2;
@ -942,6 +946,7 @@ message TransportApiRequestMsg {
GetDeviceRequestMsg deviceRequestMsg = 12; GetDeviceRequestMsg deviceRequestMsg = 12;
GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13; GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13;
GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14; GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14;
ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateX509CertRequestMsg = 15;
} }
/* Response from ThingsBoard Core Service to Transport Service */ /* Response from ThingsBoard Core Service to Transport Service */

5
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -105,6 +106,10 @@ public interface AlarmService extends EntityDaoService {
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId); AlarmStatus alarmStatus, String assigneeId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java

@ -15,10 +15,10 @@
*/ */
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import com.fasterxml.jackson.databind.JsonNode;
public interface DeviceCredentialsService { public interface DeviceCredentialsService {

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java

@ -39,6 +39,8 @@ public interface DeviceProfileService extends EntityDaoService {
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType); PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey);
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName); DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName);
DeviceProfile createDefaultDeviceProfile(TenantId tenantId); DeviceProfile createDefaultDeviceProfile(TenantId tenantId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.device.provision.ProvisionResponse;
@ -22,4 +23,6 @@ import org.thingsboard.server.dao.device.provision.ProvisionResponse;
public interface DeviceProvisionService { public interface DeviceProvisionService {
ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) throws ProvisionFailedException; ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) throws ProvisionFailedException;
ProvisionResponse provisionDeviceViaX509Chain(DeviceProfile deviceProfile, ProvisionRequest provisionRequest) throws ProvisionFailedException;
} }

4
common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java

@ -16,7 +16,11 @@
package org.thingsboard.server.dao.device.provision; package org.thingsboard.server.dao.device.provision;
public class ProvisionFailedException extends RuntimeException { public class ProvisionFailedException extends RuntimeException {
private static final long serialVersionUID = 1673991117668477401L;
public ProvisionFailedException(String errorMsg) { public ProvisionFailedException(String errorMsg) {
super(errorMsg); super(errorMsg);
} }
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -74,8 +74,8 @@ public class DataConstants {
public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED"; public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED";
public static final String ALARM_ACK = "ALARM_ACK"; public static final String ALARM_ACK = "ALARM_ACK";
public static final String ALARM_CLEAR = "ALARM_CLEAR"; public static final String ALARM_CLEAR = "ALARM_CLEAR";
public static final String ALARM_ASSIGN = "ALARM_ASSIGN"; public static final String ALARM_ASSIGNED = "ALARM_ASSIGNED";
public static final String ALARM_UNASSIGN = "ALARM_UNASSIGN"; public static final String ALARM_UNASSIGNED = "ALARM_UNASSIGNED";
public static final String ALARM_DELETE = "ALARM_DELETE"; public static final String ALARM_DELETE = "ALARM_DELETE";
public static final String COMMENT_CREATED = "COMMENT_CREATED"; public static final String COMMENT_CREATED = "COMMENT_CREATED";
public static final String COMMENT_UPDATED = "COMMENT_UPDATED"; public static final String COMMENT_UPDATED = "COMMENT_UPDATED";

3
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java

@ -18,5 +18,6 @@ package org.thingsboard.server.common.data;
public enum DeviceProfileProvisionType { public enum DeviceProfileProvisionType {
DISABLED, DISABLED,
ALLOW_CREATE_NEW_DEVICES, ALLOW_CREATE_NEW_DEVICES,
CHECK_PRE_PROVISIONED_DEVICES CHECK_PRE_PROVISIONED_DEVICES,
X509_CERTIFICATE_CHAIN
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -46,6 +46,10 @@ public class StringUtils {
return source != null && !source.isEmpty() && !source.trim().isEmpty(); return source != null && !source.isEmpty() && !source.trim().isEmpty();
} }
public static String notBlankOrDefault(String src, String def) {
return isNotBlank(src) ? src : def;
}
public static String removeStart(final String str, final String remove) { public static String removeStart(final String str, final String remove) {
if (isEmpty(str) || isEmpty(remove)) { if (isEmpty(str) || isEmpty(remove)) {
return str; return str;

6
common/data/src/main/java/org/thingsboard/server/common/data/User.java

@ -74,7 +74,7 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
@ApiModelProperty(position = 1, value = "JSON object with the User Id. " + @ApiModelProperty(position = 1, value = "JSON object with the User Id. " +
"Specify this field to update the device. " + "Specify this field to update the device. " +
"Referencing non-existing User Id will cause error. " + "Referencing non-existing User Id will cause error. " +
"Omit this field to create new customer." ) "Omit this field to create new customer.")
@Override @Override
public UserId getId() { public UserId getId() {
return super.getId(); return super.getId();
@ -169,6 +169,10 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
@JsonIgnore @JsonIgnore
public String getTitle() { public String getTitle() {
return getTitle(email, firstName, lastName);
}
public static String getTitle(String email, String firstName, String lastName) {
String title = ""; String title = "";
if (isNotEmpty(firstName)) { if (isNotEmpty(firstName)) {
title += firstName; title += firstName;

39
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.alarm;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
public class AlarmQueryV2 {
private EntityId affectedEntityId;
private TimePageLink pageLink;
private List<String> typeList;
private List<AlarmSearchStatus> statusList;
private List<AlarmSeverity> severityList;
private UserId assigneeId;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java

@ -40,8 +40,8 @@ public enum ActionType {
ALARM_ACK(false), ALARM_ACK(false),
ALARM_CLEAR(false), ALARM_CLEAR(false),
ALARM_DELETE(false), ALARM_DELETE(false),
ALARM_ASSIGN(false), ALARM_ASSIGNED(false),
ALARM_UNASSIGN(false), ALARM_UNASSIGNED(false),
LOGIN(false), LOGIN(false),
LOGOUT(false), LOGOUT(false),
LOCKOUT(false), LOCKOUT(false),

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

@ -31,7 +31,8 @@ import java.io.Serializable;
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
@JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"),
@JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")}) @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES"),
@JsonSubTypes.Type(value = X509CertificateChainProvisionConfiguration.class, name = "X509_CERTIFICATE_CHAIN")})
public interface DeviceProfileProvisionConfiguration extends Serializable { public interface DeviceProfileProvisionConfiguration extends Serializable {
String getProvisionDeviceSecret(); String getProvisionDeviceSecret();

19
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java

@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.device.profile;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Objects;
import java.util.Set; import java.util.Set;
@Data @Data
@ -42,12 +44,19 @@ public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTra
} }
public TransportPayloadTypeConfiguration getTransportPayloadTypeConfiguration() { public TransportPayloadTypeConfiguration getTransportPayloadTypeConfiguration() {
if (transportPayloadTypeConfiguration != null) { return Objects.requireNonNullElseGet(transportPayloadTypeConfiguration, JsonTransportPayloadConfiguration::new);
return transportPayloadTypeConfiguration;
} else {
return new JsonTransportPayloadConfiguration();
}
} }
public String getDeviceTelemetryTopic() {
return StringUtils.notBlankOrDefault(deviceTelemetryTopic, MqttTopics.DEVICE_TELEMETRY_TOPIC);
}
public String getDeviceAttributesTopic() {
return StringUtils.notBlankOrDefault(deviceAttributesTopic, MqttTopics.DEVICE_ATTRIBUTES_TOPIC);
}
public String getDeviceAttributesSubscribeTopic() {
return StringUtils.notBlankOrDefault(deviceAttributesSubscribeTopic, MqttTopics.DEVICE_ATTRIBUTES_TOPIC);
}
} }

36
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.device.profile;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@Data
@NoArgsConstructor
public class X509CertificateChainProvisionConfiguration implements DeviceProfileProvisionConfiguration {
private String provisionDeviceSecret;
private String certificateRegExPattern;
private boolean allowCreateNewDevicesByX509Certificate;
@Override
public DeviceProfileProvisionType getType() {
return DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java

@ -31,8 +31,8 @@ public enum EdgeEventActionType {
RPC_CALL, RPC_CALL,
ALARM_ACK, ALARM_ACK,
ALARM_CLEAR, ALARM_CLEAR,
ALARM_ASSIGN, ALARM_ASSIGNED,
ALARM_UNASSIGN, ALARM_UNASSIGNED,
ASSIGNED_TO_EDGE, ASSIGNED_TO_EDGE,
UNASSIGNED_FROM_EDGE, UNASSIGNED_FROM_EDGE,
CREDENTIALS_REQUEST, CREDENTIALS_REQUEST,

31
common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantNotFoundException.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.exception;
import lombok.Getter;
import org.thingsboard.server.common.data.id.TenantId;
public class TenantNotFoundException extends RuntimeException {
@Getter
private final TenantId tenantId;
public TenantNotFoundException(TenantId tenantId) {
super("Tenant with id " + tenantId + " not found");
this.tenantId = tenantId;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -23,6 +24,7 @@ import java.util.Optional;
public abstract class BasicKvEntry implements KvEntry { public abstract class BasicKvEntry implements KvEntry {
@Length(fieldName = "attribute key") @Length(fieldName = "attribute key")
@NoXss
private final String key; private final String key;
protected BasicKvEntry(String key) { protected BasicKvEntry(String key) {

2
common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java

@ -15,12 +15,14 @@
*/ */
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import javax.validation.Valid;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
public class BasicTsKvEntry implements TsKvEntry { public class BasicTsKvEntry implements TsKvEntry {
private static final int MAX_CHARS_PER_DATA_POINT = 512; private static final int MAX_CHARS_PER_DATA_POINT = 512;
protected final long ts; protected final long ts;
@Valid
private final KvEntry kv; private final KvEntry kv;
public BasicTsKvEntry(long ts, KvEntry kv) { public BasicTsKvEntry(long ts, KvEntry kv) {

1
common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java

@ -19,6 +19,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
public class JsonDataEntry extends BasicKvEntry { public class JsonDataEntry extends BasicKvEntry {
private final String value; private final String value;
public JsonDataEntry(String key, String value) { public JsonDataEntry(String key, String value) {

2
common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java

@ -65,7 +65,7 @@ public class StringDataEntry extends BasicKvEntry {
public String toString() { public String toString() {
return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString(); return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString();
} }
@Override @Override
public String getValueAsString() { public String getValueAsString() {
return value; return value;

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -27,6 +27,6 @@ public enum NotificationType {
NEW_PLATFORM_VERSION, NEW_PLATFORM_VERSION,
ENTITIES_LIMIT, ENTITIES_LIMIT,
API_USAGE_LIMIT, API_USAGE_LIMIT,
RULE_ENGINE RULE_NODE
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -59,10 +60,12 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
return mapOf( return mapOf(
"action", action, "action", action,
"assigneeTitle", User.getTitle(assigneeEmail, assigneeFirstName, assigneeLastName),
"assigneeFirstName", assigneeFirstName, "assigneeFirstName", assigneeFirstName,
"assigneeLastName", assigneeLastName, "assigneeLastName", assigneeLastName,
"assigneeEmail", assigneeEmail, "assigneeEmail", assigneeEmail,
"assigneeId", assigneeId != null ? assigneeId.toString() : null, "assigneeId", assigneeId != null ? assigneeId.toString() : null,
"userTitle", User.getTitle(userEmail, userFirstName, userLastName),
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName, "userLastName", userLastName,

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -55,6 +56,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
return mapOf( return mapOf(
"comment", comment, "comment", comment,
"action", action, "action", action,
"userTitle", User.getTitle(userEmail, userFirstName, userLastName),
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName, "userLastName", userLastName,

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntityActionNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -40,6 +41,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
private CustomerId entityCustomerId; private CustomerId entityCustomerId;
private UUID userId; private UUID userId;
private String userTitle;
private String userEmail; private String userEmail;
private String userFirstName; private String userFirstName;
private String userLastName; private String userLastName;
@ -52,6 +54,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
"entityName", entityName, "entityName", entityName,
"actionType", actionType.name().toLowerCase(), "actionType", actionType.name().toLowerCase(),
"userId", userId.toString(), "userId", userId.toString(),
"userTitle", userTitle,
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName "userLastName", userLastName

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid; import javax.validation.Valid;
@ -43,6 +44,7 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
private TenantId tenantId; private TenantId tenantId;
@NotBlank @NotBlank
@NoXss @NoXss
@Length(max = 255, message = "cannot be longer than 255 chars")
private String name; private String name;
@NotNull @NotNull
private NotificationTemplateId templateId; private NotificationTemplateId templateId;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java

@ -15,7 +15,10 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@ -23,6 +26,9 @@ import javax.validation.constraints.NotEmpty;
import java.util.Set; import java.util.Set;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<String> alarmTypes; private Set<String> alarmTypes;

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

Loading…
Cancel
Save