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 = 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;
CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active
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;
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
@ -609,4 +613,17 @@ BEGIN
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.widget.WidgetTypeService;
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.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.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
@ -179,6 +180,10 @@ public class ActorSystemContext {
@Setter
private ComponentDiscoveryService componentService;
@Autowired
@Getter
private DiscoveryService discoveryService;
@Autowired
@Getter
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.RuleNodeException;
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.gen.transport.TransportProtos;
/**
* @author Andrew Shvayka
@ -39,11 +42,10 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient;
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
private final String ruleChainName;
private final TbActorRef self;
private final TbApiUsageReportClient apiUsageClient;
private final DefaultTbContext defaultCtx;
private RuleNode ruleNode;
private TbNode tbNode;
private DefaultTbContext defaultCtx;
private RuleNodeInfo info;
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
@ -51,7 +53,6 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
super(systemContext, tenantId, ruleNodeId);
this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChainName;
this.self = self;
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode));
this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown");
@ -59,29 +60,36 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override
public void start(TbActorCtx context) throws Exception {
tbNode = initComponent(ruleNode);
if (tbNode != null) {
state = ComponentLifecycleState.ACTIVE;
if (isMyNodePartition()) {
tbNode = initComponent(ruleNode);
if (tbNode != null) {
state = ComponentLifecycleState.ACTIVE;
}
}
}
@Override
public void onUpdate(TbActorCtx context) throws Exception {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown");
boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.ruleNode = newRuleNode;
this.defaultCtx.updateSelf(newRuleNode);
if (restartRequired) {
if (tbNode != null) {
tbNode.destroy();
}
try {
start(context);
} catch (Exception e) {
throw new TbRuleNodeUpdateException("Failed to update rule node", e);
if (isMyNodePartition()) {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown");
boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.ruleNode = newRuleNode;
this.defaultCtx.updateSelf(newRuleNode);
if (restartRequired) {
if (tbNode != null) {
tbNode.destroy();
}
try {
start(context);
} 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
public void onPartitionChangeMsg(PartitionChangeMsg msg) {
public void onPartitionChangeMsg(PartitionChangeMsg msg) throws Exception {
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 {
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);
}
if (!isMyNodePartition()) {
putToNodePartition(msg.getMsg());
} 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() {
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);
}
} catch (Exception e) {
log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e);
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.AlarmInfo;
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.AlarmSeverity;
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.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
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. " +
"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_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_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_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_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'.";
@ -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)",
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

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.RuleChainType;
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.widget.WidgetTypeDetails;
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.rpc.RpcService;
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.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
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.WidgetsBundleService;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
@ -164,7 +163,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
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}
* */
@ExceptionHandler(MethodArgumentNotValidException.class)
public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) {
String errorMessage = "Validation error: " + e.getFieldErrors().stream()
public void handleValidationError(MethodArgumentNotValidException validationError, HttpServletResponse response) {
List<ConstraintViolation<Object>> constraintsViolations = validationError.getFieldErrors().stream()
.map(fieldError -> {
String property = fieldError.getField();
if (property.equals("valid") || StringUtils.endsWith(property, ".valid")) { // when custom @AssertTrue is used
property = "";
try {
return (ConstraintViolation<Object>) fieldError.unwrap(ConstraintViolation.class);
} 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);
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) {
EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType);
entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity,
user.getCustomerId(), actionType, e);
if (!user.isSystemAdmin()) {
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 {

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.NotificationTargetType;
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.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData;
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.NotificationService;
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 javax.validation.Valid;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -201,9 +205,6 @@ public class NotificationController extends BaseController {
public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request,
@RequestParam(defaultValue = "20") int recipientsPreviewSize,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
NotificationRequestPreview preview = new NotificationRequestPreview();
request.setOriginatorEntityId(user.getId());
NotificationTemplate template;
if (request.getTemplateId() != null) {
template = checkEntityId(request.getTemplateId(), notificationTemplateService::findNotificationTemplateById, Operation.READ);
@ -213,56 +214,70 @@ public class NotificationController extends BaseController {
if (template == null) {
throw new IllegalArgumentException("Template is missing");
}
NotificationProcessingContext tmpProcessingCtx = NotificationProcessingContext.builder()
.tenantId(user.getTenantId())
.request(request)
.template(template)
.settings(null)
.build();
request.setOriginatorEntityId(user.getId());
List<NotificationTarget> targets = request.getTargets().stream()
.map(NotificationTargetId::new)
.map(targetId -> notificationTargetService.findNotificationTargetById(user.getTenantId(), targetId))
.sorted(Comparator.comparing(target -> target.getConfiguration().getType()))
.collect(Collectors.toList());
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = tmpProcessingCtx.getDeliveryMethods().stream()
.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);
NotificationRequestPreview preview = new NotificationRequestPreview();
// generic permission
Set<String> recipientsPreview = new LinkedHashSet<>();
Map<String, Integer> recipientsCountByTarget = new HashMap<>();
List<NotificationTarget> targets = notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(),
request.getTargets().stream().map(NotificationTargetId::new).collect(Collectors.toList()));
Map<String, Integer> recipientsCountByTarget = new LinkedHashMap<>();
Map<NotificationTargetType, NotificationRecipient> firstRecipient = new HashMap<>();
for (NotificationTarget target : targets) {
int recipientsCount;
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(),
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize));
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null,
new SortOrder("createdTime", SortOrder.Direction.DESC)));
recipientsCount = (int) recipients.getTotalElements();
recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList());
} else {
recipientsCount = 1;
recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation());
}
firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null);
for (NotificationRecipient recipient : recipientsPart) {
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 {
break;
}
}
recipientsCountByTarget.put(target.getName(), recipientsCount);
}
preview.setRecipientsPreview(recipientsPreview);
preview.setRecipientsCountByTarget(recipientsCountByTarget);
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;
}

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

@ -71,7 +71,7 @@ public class NotificationTargetController extends BaseController {
private final NotificationTargetService notificationTargetService;
@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)
@PostMapping("/target")
@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.tenant.profile.DefaultTenantProfileConfiguration;
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.exception.InvalidParametersException;
import org.thingsboard.server.exception.UncheckedApiException;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.AttributeData;
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.PostConstruct;
@ -624,7 +623,6 @@ public class TelemetryController extends BaseController {
}
if (json.isObject()) {
List<AttributeKvEntry> attributes = extractRequestAttributes(json);
attributes.forEach(ConstraintValidator::validateFields);
if (attributes.isEmpty()) {
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.TbMsgDataType;
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.queue.notification.NotificationRuleProcessor;
import java.util.List;
import java.util.Map;
@ -53,6 +56,7 @@ import java.util.stream.Collectors;
public class EntityActionService {
private final TbClusterService tbClusterService;
private final AuditLogService auditLogService;
private final NotificationRuleProcessor notificationRuleProcessor;
private static final ObjectMapper json = new ObjectMapper();
@ -87,11 +91,11 @@ public class EntityActionService {
case ALARM_CLEAR:
msgType = DataConstants.ALARM_CLEAR;
break;
case ALARM_ASSIGN:
msgType = DataConstants.ALARM_ASSIGN;
case ALARM_ASSIGNED:
msgType = DataConstants.ALARM_ASSIGNED;
break;
case ALARM_UNASSIGN:
msgType = DataConstants.ALARM_UNASSIGN;
case ALARM_UNASSIGNED:
msgType = DataConstants.ALARM_UNASSIGNED;
break;
case ALARM_DELETE:
msgType = DataConstants.ALARM_DELETE;
@ -233,12 +237,28 @@ public class EntityActionService {
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 (entity instanceof HasTenantId) {
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);
} catch (Exception 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,
ActionType actionType, Exception e, Object... additionalInfo) {
ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) {
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.tenant.profile.TenantProfileConfiguration;
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.queue.ServiceType;
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;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
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.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultRateLimitService implements RateLimitService {
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
public boolean checkRateLimit(TenantId tenantId, LimitedApi api) {
public boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId) {
if (tenantId.isSysTenantId()) {
return true;
}
RateLimitKey key = new RateLimitKey(api, entityId);
String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration()
.map(api::getLimitConfig).orElse(null);
Map<TenantId, TbRateLimits> rateLimits = this.rateLimits.get(api);
if (StringUtils.isEmpty(rateLimitConfig)) {
if (rateLimits != null) {
rateLimits.remove(tenantId);
if (rateLimits.isEmpty()) {
this.rateLimits.remove(api);
}
}
rateLimits.invalidate(key);
return true;
}
log.trace("[{}] Checking rate limit for {} ({})", entityId, api, rateLimitConfig);
if (rateLimits == null) {
rateLimits = new ConcurrentHashMap<>();
this.rateLimits.put(api, rateLimits);
}
TbRateLimits rateLimit = rateLimits.get(tenantId);
if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) {
rateLimit = new TbRateLimits(rateLimitConfig);
rateLimits.put(tenantId, rateLimit);
TbRateLimits rateLimit = rateLimits.asMap().compute(key, (k, limit) -> {
if (limit == null || !limit.getConfiguration().equals(rateLimitConfig)) {
limit = new TbRateLimits(rateLimitConfig);
log.trace("[{}] Created new rate limit bucket for {} ({})", entityId, api, rateLimitConfig);
}
return limit;
});
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_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit),
NOTIFICATION_REQUEST(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit);
NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit),
NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit);
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;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
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);
scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
ObjectNode configurationDescriptor = mapper.createObjectNode();
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
public ListenableFuture<Void> registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs) {
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId);
return Futures.transformAsync(deviceFuture, device -> {
Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE);
List<Object> key = constructCacheKey(device.getId());
if (isAllowedClaimingByDefault) {
if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
persistInCache(secretKey, durationMs, cache, key);
return Futures.immediateFuture(null);
Device device = deviceService.findDeviceById(tenantId, deviceId);
Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE);
List<Object> key = constructCacheKey(device.getId());
if (isAllowedClaimingByDefault) {
if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
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();
} 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("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName());
throw new IllegalArgumentException();
}, MoreExecutors.directExecutor());
}
}, MoreExecutors.directExecutor());
}, MoreExecutors.directExecutor());
}
}
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.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
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.StringUtils;
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.TenantId;
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.StringDataEntry;
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.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
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.audit.AuditLogService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.device.DeviceProfileDao;
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.ProvisionFailedException;
@ -59,12 +61,13 @@ 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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@ -77,35 +80,56 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
private static final String DEVICE_PROVISION_STATE = "provisionState";
private static final String PROVISIONED_STATE = "provisioned";
@Autowired
TbClusterService clusterService;
private final 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
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) {
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider, TbClusterService clusterService, DeviceProfileService deviceProfileService, DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, AttributesService attributesService, AuditLogService auditLogService, PartitionService partitionService) {
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
@ -124,14 +148,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey);
DeviceProfile targetProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequestKey);
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
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()) {
case ALLOW_CREATE_NEW_DEVICES:
@ -155,6 +179,8 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
}
}
break;
case X509_CERTIFICATE_CHAIN:
throw new ProvisionFailedException("Invalid provision strategy type!");
}
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) {
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
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;
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;
@Value("${edges.state.persistToTelemetry:false}")
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;
@Value("${edges.rpc.max_inbound_message_size:4194304}")
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}")
private int schedulerPoolSize;
@ -131,6 +134,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.info("Initializing Edge RPC service!");
NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort)
.permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS)
.keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS)
.keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS)
.permitKeepAliveWithoutCalls(true)
.maxInboundMessageSize(maxInboundMessageSize)
.addService(this);
if (sslEnabled) {
@ -282,7 +288,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
if (session != null) {
boolean success = false;
if (session.isConnected()) {
session.startSyncProcess(tenantId, edgeId, true);
session.startSyncProcess(true);
success = true;
}
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()) {
fullSync = requestMsg.getSyncRequestMsg().getFullSync();
}
startSyncProcess(edge.getTenantId(), edge.getId(), fullSync);
startSyncProcess(fullSync);
} else {
syncCompleted = true;
}
@ -192,10 +192,10 @@ public final class EdgeGrpcSession implements Closeable {
};
}
public void startSyncProcess(TenantId tenantId, EdgeId edgeId, boolean fullSync) {
log.trace("[{}][{}] Staring edge sync process", tenantId, edgeId);
public void startSyncProcess(boolean fullSync) {
log.trace("[{}][{}][{}] Staring edge sync process", edge.getTenantId(), edge.getId(), this.sessionId);
syncCompleted = false;
interruptGeneralProcessingOnSync(tenantId, edgeId);
interruptGeneralProcessingOnSync();
doSync(new EdgeSyncCursor(ctx, edge, fullSync));
}
@ -221,9 +221,9 @@ public final class EdgeGrpcSession implements Closeable {
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build())
.build();
Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<Void>() {
Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() {
@Override
public void onSuccess(Void result) {
public void onSuccess(Boolean isInterrupted) {
syncCompleted = true;
ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId());
}
@ -272,7 +272,7 @@ public final class EdgeGrpcSession implements Closeable {
}
if (sessionState.getPendingMsgsMap().isEmpty()) {
log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey());
stopCurrentSendDownlinkMsgsTask(null);
stopCurrentSendDownlinkMsgsTask(false);
}
} catch (Exception 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()) {
log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData());
Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<Void>() {
Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Void tmp) {
if (isConnected() && pageData.hasNext()) {
processEdgeEvents(fetcher, pageLink.nextPageLink(), result);
public void onSuccess(@Nullable Boolean isInterrupted) {
if (Boolean.TRUE.equals(isInterrupted)) {
log.debug("[{}][{}][{}] Send downlink messages task was interrupted", edge.getTenantId(), edge.getId(), sessionId);
result.set(null);
} else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId();
result.set(ifOffset);
if (isConnected() && pageData.hasNext()) {
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();
sessionState.setSendDownlinkMsgsFuture(SettableFuture.create());
@ -433,14 +438,14 @@ public final class EdgeGrpcSession implements Closeable {
} else {
log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}",
this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy);
stopCurrentSendDownlinkMsgsTask(null);
stopCurrentSendDownlinkMsgsTask(false);
}
} else {
stopCurrentSendDownlinkMsgsTask(null);
stopCurrentSendDownlinkMsgsTask(false);
}
} catch (Exception 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() {
String msg = String.format("[%s] Previous send downlink future was not properly completed, stopping it now!", this.sessionId);
stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg));
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(true);
}
private void interruptGeneralProcessingOnSync(TenantId tenantId, EdgeId edgeId) {
String msg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId);
stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg));
private void interruptGeneralProcessingOnSync() {
log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(true);
}
public void stopCurrentSendDownlinkMsgsTask(Exception e) {
public void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) {
if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) {
if (e != null) {
log.debug(e.getMessage());
sessionState.getSendDownlinkMsgsFuture().setException(e);
} else {
sessionState.getSendDownlinkMsgsFuture().set(null);
}
sessionState.getSendDownlinkMsgsFuture().set(isInterrupted);
}
if (sessionState.getScheduledSendDownlinkTask() != null) {
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 {
private final Map<Integer, DownlinkMsg> pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>());
private SettableFuture<Void> sendDownlinkMsgsFuture;
private SettableFuture<Boolean> sendDownlinkMsgsFuture;
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;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
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) {
log.debug("[{}] Executing processDeviceCredentialsMsg, deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentialsUpdateMsg);
DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB()));
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId);
return Futures.transform(deviceFuture, device -> {
return dbCallbackExecutorService.submit(() -> {
Device device = deviceService.findDeviceById(tenantId, deviceId);
if (device != null) {
log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]",
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);
}
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;
case ALARM_CLEAR:
return EdgeEventActionType.ALARM_CLEAR;
case ALARM_ASSIGN:
return EdgeEventActionType.ALARM_ASSIGN;
case ALARM_UNASSIGN:
return EdgeEventActionType.ALARM_UNASSIGN;
case ALARM_ASSIGNED:
return EdgeEventActionType.ALARM_ASSIGNED;
case ALARM_UNASSIGNED:
return EdgeEventActionType.ALARM_UNASSIGNED;
case DELETED:
return EdgeEventActionType.DELETED;
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) {
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 {
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) {
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 {
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 ...");
clearCacheByName("repositorySettings");
break;
case "3.4.4":
log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0");
clearCacheByName("deviceProfiles");
default:
//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();
}
@Override
public boolean isConfigured(TenantId tenantId) {
return mailSender != null;
}
private String toEnabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) {
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
public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest request, Consumer<NotificationRequestStats> callback) {
if (!rateLimitService.checkRateLimit(tenantId, LimitedApi.NOTIFICATION_REQUEST)) {
throw new TbRateLimitsException(EntityType.TENANT);
if (request.getRuleId() == null) {
if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS, tenantId)) {
throw new TbRateLimitsException(EntityType.TENANT);
}
}
NotificationTemplate notificationTemplate;
@ -119,21 +121,33 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
}
if (notificationTemplate == null) throw new IllegalArgumentException("Template is missing");
Set<NotificationDeliveryMethod> deliveryMethods = new HashSet<>();
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();
notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> {
if (!template.isEnabled()) return;
if (!channels.get(deliveryMethod).check(tenantId)) {
throw new IllegalArgumentException("Unable to send notification via " + deliveryMethod.getName() + ": not configured or not working");
try {
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 (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) {
NotificationRequestConfig config = request.getAdditionalConfig();
@ -153,6 +167,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(tenantId)
.request(request)
.deliveryMethods(deliveryMethods)
.template(notificationTemplate)
.settings(settings)
.build();
@ -341,14 +356,20 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
@Override
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(TenantId tenantId) {
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)
.collect(Collectors.toSet());
}
@Override
public boolean check(TenantId tenantId) {
return true;
public void check(TenantId tenantId) throws Exception {
}
@Override

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

@ -48,20 +48,21 @@ public class NotificationProcessingContext {
private final NotificationSettings settings;
@Getter
private final NotificationRequest request;
@Getter
private final Set<NotificationDeliveryMethod> deliveryMethods;
@Getter
private final NotificationTemplate notificationTemplate;
private final Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates;
@Getter
private Set<NotificationDeliveryMethod> deliveryMethods;
@Getter
private final NotificationRequestStats stats;
@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.request = request;
this.deliveryMethods = deliveryMethods;
this.settings = settings;
this.notificationTemplate = template;
this.templates = new EnumMap<>(NotificationDeliveryMethod.class);
@ -77,7 +78,6 @@ public class NotificationProcessingContext {
templates.put(deliveryMethod, template);
}
});
deliveryMethods = templates.keySet();
}
public <C extends NotificationDeliveryMethodConfig> C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) {
@ -128,6 +128,7 @@ public class NotificationProcessingContext {
private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) {
return Map.of(
"recipientTitle", recipient.getTitle(),
"recipientEmail", Strings.nullToEmpty(recipient.getEmail()),
"recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()),
"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
public boolean check(TenantId tenantId) {
try {
mailService.testConnection(tenantId);
return true;
} catch (Exception e) {
return false;
public void check(TenantId tenantId) throws Exception {
if (!mailService.isConfigured(tenantId)) {
throw new RuntimeException("Mail server is not configured");
}
}

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);
boolean check(TenantId tenantId);
void check(TenantId tenantId) throws Exception;
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.notification.NotificationDeliveryMethod;
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.targets.slack.SlackConversation;
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.notification.NotificationProcessingContext;
@Component
@RequiredArgsConstructor
@ -47,9 +47,11 @@ public class SlackNotificationChannel implements NotificationChannel<SlackConver
}
@Override
public boolean check(TenantId tenantId) {
public void check(TenantId tenantId) throws Exception {
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

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
public boolean check(TenantId tenantId) {
return smsService.isConfigured(tenantId);
public void check(TenantId tenantId) throws Exception {
if (!smsService.isConfigured(tenantId)) {
throw new RuntimeException("SMS provider is not configured");
}
}
@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.NotificationRuleTriggerType;
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.RuleEngineMsgTrigger;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
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.service.apiusage.limits.LimitedApi;
import org.thingsboard.server.service.apiusage.limits.RateLimitService;
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.RuleEngineMsgNotificationRuleTriggerProcessor;
@ -65,6 +67,7 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
private final NotificationRulesCache notificationRulesCache;
private final NotificationRequestService notificationRequestService;
private final PartitionService partitionService;
private final RateLimitService rateLimitService;
@Autowired @Lazy
private NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor;
@ -119,6 +122,11 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
}
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);
rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> {
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 static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> {
@Override
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)) {
return false;
}
Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) &&
return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(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);
AlarmAssignee assignee = alarmInfo.getAssignee();
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)
.assigneeLastName(assignee != null ? assignee.getLastName() : null)
.assigneeEmail(assignee != null ? assignee.getEmail() : null)
@ -78,7 +79,7 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio
@Override
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 static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> {
@ -53,8 +54,8 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
}
}
Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) &&
return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
}
@ -66,9 +67,9 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
return AlarmCommentNotificationInfo.builder()
.comment(comment.getComment().get("text").asText())
.action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated")
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail"))
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName"))
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName"))
.userEmail(msg.getMetaData().getValue("userEmail"))
.userFirstName(msg.getMetaData().getValue("userFirstName"))
.userLastName(msg.getMetaData().getValue("userLastName"))
.alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType())
.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.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.AlarmStatusFilter;
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.ClearRule;
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 static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> {
@ -85,11 +85,11 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
}
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) {
return isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType());
return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType());
}
@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.dao.tenant.TenantService;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
@RequiredArgsConstructor
@ -34,8 +34,8 @@ public class ApiUsageLimitTriggerProcessor implements NotificationRuleTriggerPro
@Override
public boolean matchesFilter(ApiUsageLimitTrigger trigger, ApiUsageLimitNotificationRuleTriggerConfig triggerConfig) {
return (isEmpty(triggerConfig.getApiFeatures()) || triggerConfig.getApiFeatures().contains(trigger.getState().getApiFeature())) &&
(isEmpty(triggerConfig.getNotifyOn()) || triggerConfig.getNotifyOn().contains(trigger.getStatus()));
return emptyOrContains(triggerConfig.getApiFeatures(), trigger.getState().getApiFeature()) &&
emptyOrContains(triggerConfig.getNotifyOn(), trigger.getStatus());
}
@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.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
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.dao.entity.EntityCountService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
@ -30,6 +33,8 @@ import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
@RequiredArgsConstructor
public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor<EntitiesLimitTrigger, EntitiesLimitNotificationRuleTriggerConfig> {
private final EntityCountService entityCountService;
private final TbTenantProfileCache tenantProfileCache;
private final TenantService tenantService;
@Override
@ -37,7 +42,15 @@ public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerPro
if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) {
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

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;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasCustomerId;
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.RuleOriginatedNotificationInfo;
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.msg.TbMsg;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<EntityActionNotificationRuleTriggerConfig> {
public class EntityActionTriggerProcessor implements NotificationRuleTriggerProcessor<EntityActionTrigger, EntityActionNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) {
String msgType = trigger.getMsg().getType();
if (msgType.equals(DataConstants.ENTITY_CREATED)) {
if (!triggerConfig.isCreated()) {
return false;
}
} 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()));
public boolean matchesFilter(EntityActionTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) {
return ((trigger.getActionType() == ActionType.ADDED && triggerConfig.isCreated())
|| (trigger.getActionType() == ActionType.UPDATED && triggerConfig.isUpdated())
|| (trigger.getActionType() == ActionType.DELETED && triggerConfig.isDeleted()))
&& emptyOrContains(triggerConfig.getEntityTypes(), trigger.getEntityId().getEntityType());
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger 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;
public RuleOriginatedNotificationInfo constructNotificationInfo(EntityActionTrigger trigger) {
return EntityActionNotificationInfo.builder()
.entityId(msg.getOriginator())
.entityName(msg.getMetaData().getValue("entityName"))
.actionType(actionType)
.userId(UUID.fromString(msg.getMetaData().getValue("userId")))
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail"))
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName"))
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName"))
.entityCustomerId(msg.getCustomerId())
.entityId(trigger.getEntityId())
.entityName(trigger.getEntity().getName())
.actionType(trigger.getActionType())
.userId(trigger.getUser().getUuidId())
.userTitle(trigger.getUser().getTitle())
.userEmail(trigger.getUser().getEmail())
.userFirstName(trigger.getUser().getFirstName())
.userLastName(trigger.getUser().getLastName())
.entityCustomerId(trigger.getEntity() instanceof HasCustomerId ?
((HasCustomerId) trigger.getEntity()).getCustomerId() :
trigger.getUser().getCustomerId())
.build();
}
private static EntityType getEntityType(TbMsg msg) {
return Optional.ofNullable(msg.getMetaData().getValue("entityType"))
.map(EntityType::valueOf).orElse(null);
}
@Override
public NotificationRuleTriggerType getTriggerType() {
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.msg.MsgType;
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.queue.ServiceType;
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 org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
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.exception.TenantNotFoundException;
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.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
@Slf4j
@Service
@ -46,7 +46,7 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService
if (tenantProfile != null) {
return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbRuleEngine());
} 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.edge.EdgeService;
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.ota.OtaPackageService;
import org.thingsboard.server.dao.resource.ResourceService;
@ -263,8 +264,7 @@ public class AccessValidator {
if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
} else {
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(currentUser.getTenantId(), new DeviceId(entityId.getId()));
Futures.addCallback(deviceFuture, getCallback(callback, device -> {
Futures.addCallback(Futures.immediateFuture(deviceService.findDeviceById(currentUser.getTenantId(), new DeviceId(entityId.getId()))), getCallback(callback, device -> {
if (device == null) {
return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND);
} else {
@ -575,7 +575,7 @@ public class AccessValidator {
ResponseEntity responseEntity;
if (e instanceof 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);
} else {
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.model.ConversationType;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.id.TenantId;
@ -44,8 +43,6 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@Service
@RequiredArgsConstructor
public class DefaultSlackService implements SlackService {
@ -81,15 +78,11 @@ public class DefaultSlackService implements SlackService {
.filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot())
.map(user -> {
SlackConversation conversation = new SlackConversation();
conversation.setType(conversationType);
conversation.setId(user.getId());
conversation.setShortName(user.getName());
conversation.setName(user.getName());
conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName());
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;
})
.collect(Collectors.toList());
@ -107,10 +100,10 @@ public class DefaultSlackService implements SlackService {
.filter(channel -> !channel.isArchived())
.map(channel -> {
SlackConversation conversation = new SlackConversation();
conversation.setType(conversationType);
conversation.setId(channel.getId());
conversation.setShortName(channel.getName());
conversation.setName(channel.getName());
conversation.setWholeName(channel.getNameNormalized());
conversation.setTitle("#" + channel.getName());
return conversation;
})
.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
public String getToken(TenantId 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.EntityType;
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.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -453,6 +454,9 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
updateInactivityStateIfExpired(ts, deviceId, stateData);
} catch (Exception e) {
log.warn("[{}] Failed to update inactivity state", deviceId, e);
if (e instanceof TenantNotFoundException) {
continue;
}
}
Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(),
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
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);
}
@ -85,7 +85,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
@Override
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);
}
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);
E entity = importedEntityInfo.getEntity();
saveKvs(user, entity, entityData.getKvs());
if (request.getMapping().getUpdate() || !importedEntityInfo.isUpdated()) {
saveKvs(user, entity, entityData.getKvs());
}
return 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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
@ -87,7 +86,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
@Autowired
protected TbNotificationEntityService entityNotificationService;
@Transactional(rollbackFor = Exception.class)
@Override
public EntityImportResult<E> importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException {
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();
for (EntityType entityType : EntityType.values()) {
Optional<EntityId> externalIdOpt = buildEntityId(entityType, externalUuid);
if (!externalIdOpt.isPresent()) {
Optional<EntityId> externalId = buildEntityId(entityType, externalUuid);
if (externalId.isEmpty()) {
continue;
}
EntityId internalId = ctx.getInternalId(externalIdOpt.get());
EntityId internalId = ctx.getInternalId(externalId.get());
if (internalId != null) {
return Optional.of(internalId);
}
}
if (fetchAllUUIDs) {
for (EntityType entityType : hints) {
Optional<EntityId> internalId = lookupInDb(externalUuid, entityType);
if (internalId.isPresent()) return internalId;
}
Set<EntityType> processLast = Set.of(EntityType.TENANT);
List<EntityType> entityTypes = new ArrayList<>(hints);
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;
}
Optional<EntityId> internalId = lookupInDb(externalUuid, entityType);
if (internalId.isPresent()) return internalId;
EntityId internalId = getInternalId(externalId.get(), false);
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();
}
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) {
try {
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
protected Dashboard prepare(EntitiesImportCtx ctx, Dashboard dashboard, Dashboard old, EntityExportData<Dashboard> exportData, IdProvider idProvider) {
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()) {
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;
}

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.page.PageData;
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.EntityExportSettings;
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.VersionLoadConfig;
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.edge.EdgeService;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
@ -259,7 +259,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
return gitServiceQueue.listEntitiesAtVersion(tenantId, versionId);
}
@SuppressWarnings({"UnstableApiUsage", "rawtypes"})
@SuppressWarnings({"rawtypes"})
@Override
public UUID loadEntitiesVersion(User user, VersionLoadRequest request) throws Exception {
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<>())
.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;
} 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;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ProtocolStringList;
import lombok.RequiredArgsConstructor;
@ -23,10 +22,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
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.ApiUsageState;
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.SystemInfoData;
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.stats.TbApiUsageStateClient;
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.gen.transport.TransportProtos.ServiceInfo;
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.TimeUnit;
import static org.thingsboard.common.util.SystemUtil.getCpuCount;
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.getCpuCount;
import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace;
import static org.thingsboard.common.util.SystemUtil.getTotalMemory;
@ -90,6 +89,8 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private final TbApiUsageStateClient apiUsageStateClient;
private final AdminSettingsService adminSettingsService;
private final OAuth2Service oAuth2Service;
private final MailService mailService;
private final SmsService smsService;
private volatile ScheduledExecutorService scheduler;
@Override
@ -135,18 +136,37 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
public FeaturesInfo getFeaturesInfo() {
FeaturesInfo featuresInfo = new FeaturesInfo();
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.setTwoFaEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings") != null);
featuresInfo.setNotificationEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications") != null);
featuresInfo.setTwoFaEnabled(isTwoFaEnabled());
featuresInfo.setNotificationEnabled(isSlackEnabled());
return featuresInfo;
}
private boolean isEmailEnabled() {
AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (mailSettings != null) {
JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom");
return StringUtils.isNotEmpty(mailFrom.asText()) && !"ThingsBoard <sysadmin@localhost.localdomain>".equalsIgnoreCase(mailFrom.asText());
try {
mailService.testConnection(TenantId.SYS_TENANT_ID);
return true;
} 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;
}

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.AlarmModificationRequest;
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.AlarmSeverity;
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.dao.alarm.AlarmOperationResult;
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.entitiy.alarm.TbAlarmCommentService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
@ -189,6 +190,16 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
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
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String 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.Device;
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.EntityType;
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.TbMsgMetaData;
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.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.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
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.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
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 Pattern X509_CERTIFICATE_TRIM_CHAIN_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----");
private final TbDeviceProfileCache deviceProfileCache;
private final TbTenantProfileCache tenantProfileCache;
private final TbApiUsageStateService apiUsageStateService;
private final DeviceService deviceService;
private final DeviceProfileService deviceProfileService;
private final RelationService relationService;
private final DeviceCredentialsService deviceCredentialsService;
private final DbCallbackExecutorService dbCallbackExecutorService;
@ -159,6 +166,9 @@ public class DefaultTransportApiService implements TransportApiService {
} else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE);
} else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) {
TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg();
result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain());
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg());
} 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) {
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
if (credentials != null) {
@ -665,4 +704,11 @@ public class DefaultTransportApiService implements TransportApiService {
private Long checkLong(Long l) {
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.server.common.data.UpdateMessage;
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.TbCoreComponent;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
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/}"
githubMapper:
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:
@ -479,10 +482,14 @@ cache:
entityCount:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}"
maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}"
# deliberately placed outside 'specs' group above
notificationRules:
timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_RULES_TTL:30}"
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.
spring.data.redis.repositories.enabled: false
@ -949,7 +956,9 @@ edges:
enabled: "${EDGES_ENABLED:true}"
rpc:
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:
# Enable/disable SSL support
enabled: "${EDGES_RPC_SSL_ENABLED:false}"
@ -1009,6 +1018,8 @@ queue:
max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}"
max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}"
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}"
confluent:
ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}"
@ -1027,11 +1038,12 @@ queue:
# tb_rule_engine.sq:
# - key: max.poll.records
# 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
- key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms
value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 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)
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"
other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside
# - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms
# value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 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:
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}"

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.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
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);
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.setAssigneeId(null);
@ -192,7 +191,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
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
@ -414,7 +413,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
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
@ -445,7 +444,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
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();
@ -462,7 +461,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
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
@ -480,7 +479,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
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();
Thread.sleep(2);
@ -491,7 +490,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
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
@ -509,7 +508,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
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();
loginCustomerUser();
@ -525,7 +524,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
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

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.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
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.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
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.SaveDeviceWithCredentialsRequest;
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.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@ -1392,6 +1397,73 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
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) {
Device device = new Device();
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));
}
@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
public void testChangeDeviceProfileTypeNull() throws Exception {
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 org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
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.ApiUsageState;
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.tenant.profile.DefaultTenantProfileConfiguration;
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.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
@ -76,6 +81,15 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
@Autowired
private TbTenantProfileCache tenantProfileCache;
@Autowired
private AdminSettingsService adminSettingsService;
@MockBean
private MailService mailService;
@MockBean
private SmsService smsService;
//For system administrator
@Test
public void testTenantsCountWsCmd() throws Exception {
@ -266,6 +280,19 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
@Test
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();
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);
JsonNode jsonValue = mailSettings.getJsonValue();
((ObjectNode) jsonValue).put("mailFrom", "test@thingsboard.org");
((ObjectNode) jsonValue).put("mailFrom", mail);
mailSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", mailSettings).andExpect(status().isOk());
@ -305,7 +332,12 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
AdminSettings twoFaSettingsSettings = new AdminSettings();
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());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
@ -317,7 +349,13 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
AdminSettings notificationsSettings = new AdminSettings();
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());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
@ -338,6 +376,10 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
Assert.assertTrue(featuresInfo.isTwoFaEnabled());
Assert.assertTrue(featuresInfo.isNotificationEnabled());
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

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
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.template;
package org.thingsboard.server.controller.sql;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NotificationText {
private String body;
private String subject;
import org.thingsboard.server.controller.BaseTelemetryControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
@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("timeoutSecs", 3);
updateEdgeClientFields("keepAliveTimeSec", 300);
updateEdgeClientFields("keepAliveTimeoutSec", 5);
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.NotificationRequestStats;
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.targets.NotificationTarget;
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 java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -195,6 +200,32 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
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
protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
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.thingsboard.server.common.data.id.TenantId;
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;
@Service
@Primary
public class MockNotificationSettingsService extends DefaultNotificationSettingsService {
public MockNotificationSettingsService(AdminSettingsService adminSettingsService, NotificationTargetService notificationTargetService, NotificationTemplateService notificationTemplateService, NotificationRuleService notificationRuleService) {
super(adminSettingsService, notificationTargetService, notificationTemplateService, notificationRuleService);
public MockNotificationSettingsService(AdminSettingsService adminSettingsService) {
super(adminSettingsService, null, null);
}
@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.UserListFilter;
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.template.DeliveryMethodNotificationTemplate;
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.WebDeliveryMethodNotificationTemplate;
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.service.DaoSqlTest;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
@ -226,13 +226,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText, 5);
assertThat(notificationRequest.getStatus()).isEqualTo(NotificationRequestStatus.SCHEDULED);
await().atLeast(4, TimeUnit.SECONDS)
.atMost(6, TimeUnit.SECONDS)
.atMost(15, TimeUnit.SECONDS)
.until(() -> wsClient.getLastMsg() != null);
Notification delayedNotification = wsClient.getLastDataUpdate().getUpdate();
assertThat(delayedNotification).extracting(Notification::getText).isEqualTo(notificationText);
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);
}
@ -324,16 +324,17 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test
public void testNotificationRequestPreview() throws Exception {
NotificationTarget target1 = new NotificationTarget();
target1.setName("Me");
PlatformUsersNotificationTargetConfig target1Config = new PlatformUsersNotificationTargetConfig();
NotificationTarget tenantAdminTarget = new NotificationTarget();
tenantAdminTarget.setName("Me");
PlatformUsersNotificationTargetConfig tenantAdminTargetConfig = new PlatformUsersNotificationTargetConfig();
UserListFilter userListFilter = new UserListFilter();
userListFilter.setUsersIds(DaoUtil.toUUIDs(List.of(tenantAdminUserId)));
target1Config.setUsersFilter(userListFilter);
target1.setConfiguration(target1Config);
target1 = saveNotificationTarget(target1);
userListFilter.setUsersIds(List.of(tenantAdminUserId.getId()));
tenantAdminTargetConfig.setUsersFilter(userListFilter);
tenantAdminTarget.setConfiguration(tenantAdminTargetConfig);
tenantAdminTarget = saveNotificationTarget(tenantAdminTarget);
List<String> recipients = new ArrayList<>();
recipients.add(TENANT_ADMIN_EMAIL);
String firstRecipientEmail = TENANT_ADMIN_EMAIL;
createDifferentCustomer();
loginTenantAdmin();
@ -347,21 +348,33 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
customerUser = createUser(customerUser, "12345678");
recipients.add(customerUser.getEmail());
}
NotificationTarget target2 = new NotificationTarget();
target2.setName("Other customer users");
PlatformUsersNotificationTargetConfig target2Config = new PlatformUsersNotificationTargetConfig();
NotificationTarget customerUsersTarget = new NotificationTarget();
customerUsersTarget.setName("Other customer users");
PlatformUsersNotificationTargetConfig customerUsersTargetConfig = new PlatformUsersNotificationTargetConfig();
CustomerUsersFilter customerUsersFilter = new CustomerUsersFilter();
customerUsersFilter.setCustomerId(differentCustomerId.getId());
target2Config.setUsersFilter(customerUsersFilter);
target2.setConfiguration(target2Config);
target2 = saveNotificationTarget(target2);
customerUsersTargetConfig.setUsersFilter(customerUsersFilter);
customerUsersTarget.setConfiguration(customerUsersTargetConfig);
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.setNotificationType(NotificationType.GENERAL);
notificationTemplate.setName("Test template");
String requestorEmail = TENANT_ADMIN_EMAIL;
NotificationTemplateConfig templateConfig = new NotificationTemplateConfig();
HashMap<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates = new HashMap<>();
templateConfig.setDeliveryMethodsTemplates(templates);
@ -369,69 +382,59 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
WebDeliveryMethodNotificationTemplate webNotificationTemplate = new WebDeliveryMethodNotificationTemplate();
webNotificationTemplate.setEnabled(true);
webNotificationTemplate.setBody("Message for WEB: ${recipientEmail} ${unknownParam}");
webNotificationTemplate.setSubject("Subject for WEB: ${recipientEmail}");
webNotificationTemplate.setSubject("WEB SUBJECT: ${recipientEmail}");
webNotificationTemplate.setBody("WEB: ${recipientEmail} ${unknownParam}");
templates.put(NotificationDeliveryMethod.WEB, webNotificationTemplate);
SmsDeliveryMethodNotificationTemplate smsNotificationTemplate = new SmsDeliveryMethodNotificationTemplate();
smsNotificationTemplate.setEnabled(true);
smsNotificationTemplate.setBody("Message for SMS: ${recipientEmail}");
smsNotificationTemplate.setBody("SMS: ${recipientEmail}");
templates.put(NotificationDeliveryMethod.SMS, smsNotificationTemplate);
EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
emailNotificationTemplate.setEnabled(true);
emailNotificationTemplate.setSubject("Subject for EMAIL: ${recipientEmail}");
emailNotificationTemplate.setBody("Message for EMAIL: ${recipientEmail}");
emailNotificationTemplate.setSubject("EMAIL SUBJECT: ${recipientEmail}");
emailNotificationTemplate.setBody("EMAIL: ${recipientEmail}");
templates.put(NotificationDeliveryMethod.EMAIL, emailNotificationTemplate);
SlackDeliveryMethodNotificationTemplate slackNotificationTemplate = new SlackDeliveryMethodNotificationTemplate();
slackNotificationTemplate.setEnabled(true);
slackNotificationTemplate.setBody("Message for SLACK: ${recipientEmail}");
slackNotificationTemplate.setBody("SLACK: ${recipientFirstName} ${recipientLastName}");
templates.put(NotificationDeliveryMethod.SLACK, slackNotificationTemplate);
notificationTemplate = saveNotificationTemplate(notificationTemplate);
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.setAdditionalConfig(new NotificationRequestConfig());
NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class);
assertThat(preview.getRecipientsCountByTarget().get(target1.getName())).isEqualTo(1);
assertThat(preview.getRecipientsCountByTarget().get(target2.getName())).isEqualTo(customerUsersCount);
assertThat(preview.getTotalRecipientsCount()).isEqualTo(1 + customerUsersCount);
assertThat(preview.getRecipientsCountByTarget().get(tenantAdminTarget.getName())).isEqualTo(1);
assertThat(preview.getRecipientsCountByTarget().get(customerUsersTarget.getName())).isEqualTo(customerUsersCount);
assertThat(preview.getRecipientsCountByTarget().get(slackTarget.getName())).isEqualTo(1);
assertThat(preview.getTotalRecipientsCount()).isEqualTo(2 + customerUsersCount);
assertThat(preview.getRecipientsPreview()).containsAll(recipients);
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = preview.getProcessedTemplates();
assertThat(processedTemplates.get(NotificationDeliveryMethod.WEB)).asInstanceOf(type(WebDeliveryMethodNotificationTemplate.class))
.satisfies(template -> {
assertThat(template.getBody())
.startsWith("Message for WEB")
.endsWith(requestorEmail + " ${unknownParam}");
assertThat(template.getSubject())
.startsWith("Subject for WEB")
.endsWith(requestorEmail);
assertThat(template.getSubject()).isEqualTo("WEB SUBJECT: " + firstRecipientEmail);
assertThat(template.getBody()).isEqualTo("WEB: " + firstRecipientEmail + " ${unknownParam}");
});
assertThat(processedTemplates.get(NotificationDeliveryMethod.SMS)).asInstanceOf(type(SmsDeliveryMethodNotificationTemplate.class))
.satisfies(template -> {
assertThat(template.getBody())
.startsWith("Message for SMS")
.endsWith(requestorEmail);
assertThat(template.getBody()).isEqualTo("SMS: " + firstRecipientEmail);
});
assertThat(processedTemplates.get(NotificationDeliveryMethod.EMAIL)).asInstanceOf(type(EmailDeliveryMethodNotificationTemplate.class))
.satisfies(template -> {
assertThat(template.getBody())
.startsWith("Message for EMAIL")
.endsWith(requestorEmail);
assertThat(template.getSubject())
.startsWith("Subject for EMAIL")
.endsWith(requestorEmail);
assertThat(template.getSubject()).isEqualTo("EMAIL SUBJECT: " + firstRecipientEmail);
assertThat(template.getBody()).isEqualTo("EMAIL: " + firstRecipientEmail);
});
assertThat(processedTemplates.get(NotificationDeliveryMethod.SLACK)).asInstanceOf(type(SlackDeliveryMethodNotificationTemplate.class))
.satisfies(template -> {
assertThat(template.getBody())
.isEqualTo("Message for SLACK: ${recipientEmail}"); // ${recipientEmail} should not be processed
assertThat(template.getBody()).isEqualTo("SLACK: John Doe");
});
}
@ -498,8 +501,9 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
notificationTarget.setName(conversationName + " in Slack");
SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig();
targetConfig.setConversation(SlackConversation.builder()
.type(SlackConversationType.DIRECT)
.id(conversationId)
.title(conversationName)
.name(conversationName)
.build());
notificationTarget.setConfiguration(targetConfig);
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;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import org.junit.Before;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair;
import org.springframework.test.context.TestPropertySource;
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.Device;
import org.thingsboard.server.common.data.DeviceProfile;
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.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
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.AlarmConditionFilter;
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.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
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.trigger.AlarmNotificationRuleTriggerConfig;
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.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.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData;
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.EntityKeyValueType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.rule.RuleChain;
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.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.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.rule.RuleChainService;
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 java.util.ArrayList;
@ -87,7 +86,10 @@ import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.Callable;
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.offset;
@ -96,9 +98,6 @@ import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"js.evaluator=local"
})
public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@SpyBean
@ -106,72 +105,48 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationRequestService notificationRequestService;
@Autowired
private NotificationRuleService notificationRuleService;
private TenantProfileService tenantProfileService;
@Autowired
private NotificationTemplateService notificationTemplateService;
@SpyBean
private AlarmService alarmService;
private TbTenantProfileService tbTenantProfileService;
@Autowired
private RateLimitService rateLimitService;
@Autowired
private RuleChainService ruleChainService;
@Before
public void beforeEach() throws Exception {
loginTenantAdmin();
notificationRuleService.deleteNotificationRulesByTenantId(tenantId);
notificationTemplateService.deleteNotificationTemplatesByTenantId(tenantId);
}
@Test
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();
triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE));
triggerConfig.setCreated(true);
triggerConfig.setUpdated(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();
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId()));
notificationRule.setTriggerConfig(triggerConfig);
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);
checkNotificationAfter(() -> {
device.setName("Updated name");
doPost("/api/device", device, Device.class);
}, notification -> {
assertThat(notification.getSubject()).isEqualTo("updated: Device [" + device.getId() + "]");
});
notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]");
checkNotificationAfter(() -> {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
}, notification -> {
assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]");
});
}
@Test
@ -208,7 +183,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
String alarmType = "myBoolIsTrue";
DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType);
DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType);
Device device = createDevice("Device 1", deviceProfile.getName(), "1234");
clients.values().forEach(wsClient -> {
@ -230,7 +205,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
clients.forEach((expectedDelay, wsClient) -> {
Notification notification = wsClient.getLastDataUpdate().getUpdate();
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 + ", " +
"severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId());
@ -272,7 +247,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM);
String alarmType = "myBoolIsTrue";
DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType);
DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType);
Device device = createDevice("Device 1", deviceProfile.getName(), "1234");
AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig();
@ -327,6 +302,79 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
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
public void testNotificationRuleInfo() throws Exception {
NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL};
@ -352,7 +400,80 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
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.setTenantId(tenantId);
@ -387,52 +508,4 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
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.thingsboard.server.common.data.User;
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.platform.AllUsersFilter;
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.page.PageData;
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.service.DaoSqlTest;
@ -41,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class NotificationTargetApiTest extends AbstractControllerTest {
public class NotificationTargetApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationTargetDao notificationTargetDao;
@ -148,6 +149,26 @@ public class NotificationTargetApiTest extends AbstractControllerTest {
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 {
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.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
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.page.PageData;
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 java.util.List;
@ -43,16 +40,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest
public class NotificationTemplateApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationTemplateService templateService;
@Autowired
private NotificationRuleService notificationRuleService;
@Before
public void beforeEach() throws Exception {
loginTenantAdmin();
notificationRuleService.deleteNotificationRulesByTenantId(tenantId);
templateService.deleteNotificationTemplatesByTenantId(tenantId);
}
@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.timeseries.TimeseriesService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.ArrayList;
import java.util.List;
@ -35,17 +36,14 @@ import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"usage.stats.report.enabled=true",
"transport.http.enabled=true",
"usage.stats.report.interval=2",
"usage.stats.gauge_report_interval=1",
"state.defaultStateCheckIntervalInSec=3",
"state.defaultInactivityTimeoutInSec=10"
})
public class DevicesStatisticsTest extends AbstractControllerTest {
@ -53,6 +51,8 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
private TbApiUsageStateService apiUsageStateService;
@Autowired
private TimeseriesService timeseriesService;
@Autowired
private DeviceStateService deviceStateService;
private ApiUsageStateId apiUsageStateId;
@ -86,7 +86,7 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
});
for (Device device : activeDevices) {
postTelemetry(device.getName(), "{\"dp\":1}");
deviceStateService.onDeviceActivity(tenantId, device.getId(), System.currentTimeMillis());
}
await().atMost(40, TimeUnit.SECONDS)
@ -102,9 +102,4 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
.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.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -254,21 +253,25 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2");
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" +
"\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" +
"\t\t\"alias\": \"assets\",\n" +
"\t\t\"filter\": {\n" +
"\t\t\t\"entityList\": [\n" +
"\t\t\t\t\"" + asset1.getId().toString() + "\",\n" +
"\t\t\t\t\"" + asset2.getId().toString() + "\"\n" +
"\t\t\t],\n" +
"\t\t\t\"entityType\": \"ASSET\",\n" +
"\t\t\t\"resolveMultiple\": true,\n" +
"\t\t\t\"type\": \"entityList\"\n" +
"\t\t},\n" +
"\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" +
"\t}\n" +
"\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" +
"\"filter\": {\n" +
"\"entityList\": [\n" +
"\"" + asset1.getId().toString() + "\",\n" +
"\"" + asset2.getId().toString() + "\",\n" +
"\"" + tenantId1.getId().toString() + "\",\n" +
"\"" + existingDeviceProfile.getId().toString() + "\",\n" +
"\"" + unknownUuid + "\"\n" +
"],\n" +
"\"resolveMultiple\": true\n" +
"},\n" +
"\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
@ -287,11 +290,23 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity();
Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity();
Set<String> entityAliasEntitiesIds = Streams.stream(importedDashboard.getConfiguration()
.get("entityAliases").elements().next().get("filter").get("entityList").elements())
.map(JsonNode::asText).collect(Collectors.toSet());
assertThat(entityAliasEntitiesIds).doesNotContain(asset1.getId().toString(), asset2.getId().toString());
assertThat(entityAliasEntitiesIds).contains(importedAsset1.getId().toString(), importedAsset2.getId().toString());
Map.Entry<String, JsonNode> entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next();
assertThat(entityAlias.getKey()).isEqualTo(aliasId);
assertThat(entityAlias.getValue().get("id").asText()).isEqualTo(aliasId);
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");
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 -> {
try {
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;
}
message ValidateOrCreateDeviceX509CertRequestMsg {
string certificateChain = 1;
}
message ValidateBasicMqttCredRequestMsg {
string clientId = 1;
string userName = 2;
@ -942,6 +946,7 @@ message TransportApiRequestMsg {
GetDeviceRequestMsg deviceRequestMsg = 12;
GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13;
GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14;
ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateX509CertRequestMsg = 15;
}
/* 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.AlarmInfo;
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.AlarmSeverity;
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>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
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;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import com.fasterxml.jackson.databind.JsonNode;
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);
DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey);
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName);
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;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
@ -22,4 +23,6 @@ import org.thingsboard.server.dao.device.provision.ProvisionResponse;
public interface DeviceProvisionService {
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;
public class ProvisionFailedException extends RuntimeException {
private static final long serialVersionUID = 1673991117668477401L;
public ProvisionFailedException(String 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 ALARM_ACK = "ALARM_ACK";
public static final String ALARM_CLEAR = "ALARM_CLEAR";
public static final String ALARM_ASSIGN = "ALARM_ASSIGN";
public static final String ALARM_UNASSIGN = "ALARM_UNASSIGN";
public static final String ALARM_ASSIGNED = "ALARM_ASSIGNED";
public static final String ALARM_UNASSIGNED = "ALARM_UNASSIGNED";
public static final String ALARM_DELETE = "ALARM_DELETE";
public static final String COMMENT_CREATED = "COMMENT_CREATED";
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 {
DISABLED,
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();
}
public static String notBlankOrDefault(String src, String def) {
return isNotBlank(src) ? src : def;
}
public static String removeStart(final String str, final String remove) {
if (isEmpty(str) || isEmpty(remove)) {
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. " +
"Specify this field to update the device. " +
"Referencing non-existing User Id will cause error. " +
"Omit this field to create new customer." )
"Omit this field to create new customer.")
@Override
public UserId getId() {
return super.getId();
@ -169,6 +169,10 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
@JsonIgnore
public String getTitle() {
return getTitle(email, firstName, lastName);
}
public static String getTitle(String email, String firstName, String lastName) {
String title = "";
if (isNotEmpty(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_CLEAR(false),
ALARM_DELETE(false),
ALARM_ASSIGN(false),
ALARM_UNASSIGN(false),
ALARM_ASSIGNED(false),
ALARM_UNASSIGNED(false),
LOGIN(false),
LOGOUT(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.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
@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 {
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 org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Objects;
import java.util.Set;
@Data
@ -42,12 +44,19 @@ public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTra
}
public TransportPayloadTypeConfiguration getTransportPayloadTypeConfiguration() {
if (transportPayloadTypeConfiguration != null) {
return transportPayloadTypeConfiguration;
} else {
return new JsonTransportPayloadConfiguration();
}
return Objects.requireNonNullElseGet(transportPayloadTypeConfiguration, JsonTransportPayloadConfiguration::new);
}
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,
ALARM_ACK,
ALARM_CLEAR,
ALARM_ASSIGN,
ALARM_UNASSIGN,
ALARM_ASSIGNED,
ALARM_UNASSIGNED,
ASSIGNED_TO_EDGE,
UNASSIGNED_FROM_EDGE,
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;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Objects;
import java.util.Optional;
@ -23,6 +24,7 @@ import java.util.Optional;
public abstract class BasicKvEntry implements KvEntry {
@Length(fieldName = "attribute key")
@NoXss
private final 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;
import javax.validation.Valid;
import java.util.Objects;
import java.util.Optional;
public class BasicTsKvEntry implements TsKvEntry {
private static final int MAX_CHARS_PER_DATA_POINT = 512;
protected final long ts;
@Valid
private final 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;
public class JsonDataEntry extends BasicKvEntry {
private final 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() {
return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString();
}
@Override
public String getValueAsString() {
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,
ENTITIES_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.Data;
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.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId;
@ -59,10 +60,12 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
public Map<String, String> getTemplateData() {
return mapOf(
"action", action,
"assigneeTitle", User.getTitle(assigneeEmail, assigneeFirstName, assigneeLastName),
"assigneeFirstName", assigneeFirstName,
"assigneeLastName", assigneeLastName,
"assigneeEmail", assigneeEmail,
"assigneeId", assigneeId != null ? assigneeId.toString() : null,
"userTitle", User.getTitle(userEmail, userFirstName, userLastName),
"userEmail", userEmail,
"userFirstName", userFirstName,
"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.Data;
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.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId;
@ -55,6 +56,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
return mapOf(
"comment", comment,
"action", action,
"userTitle", User.getTitle(userEmail, userFirstName, userLastName),
"userEmail", userEmail,
"userFirstName", userFirstName,
"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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -40,6 +41,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
private CustomerId entityCustomerId;
private UUID userId;
private String userTitle;
private String userEmail;
private String userFirstName;
private String userLastName;
@ -52,6 +54,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
"entityName", entityName,
"actionType", actionType.name().toLowerCase(),
"userId", userId.toString(),
"userTitle", userTitle,
"userEmail", userEmail,
"userFirstName", userFirstName,
"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.notification.rule.trigger.NotificationRuleTriggerConfig;
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 javax.validation.Valid;
@ -43,6 +44,7 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
private TenantId tenantId;
@NotBlank
@NoXss
@Length(max = 255, message = "cannot be longer than 255 chars")
private String name;
@NotNull
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;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@ -23,6 +26,9 @@ import javax.validation.constraints.NotEmpty;
import java.util.Set;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<String> alarmTypes;

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

Loading…
Cancel
Save