Browse Source

Merge pull request #8702 from thingsboard/feature/exceeded-rate-limits-notifications

Notifications about exceeded rate limits
pull/8759/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
7258c5de73
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
  2. 2
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  3. 2
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  4. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 5
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  8. 2
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  9. 71
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RateLimitsTriggerProcessor.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  12. 2
      application/src/main/resources/thingsboard.yml
  13. 10
      application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
  14. 2
      application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java
  15. 131
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  16. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
  17. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java
  18. 70
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  19. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  20. 59
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RateLimitsNotificationInfo.java
  21. 62
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java
  22. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java
  23. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java
  24. 45
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java
  25. 14
      common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java
  26. 6
      common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java
  27. 6
      common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java
  28. 12
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java
  29. 7
      common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java
  30. 7
      common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java
  31. 7
      common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java
  32. 3
      common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java
  33. 4
      common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java
  34. 27
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java
  35. 15
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  36. 32
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  37. 6
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  38. 52
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  39. 3
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java
  40. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java
  41. 2
      dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
  42. 22
      dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java
  43. 67
      dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java
  44. 1
      dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java
  45. 5
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  46. 5
      transport/coap/src/main/resources/tb-coap-transport.yml
  47. 5
      transport/http/src/main/resources/tb-http-transport.yml
  48. 5
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  49. 5
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  50. 5
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  51. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  52. 22
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html
  53. 20
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts
  54. 1
      ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts
  55. 65
      ui-ngx/src/app/shared/components/string-items-list.component.html
  56. 167
      ui-ngx/src/app/shared/components/string-items-list.component.ts
  57. 43
      ui-ngx/src/app/shared/models/limited-api.models.ts
  58. 20
      ui-ngx/src/app/shared/models/notification.models.ts
  59. 1
      ui-ngx/src/app/shared/models/public-api.ts
  60. 50
      ui-ngx/src/assets/help/en_US/notification/rate_limits.md
  61. 22
      ui-ngx/src/assets/locale/locale.constant-en_US.json

6
application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java

@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -49,7 +49,7 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter {
SecurityUser user = getCurrentUser();
if (user != null && !user.isSystemAdmin()) {
try {
if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId())) {
if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS_PER_TENANT, user.getTenantId())) {
rateLimitExceeded(EntityType.TENANT, response);
return;
}
@ -60,7 +60,7 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter {
}
if (user.isCustomerUser()) {
if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId(), user.getCustomerId())) {
if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS_PER_CUSTOMER, user.getTenantId(), user.getCustomerId())) {
rateLimitExceeded(EntityType.CUSTOMER, response);
return;
}

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

@ -49,7 +49,7 @@ import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest;

2
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;

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

@ -261,6 +261,7 @@ public class ThingsboardInstallService {
log.info("Upgrading ThingsBoard from version 3.5.1 to 3.5.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.5.1");
dataUpdateService.updateData("3.5.1");
systemDataLoaderService.updateDefaultNotificationConfigs();
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:

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

@ -709,4 +709,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
@Override
public void updateDefaultNotificationConfigs() {
notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID);
}
}

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

@ -41,4 +41,6 @@ public interface SystemDataLoaderService {
void createDefaultNotificationConfigs();
void updateDefaultNotificationConfigs();
}

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

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.AlreadySentException;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
@ -56,7 +57,6 @@ import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;

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

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
@ -40,7 +41,6 @@ import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
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.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.notification.NotificationDeduplicationService;

71
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RateLimitsTriggerProcessor.java

@ -0,0 +1,71 @@
/**
* 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.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.RateLimitsNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class RateLimitsTriggerProcessor implements NotificationRuleTriggerProcessor<RateLimitsTrigger, RateLimitsNotificationRuleTriggerConfig> {
private final TenantService tenantService;
private final EntityService entityService;
@Override
public boolean matchesFilter(RateLimitsTrigger trigger, RateLimitsNotificationRuleTriggerConfig triggerConfig) {
return trigger.getLimitLevel() != null && trigger.getApi().getLabel() != null &&
CollectionsUtil.emptyOrContains(triggerConfig.getApis(), trigger.getApi());
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RateLimitsTrigger trigger) {
EntityId limitLevel = trigger.getLimitLevel();
String tenantName = tenantService.findTenantById(trigger.getTenantId()).getName();
String limitLevelEntityName = null;
if (limitLevel instanceof TenantId) {
limitLevelEntityName = tenantName;
} else if (limitLevel != null) {
limitLevelEntityName = Optional.ofNullable(trigger.getLimitLevelEntityName())
.orElseGet(() -> entityService.fetchEntityName(trigger.getTenantId(), limitLevel).orElse(null));
}
return RateLimitsNotificationInfo.builder()
.tenantId(trigger.getTenantId())
.tenantName(tenantName)
.api(trigger.getApi())
.limitLevel(limitLevel)
.limitLevelEntityName(limitLevelEntityName)
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RATE_LIMITS;
}
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java

@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccoun
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;

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

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;

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

@ -1277,7 +1277,7 @@ notification_system:
thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}"
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;}"
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}"
management:
endpoints:

10
application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java

@ -24,11 +24,12 @@ import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.util.limits.DefaultRateLimitService;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import java.util.List;
@ -37,6 +38,7 @@ import java.util.UUID;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
@ -50,7 +52,7 @@ public class RateLimitServiceTest {
@Before
public void beforeEach() {
tenantProfileCache = Mockito.mock(TbTenantProfileCache.class);
rateLimitService = new DefaultRateLimitService(tenantProfileCache, 60, 100);
rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100);
tenantId = new TenantId(UUID.randomUUID());
}
@ -73,14 +75,14 @@ public class RateLimitServiceTest {
LimitedApi.ENTITY_EXPORT,
LimitedApi.ENTITY_IMPORT,
LimitedApi.NOTIFICATION_REQUESTS,
LimitedApi.REST_REQUESTS,
LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES
)) {
testRateLimits(limitedApi, max, tenantId);
}
CustomerId customerId = new CustomerId(UUID.randomUUID());
testRateLimits(LimitedApi.REST_REQUESTS, max, customerId);
testRateLimits(LimitedApi.REST_REQUESTS_PER_CUSTOMER, max, customerId);
NotificationRuleId notificationRuleId = new NotificationRuleId(UUID.randomUUID());
testRateLimits(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, max, notificationRuleId);

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

@ -26,7 +26,7 @@ import org.thingsboard.server.dao.settings.AdminSettingsService;
public class MockNotificationSettingsService extends DefaultNotificationSettingsService {
public MockNotificationSettingsService(AdminSettingsService adminSettingsService) {
super(adminSettingsService, null, null);
super(adminSettingsService, null, null, null);
}
@Override

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

@ -51,16 +51,21 @@ 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.AlarmId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RateLimitsNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig;
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.NewPlatformVersionTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
@ -70,7 +75,10 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Entit
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
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;
@ -81,13 +89,12 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.notification.DefaultNotificationDeduplicationService;
import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
@ -103,6 +110,7 @@ import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset;
@ -116,7 +124,8 @@ import static org.thingsboard.server.common.data.notification.rule.trigger.confi
@DaoSqlTest
@TestPropertySource(properties = {
"transport.http.enabled=true"
"transport.http.enabled=true",
"notification_system.rules.deduplication_durations=RATE_LIMITS:10000"
})
public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@ -400,6 +409,60 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
});
}
@Test
public void testNotificationRuleProcessing_exceededRateLimits() throws Exception {
loginSysAdmin();
NotificationTarget sysadmins = createNotificationTarget(new SystemAdministratorsFilter());
NotificationTarget affectedTenantAdmins = createNotificationTarget(new AffectedTenantAdministratorsFilter());
defaultNotifications.create(TenantId.SYS_TENANT_ID, DefaultNotifications.exceededRateLimitsForSysadmin, sysadmins.getId());
defaultNotifications.create(TenantId.SYS_TENANT_ID, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId());
defaultNotifications.create(TenantId.SYS_TENANT_ID, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId());
notificationRulesCache.evict(TenantId.SYS_TENANT_ID);
int n = 10;
updateDefaultTenantProfile(profileConfiguration -> {
profileConfiguration.setTenantEntityExportRateLimit(n + ":600");
profileConfiguration.setCustomerServerRestLimitsConfiguration(n + ":600");
profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(n + ":600");
profileConfiguration.setTransportDeviceTelemetryMsgRateLimit(n + ":600");
});
loginTenantAdmin();
NotificationRule rule = createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder()
.alarmTypes(Set.of("weklfjkwefa"))
.build(), "Test", "Test", createNotificationTarget(tenantAdminUserId).getId());
for (int i = 1; i <= n * 2; i++) {
rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, tenantId);
rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS_PER_CUSTOMER, tenantId, customerId);
rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, tenantId, rule.getId());
}
loginTenantAdmin();
List<Notification> notifications = await().atMost(30, TimeUnit.SECONDS)
.until(() -> getMyNotifications(true, 10), list -> list.size() == 3);
assertThat(notifications).allSatisfy(notification -> {
assertThat(notification.getSubject()).isEqualTo("Rate limits exceeded");
});
assertThat(notifications).anySatisfy(notification -> {
assertThat(notification.getText()).isEqualTo("Rate limits for entity version creation exceeded");
});
assertThat(notifications).anySatisfy(notification -> {
assertThat(notification.getText()).isEqualTo("Rate limits for REST API requests per customer " +
"exceeded for 'Customer'");
});
assertThat(notifications).anySatisfy(notification -> {
assertThat(notification.getText()).isEqualTo("Rate limits for notification requests " +
"per rule exceeded for '" + rule.getName() + "'");
});
loginSysAdmin();
notifications = await().atMost(30, TimeUnit.SECONDS)
.until(() -> getMyNotifications(true, 10), list -> list.size() == 1);
assertThat(notifications).allSatisfy(notification -> {
assertThat(notification.getSubject()).isEqualTo("Rate limits exceeded for tenant " + TEST_TENANT_NAME);
});
assertThat(notifications.get(0).getText()).isEqualTo("Rate limits for entity version creation exceeded");
}
@Test
public void testNotificationRuleProcessing_alarmAssignment() throws Exception {
AlarmAssignmentNotificationRuleTriggerConfig triggerConfig = AlarmAssignmentNotificationRuleTriggerConfig.builder()
@ -617,6 +680,68 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
});
}
@Test
public void testNotificationsDeduplication_exceededRateLimits() throws Exception {
RateLimitsNotificationRuleTriggerConfig triggerConfig = new RateLimitsNotificationRuleTriggerConfig();
triggerConfig.setApis(Set.of(LimitedApi.ENTITY_EXPORT, LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE));
loginSysAdmin();
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
NotificationRule rule = createNotificationRule(triggerConfig, "Test 1", "Test", target.getId());
int n = 5;
updateDefaultTenantProfile(profileConfiguration -> {
profileConfiguration.setTenantEntityExportRateLimit(n + ":600");
profileConfiguration.setTransportDeviceTelemetryMsgRateLimit(n + ":800");
});
RateLimitsTrigger expectedTrigger = RateLimitsTrigger.builder()
.tenantId(tenantId)
.api(LimitedApi.ENTITY_EXPORT)
.limitLevel(tenantId)
.build();
assertThat(DefaultNotificationDeduplicationService.getDeduplicationKey(expectedTrigger, rule))
.isEqualTo("RATE_LIMITS:TENANT:" + tenantId + ":ENTITY_EXPORT_" +
target.getId() + ":ENTITY_EXPORT,TRANSPORT_MESSAGES_PER_DEVICE");
loginTenantAdmin();
getWsClient().subscribeForUnreadNotifications(10).waitForReply();
getWsClient().registerWaitForUpdate(2);
Device device = createDevice("Test", "Test");
for (int i = 1; i <= n + 1; i++) {
rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, tenantId);
doPost("/api/v1/" + device.getName() + "/telemetry", "{\"dp1\":123}", String.class);
}
int expectedNotificationsCount1 = 2;
getWsClient().waitForUpdate(true);
List<Notification> notifications1 = getMyNotifications(true, 10);
assertThat(notifications1).size().isEqualTo(expectedNotificationsCount1);
assertThat(notifications1)
.anyMatch(notification -> ((RateLimitsNotificationInfo) notification.getInfo()).getApi() == LimitedApi.ENTITY_EXPORT)
.anyMatch(notification -> ((RateLimitsNotificationInfo) notification.getInfo()).getApi() == LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE);
getWsClient().registerWaitForUpdate(2);
for (int i = 0; i < 10; i++) {
rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, tenantId);
doPost("/api/v1/" + device.getName() + "/telemetry", "{\"dp1\":123}", String.class);
}
assertThat(getWsClient().waitForUpdate(5000)).isNull();
int deduplicationDuration = 10000; // configured in TestPropertySource above
await().atLeast(2, TimeUnit.SECONDS)
.atMost(deduplicationDuration, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, tenantId);
doPost("/api/v1/" + device.getName() + "/telemetry", "{\"dp1\":123}", String.class);
Map<LimitedApi, Long> notifications2 = getMyNotifications(true, 10).stream()
.map(notification -> (RateLimitsNotificationInfo) notification.getInfo())
.collect(Collectors.groupingBy(RateLimitsNotificationInfo::getApi, Collectors.counting()));
assertThat(notifications2.get(LimitedApi.ENTITY_EXPORT)).isEqualTo(2);
assertThat(notifications2.get(LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE)).isEqualTo(2);
});
}
@Test
public void testNotificationRuleDisabling() throws Exception {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();

2
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java

@ -26,4 +26,6 @@ public interface NotificationSettingsService {
void createDefaultNotificationConfigs(TenantId tenantId);
void updateDefaultNotificationConfigs(TenantId tenantId);
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -40,6 +41,8 @@ public interface NotificationTargetService {
List<NotificationTarget> findNotificationTargetsByTenantIdAndIds(TenantId tenantId, List<NotificationTargetId> ids);
List<NotificationTarget> findNotificationTargetsByTenantIdAndUsersFilterType(TenantId tenantId, UsersFilterType filterType);
PageData<User> findRecipientsForNotificationTarget(TenantId tenantId, CustomerId customerId, NotificationTargetId targetId, PageLink pageLink);
PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, PageLink pageLink);

70
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -0,0 +1,70 @@
/**
* 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.limit;
import lombok.Getter;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import java.util.Optional;
import java.util.function.Function;
public enum LimitedApi {
ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit, "entity version creation", true),
ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit, "entity version load", true),
NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit, "notification requests", true),
NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit, "notification requests per rule", false),
REST_REQUESTS_PER_TENANT(DefaultTenantProfileConfiguration::getTenantServerRestLimitsConfiguration, "REST API requests", true),
REST_REQUESTS_PER_CUSTOMER(DefaultTenantProfileConfiguration::getCustomerServerRestLimitsConfiguration, "REST API requests per customer", false),
WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit, "WS updates per session", true),
CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true),
PASSWORD_RESET(false, true),
TWO_FA_VERIFICATION_CODE_SEND(false, true),
TWO_FA_VERIFICATION_CODE_CHECK(false, true),
TRANSPORT_MESSAGES_PER_TENANT("transport messages", true),
TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false);
private Function<DefaultTenantProfileConfiguration, String> configExtractor;
@Getter
private final boolean perTenant;
@Getter
private boolean refillRateLimitIntervally;
@Getter
private String label;
LimitedApi(Function<DefaultTenantProfileConfiguration, String> configExtractor, String label, boolean perTenant) {
this.configExtractor = configExtractor;
this.label = label;
this.perTenant = perTenant;
}
LimitedApi(boolean perTenant, boolean refillRateLimitIntervally) {
this.perTenant = perTenant;
this.refillRateLimitIntervally = refillRateLimitIntervally;
}
LimitedApi(String label, boolean perTenant) {
this.label = label;
this.perTenant = perTenant;
}
public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) {
return Optional.ofNullable(configExtractor)
.map(extractor -> extractor.apply(profileConfiguration))
.orElse(null);
}
}

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

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

59
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RateLimitsNotificationInfo.java

@ -0,0 +1,59 @@
/**
* 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.notification.info;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import java.util.Map;
import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RateLimitsNotificationInfo implements RuleOriginatedNotificationInfo {
private TenantId tenantId;
private String tenantName;
private LimitedApi api;
private EntityId limitLevel;
private String limitLevelEntityName;
@Override
public Map<String, String> getTemplateData() {
return mapOf(
"api", api.getLabel(),
"limitLevelEntityType", limitLevel != null ? limitLevel.getEntityType().getNormalName() : null,
"limitLevelEntityId", limitLevel != null ? limitLevel.getId().toString() : null,
"limitLevelEntityName", limitLevelEntityName,
"tenantName", tenantName,
"tenantId", tenantId.toString()
);
}
@Override
public TenantId getAffectedTenantId() {
return tenantId;
}
}

62
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java

@ -0,0 +1,62 @@
/**
* 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.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class RateLimitsTrigger implements NotificationRuleTrigger {
private final TenantId tenantId;
private final LimitedApi api;
private final EntityId limitLevel;
private final String limitLevelEntityName;
@Override
public NotificationRuleTriggerType getType() {
return NotificationRuleTriggerType.RATE_LIMITS;
}
@Override
public EntityId getOriginatorEntityId() {
return limitLevel != null ? limitLevel : tenantId;
}
@Override
public boolean deduplicate() {
return true;
}
@Override
public String getDeduplicationKey() {
return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), api.toString());
}
@Override
public long getDefaultDeduplicationDuration() {
return TimeUnit.HOURS.toMillis(4);
}
}

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java

@ -35,6 +35,7 @@ import java.io.Serializable;
@Type(value = NewPlatformVersionNotificationRuleTriggerConfig.class, name = "NEW_PLATFORM_VERSION"),
@Type(value = EntitiesLimitNotificationRuleTriggerConfig.class, name = "ENTITIES_LIMIT"),
@Type(value = ApiUsageLimitNotificationRuleTriggerConfig.class, name = "API_USAGE_LIMIT"),
@Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"),
})
public interface NotificationRuleTriggerConfig extends Serializable {

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java

@ -28,7 +28,8 @@ public enum NotificationRuleTriggerType {
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
NEW_PLATFORM_VERSION(false),
ENTITIES_LIMIT(false),
API_USAGE_LIMIT(false);
API_USAGE_LIMIT(false),
RATE_LIMITS(false);
private final boolean tenantLevel;

45
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java

@ -0,0 +1,45 @@
/**
* 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.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.limit.LimitedApi;
import java.util.Set;
import java.util.stream.Collectors;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RateLimitsNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<LimitedApi> apis;
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RATE_LIMITS;
}
@Override
public String getDeduplicationKey() {
return apis == null ? "#" : apis.stream().sorted().map(Enum::name).collect(Collectors.joining(","));
}
}

14
common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java

@ -31,6 +31,7 @@ import org.thingsboard.server.queue.util.PropertyUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentMap;
import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.SOFT;
@ -42,7 +43,8 @@ public class DefaultNotificationDeduplicationService implements NotificationDedu
private Map<NotificationRuleTriggerType, Long> deduplicationDurations;
private final CacheManager cacheManager;
@Autowired(required = false)
private CacheManager cacheManager;
private final ConcurrentMap<String, Long> localCache = new ConcurrentReferenceHashMap<>(16, SOFT);
@Override
@ -60,7 +62,7 @@ public class DefaultNotificationDeduplicationService implements NotificationDedu
private boolean alreadyProcessed(NotificationRuleTrigger trigger, String deduplicationKey, boolean onlyLocalCache) {
Long lastProcessedTs = localCache.get(deduplicationKey);
if (lastProcessedTs == null && !onlyLocalCache) {
Cache externalCache = cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE);
Cache externalCache = getExternalCache();
if (externalCache != null) {
lastProcessedTs = externalCache.get(deduplicationKey, Long.class);
} else {
@ -86,7 +88,7 @@ public class DefaultNotificationDeduplicationService implements NotificationDedu
if (!onlyLocalCache) {
if (!alreadyProcessed || deduplicationDuration == 0) {
// if lastProcessedTs is changed or if deduplicating infinitely (so that cache value not removed by ttl)
Cache externalCache = cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE);
Cache externalCache = getExternalCache();
if (externalCache != null) {
externalCache.put(deduplicationKey, lastProcessedTs);
}
@ -105,6 +107,12 @@ public class DefaultNotificationDeduplicationService implements NotificationDedu
});
}
private Cache getExternalCache() {
return Optional.ofNullable(cacheManager)
.map(cacheManager -> cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE))
.orElse(null);
}
@Autowired
public void setDeduplicationDurations(@Value("${notification_system.rules.deduplication_durations:}")
String deduplicationDurationsStr) {

6
common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java

@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
@ -110,6 +111,11 @@ public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory {
return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic());
}
@Override
public TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> createTbCoreNotificationsMsgProducer() {
return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, coreSettings.getTopic());
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> createTransportNotificationsConsumer() {
return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, transportNotificationSettings.getNotificationsTopic() + "_" + serviceInfoProvider.getServiceId(),

6
common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java

@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
@ -100,6 +101,11 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory
return new InMemoryTbQueueProducer<>(storage, coreSettings.getTopic());
}
@Override
public TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> createTbCoreNotificationsMsgProducer() {
return new InMemoryTbQueueProducer<>(storage, coreSettings.getTopic());
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> createTransportNotificationsConsumer() {
return new InMemoryTbQueueConsumer<>(storage, transportNotificationSettings.getNotificationsTopic() + "." + serviceInfoProvider.getServiceId());

12
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java

@ -18,7 +18,9 @@ package org.thingsboard.server.queue.provider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
@ -134,6 +136,16 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory {
return requestBuilder.build();
}
@Override
public TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> createTbCoreNotificationsMsgProducer() {
TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder<TbProtoQueueMsg<ToCoreNotificationMsg>> requestBuilder = TbKafkaProducerTemplate.builder();
requestBuilder.settings(kafkaSettings);
requestBuilder.clientId("transport-node-to-core-notifications-" + serviceInfoProvider.getServiceId());
requestBuilder.defaultTopic(coreSettings.getTopic());
requestBuilder.admin(notificationAdmin);
return requestBuilder.build();
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> createTransportNotificationsConsumer() {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToTransportMsg>> responseBuilder = TbKafkaConsumerTemplate.builder();

7
common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java

@ -18,7 +18,9 @@ package org.thingsboard.server.queue.provider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
@ -108,6 +110,11 @@ public class PubSubTransportQueueFactory implements TbTransportQueueFactory {
return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic());
}
@Override
public TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> createTbCoreNotificationsMsgProducer() {
return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, coreSettings.getTopic());
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> createTransportNotificationsConsumer() {
return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings,

7
common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java

@ -18,7 +18,9 @@ package org.thingsboard.server.queue.provider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
@ -110,6 +112,11 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory {
return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic());
}
@Override
public TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> createTbCoreNotificationsMsgProducer() {
return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, coreSettings.getTopic());
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> createTransportNotificationsConsumer() {
return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, transportNotificationSettings.getNotificationsTopic() + "." + serviceInfoProvider.getServiceId(),

7
common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java

@ -18,7 +18,9 @@ package org.thingsboard.server.queue.provider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
@ -110,6 +112,11 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory
return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic());
}
@Override
public TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> createTbCoreNotificationsMsgProducer() {
return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, coreSettings.getTopic());
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> createTransportNotificationsConsumer() {
return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings,

3
common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java

@ -16,6 +16,7 @@
package org.thingsboard.server.queue.provider;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
@ -33,6 +34,8 @@ public interface TbTransportQueueFactory extends TbUsageStatsClientQueueFactory
TbQueueProducer<TbProtoQueueMsg<ToCoreMsg>> createTbCoreMsgProducer();
TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> createTbCoreNotificationsMsgProducer();
TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> createTransportNotificationsConsumer();
}

4
common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java

@ -36,6 +36,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider
private final TbTransportQueueFactory tbQueueProvider;
private TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> toRuleEngine;
private TbQueueProducer<TbProtoQueueMsg<ToCoreMsg>> toTbCore;
private TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toTbCoreNotifications;
private TbQueueProducer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> toUsageStats;
public TbTransportQueueProducerProvider(TbTransportQueueFactory tbQueueProvider) {
@ -47,6 +48,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider
this.toTbCore = tbQueueProvider.createTbCoreMsgProducer();
this.toRuleEngine = tbQueueProvider.createRuleEngineMsgProducer();
this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer();
this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer();
}
@Override
@ -71,7 +73,7 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider
@Override
public TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> getTbCoreNotificationsMsgProducer() {
throw new RuntimeException("Not Implemented! Should not be used by Transport!");
return toTbCoreNotifications;
}
@Override

27
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java

@ -78,11 +78,11 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
return null;
}
private boolean checkEntityRateLimit(int dataPoints, EntityTransportRateLimits tenantLimits) {
private boolean checkEntityRateLimit(int dataPoints, EntityTransportRateLimits limits) {
if (dataPoints > 0) {
return tenantLimits.getTelemetryMsgRateLimit().tryConsume() && tenantLimits.getTelemetryDataPointsRateLimit().tryConsume(dataPoints);
return limits.getTelemetryMsgRateLimit().tryConsume() && limits.getTelemetryDataPointsRateLimit().tryConsume(dataPoints);
} else {
return tenantLimits.getRegularMsgRateLimit().tryConsume();
return limits.getRegularMsgRateLimit().tryConsume();
}
}
@ -241,7 +241,7 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
} else {
TransportRateLimit regularMsgRateLimit = newLimit(tenant ? profile.getTransportTenantMsgRateLimit() : profile.getTransportDeviceMsgRateLimit());
TransportRateLimit telemetryMsgRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryMsgRateLimit() : profile.getTransportDeviceTelemetryMsgRateLimit());
TransportRateLimit telemetryDpRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryDataPointsRateLimit() : profile.getTransportTenantTelemetryDataPointsRateLimit());
TransportRateLimit telemetryDpRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryDataPointsRateLimit() : profile.getTransportDeviceTelemetryDataPointsRateLimit());
return new EntityTransportRateLimits(regularMsgRateLimit, telemetryMsgRateLimit, telemetryDpRateLimit);
}
}
@ -251,21 +251,16 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
}
private EntityTransportRateLimits getTenantRateLimits(TenantId tenantId) {
EntityTransportRateLimits limits = perTenantLimits.get(tenantId);
if (limits == null) {
limits = createRateLimits(tenantProfileCache.get(tenantId), true);
perTenantLimits.put(tenantId, limits);
}
return limits;
return perTenantLimits.computeIfAbsent(tenantId, k -> {
return createRateLimits(tenantProfileCache.get(tenantId), true);
});
}
private EntityTransportRateLimits getDeviceRateLimits(TenantId tenantId, DeviceId deviceId) {
EntityTransportRateLimits limits = perDeviceLimits.get(deviceId);
if (limits == null) {
limits = createRateLimits(tenantProfileCache.get(tenantId), false);
perDeviceLimits.put(deviceId, limits);
return perDeviceLimits.computeIfAbsent(deviceId, k -> {
EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), false);
tenantDevices.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet()).add(deviceId);
}
return limits;
return limits;
});
}
}

15
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -48,9 +48,12 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.session.SessionMsgType;
@ -177,6 +180,7 @@ public class DefaultTransportService implements TransportService {
private final SchedulerComponent scheduler;
private final ApplicationEventPublisher eventPublisher;
private final TransportResourceCache transportResourceCache;
private final NotificationRuleProcessor notificationRuleProcessor;
protected TbQueueRequestTemplate<TbProtoQueueMsg<TransportApiRequestMsg>, TbProtoQueueMsg<TransportApiResponseMsg>> transportApiRequestTemplate;
protected TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> ruleEngineMsgProducer;
@ -206,7 +210,7 @@ public class DefaultTransportService implements TransportService {
TransportTenantProfileCache tenantProfileCache,
TransportRateLimitService rateLimitService,
DataDecodingEncodingService dataDecodingEncodingService, SchedulerComponent scheduler, TransportResourceCache transportResourceCache,
ApplicationEventPublisher eventPublisher) {
ApplicationEventPublisher eventPublisher, NotificationRuleProcessor notificationRuleProcessor) {
this.partitionService = partitionService;
this.serviceInfoProvider = serviceInfoProvider;
this.queueProvider = queueProvider;
@ -220,6 +224,7 @@ public class DefaultTransportService implements TransportService {
this.scheduler = scheduler;
this.transportResourceCache = transportResourceCache;
this.eventPublisher = eventPublisher;
this.notificationRuleProcessor = notificationRuleProcessor;
}
@PostConstruct
@ -877,6 +882,14 @@ public class DefaultTransportService implements TransportService {
if (callback != null) {
callback.onError(new TbRateLimitsException(rateLimitedEntityType));
}
if (rateLimitedEntityType == EntityType.DEVICE || rateLimitedEntityType == EntityType.TENANT) {
notificationRuleProcessor.process(RateLimitsTrigger.builder()
.tenantId(tenantId)
.api(rateLimitedEntityType == EntityType.DEVICE ? LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE : LimitedApi.TRANSPORT_MESSAGES_PER_TENANT)
.limitLevel(rateLimitedEntityType == EntityType.DEVICE ? deviceId : tenantId)
.limitLevelEntityName(rateLimitedEntityType == EntityType.DEVICE ? sessionInfo.getDeviceName() : null)
.build());
}
return false;
}
}

32
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationType;
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.AffectedTenantAdministratorsFilter;
@ -35,9 +36,12 @@ import org.thingsboard.server.common.data.notification.targets.platform.Platform
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Service
@ -46,6 +50,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
private final AdminSettingsService adminSettingsService;
private final NotificationTargetService notificationTargetService;
private final NotificationTemplateService notificationTemplateService;
private final DefaultNotifications defaultNotifications;
private static final String SETTINGS_KEY = "notifications";
@ -98,6 +103,10 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForSysadmin, sysAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForTenant, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.newPlatformVersion, sysAdmins.getId());
return;
}
@ -116,6 +125,29 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
defaultNotifications.create(tenantId, DefaultNotifications.ruleEngineComponentLifecycleFailure, tenantAdmins.getId());
}
@Override
public void updateDefaultNotificationConfigs(TenantId tenantId) {
if (tenantId.isSysTenantId()) {
if (notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(tenantId,
List.of(NotificationType.RATE_LIMITS), new PageLink(1)).getTotalElements() > 0) {
return;
}
NotificationTarget sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.SYSTEM_ADMINISTRATORS).stream()
.findFirst().orElseGet(() -> {
return createTarget(tenantId, "System administrators", new SystemAdministratorsFilter(), "All system administrators");
});
NotificationTarget affectedTenantAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.AFFECTED_TENANT_ADMINISTRATORS).stream()
.findFirst().orElseGet(() -> {
return createTarget(tenantId, "Affected tenant's administrators", new AffectedTenantAdministratorsFilter(), "");
});
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId());
}
}
private NotificationTarget createTarget(TenantId tenantId, String name, UsersFilter filter, String description) {
NotificationTarget target = new NotificationTarget();
target.setTenantId(tenantId);

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

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.notification.targets.platform.Platform
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractEntityService;
@ -93,6 +94,11 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
return notificationTargetDao.findByTenantIdAndIds(tenantId, ids);
}
@Override
public List<NotificationTarget> findNotificationTargetsByTenantIdAndUsersFilterType(TenantId tenantId, UsersFilterType filterType) {
return notificationTargetDao.findByTenantIdAndUsersFilterType(tenantId, filterType);
}
@Override
public PageData<User> findRecipientsForNotificationTarget(TenantId tenantId, CustomerId customerId, NotificationTargetId targetId, PageLink pageLink) {
NotificationTarget notificationTarget = findNotificationTargetById(tenantId, targetId);

52
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig;
@ -44,16 +45,20 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Entit
import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.function.Predicate.not;
import static org.thingsboard.common.util.JacksonUtil.newObjectNode;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
@ -66,6 +71,7 @@ public class DefaultNotifications {
.subject("Infrastructure maintenance")
.text("Maintenance work is scheduled for tomorrow (7:00 a.m. - 9:00 a.m. UTC)")
.build();
public static final DefaultNotification entitiesLimitForSysadmin = DefaultNotification.builder()
.name("Entities count limit notification for sysadmin")
.type(NotificationType.ENTITIES_LIMIT)
@ -88,6 +94,7 @@ public class DefaultNotifications {
.description("Send notification to tenant admins when count of entities of some type reached 80% threshold of the limit")
.build())
.build();
public static final DefaultNotification apiFeatureWarningForSysadmin = DefaultNotification.builder()
.name("API feature warning notification for sysadmin")
.type(NotificationType.API_USAGE_LIMIT)
@ -134,6 +141,51 @@ public class DefaultNotifications {
.description("Send notification to tenant admins when API feature is disabled")
.build())
.build();
public static final DefaultNotification exceededRateLimits = DefaultNotification.builder()
.name("Exceeded per-tenant rate limits notification for tenant")
.type(NotificationType.RATE_LIMITS)
.subject("Rate limits exceeded")
.text("Rate limits for ${api} exceeded")
.icon("block").color("#e91a1a")
.rule(DefaultRule.builder()
.name("Per-tenant rate limits exceeded")
.triggerConfig(RateLimitsNotificationRuleTriggerConfig.builder()
.apis(Arrays.stream(LimitedApi.values())
.filter(LimitedApi::isPerTenant)
.filter(api -> api.getLabel() != null)
.collect(Collectors.toSet()))
.build())
.description("Send notification to tenant admins when some per-tenant rate limit is exceeded")
.build())
.build();
public static final DefaultNotification exceededPerEntityRateLimits = DefaultNotification.builder()
.name("Exceeded per-entity rate limits notification for tenant")
.type(NotificationType.RATE_LIMITS)
.subject("Rate limits exceeded")
.text("Rate limits for ${api} exceeded for '${limitLevelEntityName}'")
.icon("block").color("#e91a1a")
.rule(DefaultRule.builder()
.name("Per-entity rate limits exceeded")
.triggerConfig(RateLimitsNotificationRuleTriggerConfig.builder()
.apis(Arrays.stream(LimitedApi.values())
.filter(not(LimitedApi::isPerTenant))
.filter(api -> api.getLabel() != null)
.collect(Collectors.toSet()))
.build())
.description("Send notification to tenant admins when some per-entity rate limit is exceeded for an entity")
.build())
.build();
public static final DefaultNotification exceededRateLimitsForSysadmin = exceededRateLimits.toBuilder()
.name("Exceeded per-tenant rate limits notification for sysadmin")
.subject("Rate limits exceeded for tenant ${tenantName}")
.button("Go to tenant").link("/tenants/${tenantId}")
.rule(exceededRateLimits.getRule().toBuilder()
.name("Per-tenant rate limits exceeded (sysadmin)")
.description("Send notification to system admins when a tenant exceeds some per-tenant rate limit")
.build())
.build();
public static final DefaultNotification newPlatformVersion = DefaultNotification.builder()
.name("New platform version notification")
.type(NotificationType.NEW_PLATFORM_VERSION)

3
dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
@ -35,6 +36,8 @@ public interface NotificationTargetDao extends Dao<NotificationTarget>, TenantEn
List<NotificationTarget> findByTenantIdAndIds(TenantId tenantId, List<NotificationTargetId> ids);
List<NotificationTarget> findByTenantIdAndUsersFilterType(TenantId tenantId, UsersFilterType filterType);
void removeByTenantId(TenantId tenantId);
}

6
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java

@ -66,6 +66,12 @@ public class JpaNotificationTargetDao extends JpaAbstractDao<NotificationTargetE
return DaoUtil.convertDataList(notificationTargetRepository.findByTenantIdAndIdIn(tenantId.getId(), DaoUtil.toUUIDs(ids)));
}
@Override
public List<NotificationTarget> findByTenantIdAndUsersFilterType(TenantId tenantId, UsersFilterType filterType) {
return DaoUtil.convertDataList(notificationTargetRepository.findByTenantIdAndSearchTextAndUsersFilterTypeIfPresent(tenantId.getId(), "",
List.of(filterType.name()), DaoUtil.toPageable(new PageLink(Integer.MAX_VALUE))).getContent());
}
@Override
public void removeByTenantId(TenantId tenantId) {
notificationTargetRepository.deleteByTenantId(tenantId.getId());

2
dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java

@ -37,7 +37,7 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import javax.annotation.Nullable;

22
dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java

@ -20,11 +20,16 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
@ -35,11 +40,14 @@ import java.util.concurrent.TimeUnit;
public class DefaultRateLimitService implements RateLimitService {
private final TbTenantProfileCache tenantProfileCache;
private final NotificationRuleProcessor notificationRuleProcessor;
public DefaultRateLimitService(TbTenantProfileCache tenantProfileCache,
@Lazy NotificationRuleProcessor notificationRuleProcessor,
@Value("${cache.rateLimits.timeToLiveInMinutes:120}") int rateLimitsTtl,
@Value("${cache.rateLimits.maxSize:200000}") int rateLimitsCacheMaxSize) {
this.tenantProfileCache = tenantProfileCache;
this.notificationRuleProcessor = notificationRuleProcessor;
this.rateLimits = Caffeine.newBuilder()
.expireAfterAccess(rateLimitsTtl, TimeUnit.MINUTES)
.maximumSize(rateLimitsCacheMaxSize)
@ -64,9 +72,17 @@ public class DefaultRateLimitService implements RateLimitService {
}
String rateLimitConfig = tenantProfile.getProfileConfiguration()
.map(profileConfiguration -> api.getLimitConfig(profileConfiguration, level))
.orElse(null);
return checkRateLimit(api, level, rateLimitConfig);
.map(api::getLimitConfig).orElse(null);
boolean success = checkRateLimit(api, level, rateLimitConfig);
if (!success) {
notificationRuleProcessor.process(RateLimitsTrigger.builder()
.tenantId(tenantId)
.api(api)
.limitLevel(level instanceof EntityId ? (EntityId) level : tenantId)
.limitLevelEntityName(null)
.build());
}
return success;
}
@Override

67
dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java

@ -1,67 +0,0 @@
/**
* 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.dao.util.limits;
import lombok.Getter;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import java.util.function.BiFunction;
import java.util.function.Function;
public enum LimitedApi {
ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit),
ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit),
NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit),
NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit),
REST_REQUESTS((profileConfiguration, level) -> ((EntityId) level).getEntityType() == EntityType.TENANT ?
profileConfiguration.getTenantServerRestLimitsConfiguration() :
profileConfiguration.getCustomerServerRestLimitsConfiguration()),
WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit),
CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration),
PASSWORD_RESET(true),
TWO_FA_VERIFICATION_CODE_SEND(true),
TWO_FA_VERIFICATION_CODE_CHECK(true);
private final BiFunction<DefaultTenantProfileConfiguration, Object, String> configExtractor;
@Getter
private final boolean refillRateLimitIntervally;
LimitedApi(Function<DefaultTenantProfileConfiguration, String> configExtractor) {
this((profileConfiguration, level) -> configExtractor.apply(profileConfiguration));
}
LimitedApi(BiFunction<DefaultTenantProfileConfiguration, Object, String> configExtractor) {
this.configExtractor = configExtractor;
this.refillRateLimitIntervally = false;
}
LimitedApi(boolean refillRateLimitIntervally) {
this.configExtractor = null;
this.refillRateLimitIntervally = refillRateLimitIntervally;
}
public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration, Object level) {
if (configExtractor != null) {
return configExtractor.apply(profileConfiguration, level);
} else {
throw new IllegalArgumentException("No tenant profile config for " + name() + " rate limits");
}
}
}

1
dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.util.limits;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
public interface RateLimitService {

5
msa/vc-executor/src/main/resources/tb-vc-executor.yml

@ -203,3 +203,8 @@ service:
type: "${TB_SERVICE_TYPE:tb-vc-executor}"
# Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}"
notification_system:
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:RATE_LIMITS:14400000;}"

5
transport/coap/src/main/resources/tb-coap-transport.yml

@ -312,3 +312,8 @@ management:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'
notification_system:
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:RATE_LIMITS:14400000;}"

5
transport/http/src/main/resources/tb-http-transport.yml

@ -297,3 +297,8 @@ management:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'
notification_system:
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:RATE_LIMITS:14400000;}"

5
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -379,3 +379,8 @@ management:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'
notification_system:
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:RATE_LIMITS:14400000;}"

5
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -327,3 +327,8 @@ management:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'
notification_system:
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:RATE_LIMITS:14400000;}"

5
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -277,3 +277,8 @@ management:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'
notification_system:
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:RATE_LIMITS:14400000;}"

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -177,6 +177,7 @@ import * as CopyButtonComponent from '@shared/components/button/copy-button.comp
import * as TogglePasswordComponent from '@shared/components/button/toggle-password.component';
import * as ProtobufContentComponent from '@shared/components/protobuf-content.component';
import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component';
import * as StringItemsListComponent from '@shared/components/string-items-list.component';
import * as ToggleHeaderComponent from '@shared/components/toggle-header.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
@ -475,6 +476,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/button/toggle-password.component': TogglePasswordComponent,
'@shared/components/protobuf-content.component': ProtobufContentComponent,
'@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent,
'@shared/components/string-items-list.component': StringItemsListComponent,
'@shared/components/toggle-header.component': ToggleHeaderComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,

22
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html

@ -495,6 +495,28 @@
</form>
</mat-step>
<mat-step *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.RATE_LIMITS"
[stepControl]="rateLimitsTemplateForm">
<ng-template matStepLabel>{{ 'notification.rate-limits-trigger-settings' | translate }}</ng-template>
<form [formGroup]="rateLimitsTemplateForm">
<section formGroupName="triggerConfig">
<tb-string-items-list
label="{{ 'notification.rate-limits' | translate }}"
hint="{{ 'notification.api-feature-hint' | translate }}"
[predefinedValues]="limitedApis"
formControlName="apis">
</tb-string-items-list>
</section>
</form>
<form [formGroup]="ruleNotificationForm">
<section formGroupName="additionalConfig">
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</section>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<mat-divider></mat-divider>

20
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts

@ -66,6 +66,8 @@ import {
ApiUsageStateValue,
ApiUsageStateValueTranslationMap
} from '@shared/models/api-usage.models';
import { LimitedApi, LimitedApiTranslationMap } from '@shared/models/limited-api.models';
import { StringItemsOption } from '@shared/components/string-items-list.component';
export interface RuleNotificationDialogData {
rule?: NotificationRule;
@ -95,6 +97,7 @@ export class RuleNotificationDialogComponent extends
entitiesLimitTemplateForm: FormGroup;
apiUsageLimitTemplateForm: FormGroup;
newPlatformVersionTemplateForm: FormGroup;
rateLimitsTemplateForm: FormGroup;
triggerType = TriggerType;
triggerTypes: TriggerType[];
@ -129,6 +132,8 @@ export class RuleNotificationDialogComponent extends
apiFeatures: ApiFeature[] = Object.values(ApiFeature);
apiFeatureTranslationMap = ApiFeatureTranslationMap;
limitedApis: StringItemsOption[];
entityType = EntityType;
isAdd = true;
@ -171,6 +176,11 @@ export class RuleNotificationDialogComponent extends
this.isAdd = data.isAdd;
}
this.limitedApis = Object.values(LimitedApi).map(value => ({
name: this.translate.instant(LimitedApiTranslationMap.get(value)),
value
}));
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
@ -302,6 +312,12 @@ export class RuleNotificationDialogComponent extends
})
});
this.rateLimitsTemplateForm = this.fb.group({
triggerConfig: this.fb.group({
apis: []
})
});
this.triggerTypeFormsMap = new Map<TriggerType, FormGroup>([
[TriggerType.ALARM, this.alarmTemplateForm],
[TriggerType.ALARM_COMMENT, this.alarmCommentTemplateForm],
@ -311,7 +327,8 @@ export class RuleNotificationDialogComponent extends
[TriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, this.ruleEngineEventsTemplateForm],
[TriggerType.ENTITIES_LIMIT, this.entitiesLimitTemplateForm],
[TriggerType.API_USAGE_LIMIT, this.apiUsageLimitTemplateForm],
[TriggerType.NEW_PLATFORM_VERSION, this.newPlatformVersionTemplateForm]
[TriggerType.NEW_PLATFORM_VERSION, this.newPlatformVersionTemplateForm],
[TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm]
]);
if (data.isAdd || data.isCopy) {
@ -447,6 +464,7 @@ export class RuleNotificationDialogComponent extends
TriggerType.ENTITIES_LIMIT,
TriggerType.API_USAGE_LIMIT,
TriggerType.NEW_PLATFORM_VERSION,
TriggerType.RATE_LIMITS
]);
if (this.isSysAdmin()) {

1
ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts

@ -181,6 +181,7 @@ export class TemplateNotificationDialogComponent
NotificationType.ENTITIES_LIMIT,
NotificationType.API_USAGE_LIMIT,
NotificationType.NEW_PLATFORM_VERSION,
NotificationType.RATE_LIMITS
]);
if (this.isSysAdmin()) {

65
ui-ngx/src/app/shared/components/string-items-list.component.html

@ -15,24 +15,47 @@
limitations under the License.
-->
<section [formGroup]="stringItemsForm">
<mat-form-field fxFlex class="mat-block" [floatLabel]="floatLabel" [appearance]="appearance" [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="label">{{ label }}</mat-label>
<mat-chip-grid #itemsChipList formControlName="items" [required]="required">
<mat-chip-row *ngFor="let item of stringItemsList"
[removable]="!disabled"
[contentEditable]="editable && !disabled"
(removed)="removeItems(item)">
{{item}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip-row>
<input placeholder="{{ placeholder }}"
[matChipInputFor]="itemsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addItem($event)">
</mat-chip-grid>
<mat-hint>{{ hint }}</mat-hint>
<mat-error *ngIf="stringItemsForm.get('items').hasError('required')">{{ requiredText }}</mat-error>
</mat-form-field>
</section>
<mat-form-field [formGroup]="stringItemsForm" fxFlex class="mat-block"
[floatLabel]="floatLabel"
[appearance]="appearance"
[subscriptSizing]="subscriptSizing">
<mat-label *ngIf="label">{{ label }}</mat-label>
<mat-chip-grid #itemsChipList formControlName="items" [required]="required">
<mat-chip-row *ngFor="let item of itemList"
[removable]="!disabled"
[contentEditable]="editable && !disabled"
(removed)="removeItems(item)">
{{ item.name }}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip-row>
<input matInput type="text"
placeholder="{{ placeholder }}"
style="max-width: 300px;min-width: 250px"
#stringItemInput
(focusin)="onFocus()"
formControlName="item"
#origin="matAutocompleteOrigin"
[matChipInputFor]="itemsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matAutocompleteOrigin
matChipInputAddOnBlur
(matChipInputTokenEnd)="addItem($event)"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="stringItemAutocomplete"
[matAutocompleteDisabled]="!predefinedValues?.length">
</mat-chip-grid>
<mat-autocomplete #stringItemAutocomplete="matAutocomplete"
[displayWith]="displayValueFn"
class="tb-autocomplete">
<mat-option *ngFor="let value of filteredValues | async" [value]="value">
<span [innerHTML]="value.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredValues | async)?.length" [value]="null">
{{ 'common.not-found' | translate }}
</mat-option>
</mat-autocomplete>
<mat-hint>{{ hint }}</mat-hint>
<mat-error *ngIf="stringItemsForm.get('items').hasError('required')">
{{ requiredText }}
</mat-error>
</mat-form-field>

167
ui-ngx/src/app/shared/components/string-items-list.component.ts

@ -14,13 +14,27 @@
/// limitations under the License.
///
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALUE_ACCESSOR,
Validators
} from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
import { coerceArray, coerceBoolean } from '@shared/decorators/coercion';
import { Observable, of } from 'rxjs';
import { filter, mergeMap, share, tap } from 'rxjs/operators';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
export interface StringItemsOption {
name: string;
value: any;
}
@Component({
selector: 'tb-string-items-list',
templateUrl: './string-items-list.component.html',
@ -33,17 +47,29 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}
]
})
export class StringItemsListComponent implements ControlValueAccessor{
export class StringItemsListComponent implements ControlValueAccessor, OnInit {
stringItemsForm: FormGroup;
filteredValues: Observable<Array<StringItemsOption>>;
searchText = '';
itemList: StringItemsOption[] = [];
private modelValue: Array<string> | null;
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
@ViewChild('stringItemInput', {static: true}) stringItemInput: ElementRef<HTMLInputElement>;
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
@coerceBoolean()
set required(value: boolean) {
@ -80,19 +106,52 @@ export class StringItemsListComponent implements ControlValueAccessor{
editable = false;
@Input()
subscriptSizing: SubscriptSizing = 'fixed'
subscriptSizing: SubscriptSizing = 'fixed';
private propagateChange = (v: any) => { };
@Input()
@coerceArray()
predefinedValues: StringItemsOption[];
get itemsControl(): AbstractControl {
return this.stringItemsForm.get('items');
}
get itemControl(): AbstractControl {
return this.stringItemsForm.get('item');
}
private propagateChange = (v: any) => {
};
private dirty = false;
constructor(private fb: FormBuilder) {
this.stringItemsForm = this.fb.group({
items: [null, this.required ? [Validators.required] : []]
item: [null],
items: [null]
});
}
ngOnInit() {
if (this.predefinedValues) {
this.filteredValues = this.itemControl.valueChanges
.pipe(
tap((value) => {
if (value && typeof value !== 'string') {
this.add(value);
} else if (value === null) {
this.clear();
}
}),
filter((value) => typeof value === 'string'),
mergeMap(name => this.fetchValues(name)),
share()
);
}
}
updateValidators() {
this.stringItemsForm.get('items').setValidators(this.required ? [Validators.required] : []);
this.stringItemsForm.get('items').updateValueAndValidity();
this.itemsControl.setValidators(this.required ? [Validators.required] : []);
this.itemsControl.updateValueAndValidity();
}
registerOnChange(fn: any): void {
@ -112,48 +171,100 @@ export class StringItemsListComponent implements ControlValueAccessor{
}
writeValue(value: Array<string> | null): void {
this.searchText = '';
if (value != null && value.length > 0) {
this.modelValue = [...value];
this.stringItemsForm.get('items').setValue(value);
this.itemList = [];
if (this.predefinedValues) {
value.forEach(item => {
const findItem = this.predefinedValues.find(option => option.value === item);
if (findItem) {
this.itemList.push(findItem);
}
});
} else {
value.forEach(item => this.itemList.push({value: item, name: item}));
}
this.itemsControl.setValue(this.itemList, {emitEvents: false});
} else {
this.stringItemsForm.get('items').setValue(null);
this.itemsControl.setValue(null, {emitEvents: false});
this.modelValue = null;
this.itemList = [];
}
this.dirty = true;
}
addItem(event: MatChipInputEvent): void {
let item = event.value || '';
const input = event.chipInput.inputElement;
item = item.trim();
const item = event.value?.trim() ?? '';
if (item) {
if (!this.modelValue || this.modelValue.indexOf(item) === -1) {
if (!this.modelValue) {
this.modelValue = [];
if (this.predefinedValues) {
const findItems = this.predefinedValues
.filter(value => value.name.toLowerCase().includes(item.toLowerCase()));
if (findItems.length === 1) {
this.add(findItems[0]);
}
this.modelValue.push(item);
this.stringItemsForm.get('items').setValue(this.modelValue);
}
this.propagateChange(this.modelValue);
if (input) {
input.value = '';
} else {
this.add({value: item, name: item});
}
}
}
removeItems(item: string) {
const index = this.modelValue.indexOf(item);
removeItems(item: StringItemsOption) {
const index = this.modelValue.indexOf(item.value);
if (index >= 0) {
this.modelValue.splice(index, 1);
this.itemList.splice(index, 1);
if (!this.modelValue.length) {
this.modelValue = null;
}
this.stringItemsForm.get('items').setValue(this.modelValue);
this.itemsControl.setValue(this.itemList);
this.propagateChange(this.modelValue);
this.autocomplete?.closePanel();
}
}
onFocus() {
if (this.dirty) {
this.itemControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
displayValueFn(values?: StringItemsOption): string | undefined {
return values ? values.name : undefined;
}
private add(item: StringItemsOption) {
if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) {
if (!this.modelValue) {
this.modelValue = [];
}
this.modelValue.push(item.value);
this.itemList.push(item);
this.itemsControl.setValue(this.itemList);
}
this.propagateChange(this.modelValue);
this.clear();
}
get stringItemsList(): string[] {
return this.stringItemsForm.get('items').value;
private fetchValues(searchText?: string): Observable<Array<StringItemsOption>> {
if (!this.predefinedValues?.length) {
return of([]);
}
this.searchText = searchText;
let result = this.predefinedValues;
if (searchText && searchText.length) {
result = this.predefinedValues.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase()));
}
return of(result);
}
private clear(value: string = '') {
this.stringItemInput.nativeElement.value = value;
this.itemControl.patchValue(value, {emitEvent: true});
setTimeout(() => {
this.stringItemInput.nativeElement.blur();
this.stringItemInput.nativeElement.focus();
}, 0);
}
}

43
ui-ngx/src/app/shared/models/limited-api.models.ts

@ -0,0 +1,43 @@
///
/// 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.
///
export enum LimitedApi {
ENTITY_EXPORT = 'ENTITY_EXPORT',
ENTITY_IMPORT = 'ENTITY_IMPORT',
NOTIFICATION_REQUESTS = 'NOTIFICATION_REQUESTS',
NOTIFICATION_REQUESTS_PER_RULE = 'NOTIFICATION_REQUESTS_PER_RULE',
REST_REQUESTS_PER_TENANT = 'REST_REQUESTS_PER_TENANT',
REST_REQUESTS_PER_CUSTOMER = 'REST_REQUESTS_PER_CUSTOMER',
WS_UPDATES_PER_SESSION = 'WS_UPDATES_PER_SESSION',
CASSANDRA_QUERIES = 'CASSANDRA_QUERIES',
TRANSPORT_MESSAGES_PER_TENANT = 'TRANSPORT_MESSAGES_PER_TENANT',
TRANSPORT_MESSAGES_PER_DEVICE = 'TRANSPORT_MESSAGES_PER_DEVICE'
}
export const LimitedApiTranslationMap = new Map<LimitedApi, string>(
[
[LimitedApi.ENTITY_EXPORT, 'api-limit.entity-version-creation'],
[LimitedApi.ENTITY_IMPORT, 'api-limit.entity-version-load'],
[LimitedApi.NOTIFICATION_REQUESTS, 'api-limit.notification-requests'],
[LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, 'api-limit.notification-requests-per-rule'],
[LimitedApi.REST_REQUESTS_PER_TENANT, 'api-limit.rest-api-requests'],
[LimitedApi.REST_REQUESTS_PER_CUSTOMER, 'api-limit.rest-api-requests-per-customer'],
[LimitedApi.WS_UPDATES_PER_SESSION, 'api-limit.ws-updates-per-session'],
[LimitedApi.CASSANDRA_QUERIES, 'api-limit.cassandra-queries'],
[LimitedApi.TRANSPORT_MESSAGES_PER_TENANT, 'api-limit.transport-messages'],
[LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE, 'api-limit.transport-messages-per-device']
]
);

20
ui-ngx/src/app/shared/models/notification.models.ts

@ -26,6 +26,7 @@ import { NotificationRuleId } from '@shared/models/id/notification-rule-id';
import { AlarmSearchStatus, AlarmSeverity, AlarmStatus } from '@shared/models/alarm.models';
import { EntityType } from '@shared/models/entity-type.models';
import { ApiFeature, ApiUsageStateValue } from '@shared/models/api-usage.models';
import { LimitedApi } from '@shared/models/limited-api.models';
export interface Notification {
readonly id: NotificationId;
@ -119,7 +120,7 @@ export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'la
export type NotificationRuleTriggerConfig = Partial<AlarmNotificationRuleTriggerConfig & DeviceInactivityNotificationRuleTriggerConfig &
EntityActionNotificationRuleTriggerConfig & AlarmCommentNotificationRuleTriggerConfig & AlarmAssignmentNotificationRuleTriggerConfig &
RuleEngineLifecycleEventNotificationRuleTriggerConfig & EntitiesLimitNotificationRuleTriggerConfig &
ApiUsageLimitNotificationRuleTriggerConfig>;
ApiUsageLimitNotificationRuleTriggerConfig & RateLimitsNotificationRuleTriggerConfig>;
export interface AlarmNotificationRuleTriggerConfig {
alarmTypes?: Array<string>;
@ -178,6 +179,10 @@ export interface ApiUsageLimitNotificationRuleTriggerConfig {
notifyOn: ApiUsageStateValue[];
}
export interface RateLimitsNotificationRuleTriggerConfig {
apis: LimitedApi[];
}
export enum ComponentLifecycleEvent {
STARTED = 'STARTED',
UPDATED = 'UPDATED',
@ -444,7 +449,8 @@ export enum NotificationType {
ENTITIES_LIMIT = 'ENTITIES_LIMIT',
API_USAGE_LIMIT = 'API_USAGE_LIMIT',
NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION',
RULE_NODE = 'RULE_NODE'
RULE_NODE = 'RULE_NODE',
RATE_LIMITS = 'RATE_LIMITS'
}
export const NotificationTypeIcons = new Map<NotificationType, string | null>([
@ -549,6 +555,12 @@ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, No
name: 'notification.template-type.rule-node',
helpId: 'notification/rule_node'
}
],
[NotificationType.RATE_LIMITS,
{
name: 'notification.template-type.rate-limits',
helpId: 'notification/rate_limits'
}
]
]);
@ -561,7 +573,8 @@ export enum TriggerType {
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT = 'RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT',
ENTITIES_LIMIT = 'ENTITIES_LIMIT',
API_USAGE_LIMIT = 'API_USAGE_LIMIT',
NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION'
NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION',
RATE_LIMITS = 'RATE_LIMITS'
}
export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
@ -574,4 +587,5 @@ export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
[TriggerType.ENTITIES_LIMIT, 'notification.trigger.entities-limit'],
[TriggerType.API_USAGE_LIMIT, 'notification.trigger.api-usage-limit'],
[TriggerType.NEW_PLATFORM_VERSION, 'notification.trigger.new-platform-version'],
[TriggerType.RATE_LIMITS, 'notification.trigger.rate-limits'],
]);

1
ui-ngx/src/app/shared/models/public-api.ts

@ -37,6 +37,7 @@ export * from './entity-type.models';
export * from './entity-view.models';
export * from './error.models';
export * from './event.models';
export * from './limited-api.models';
export * from './login.models';
export * from './material.models';
export * from './notification.models';

50
ui-ngx/src/assets/help/en_US/notification/rate_limits.md

@ -0,0 +1,50 @@
#### Exceeded rate limits notification templatization
<div class="divider"></div>
<br/>
Notification subject and message fields support templatization.
The list of available templatization parameters depends on the template type.
See the available types and parameters below:
Available template parameters:
* `api` - rate-limited API label; one of: 'REST API requests', 'REST API requests per customer', 'transport messages',
'transport messages per device', 'Cassandra queries', 'WS updates per session', 'notification requests', 'notification requests per rule',
'entity version creation', 'entity version load';
* `limitLevelEntityType` - entity type of the limit level entity, e.g. 'Tenant', 'Device', 'Notification rule', 'Customer', etc.;
* `limitLevelEntityId` - id of the limit level entity;
* `limitLevelEntityName` - name of the limit level entity;
* `tenantId` - id of the tenant;
* `tenantName` - name of the tenant;
* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise);
* `recipientEmail` - email of the recipient;
* `recipientFirstName` - first name of the recipient;
* `recipientLastName` - last name of the recipient;
Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`.
You may also modify the value of the parameter with one of the suffixes:
* `upperCase`, for example - `${recipientFirstName:upperCase}`
* `lowerCase`, for example - `${recipientFirstName:lowerCase}`
* `capitalize`, for example - `${recipientFirstName:capitalize}`
<div class="divider"></div>
##### Examples
Let's assume customer 'Customer A' exceeded rate limit for per-customer REST API requests. The following template:
```text
Rate limits for ${api} exceeded for ${limitLevelEntityType:lowerCase} '${limitLevelEntityName}'
{:copy-code}
```
will be transformed to:
```text
Rate limits for REST API requests per customer exceeded for customer 'Customer A'
```
<br>
<br>

22
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -779,6 +779,18 @@
"view-details": "View details",
"view-statistics": "View statistics"
},
"api-limit": {
"cassandra-queries": "Cassandra queries",
"entity-version-creation": "Entity version creation",
"entity-version-load": "Entity version load",
"notification-requests": "Notification requests",
"notification-requests-per-rule": "Notification requests per rule",
"rest-api-requests": "REST API requests",
"rest-api-requests-per-customer": "REST API requests per customer",
"transport-messages": "Transport messages",
"transport-messages-per-device": "Transport messages per device",
"ws-updates-per-session": "WS updates per session"
},
"audit-log": {
"audit": "Audit",
"audit-logs": "Audit Logs",
@ -862,7 +874,8 @@
"created-time": "Created time",
"loading": "Loading...",
"proceed": "Proceed",
"open-details-page": "Open details page"
"open-details-page": "Open details page",
"not-found": "Not found"
},
"content-type": {
"json": "Json",
@ -2849,6 +2862,7 @@
"api-feature-hint": "If the field is empty, the trigger will be applied to all api features",
"api-usage-trigger-settings": "API usage trigger settings",
"new-platform-version-trigger-settings": "New platform version trigger settings",
"rate-limits-trigger-settings": "Exceeded rate limits trigger settings",
"at-least-one-should-be-selected": "At least one should be selected",
"basic-settings": "Basic settings",
"button-text": "Button text",
@ -2966,6 +2980,8 @@
"only-rule-chain-lifecycle-failures": "Only rule chain lifecycle failures",
"only-rule-node-lifecycle-failures": "Only rule node lifecycle failures",
"platform-users": "Platform users",
"rate-limits": "Rate limits",
"rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits",
"recipient": "Recipient",
"recipient-group": "Recipient group",
"recipient-type": {
@ -3044,7 +3060,8 @@
"general": "General",
"rule-engine-lifecycle-event": "Rule engine lifecycle event",
"rule-node": "Rule node",
"new-platform-version": "New platform version"
"new-platform-version": "New platform version",
"rate-limits": "Exceeded rate limits"
},
"templates": "Templates",
"notification-templates": "Notifications / Templates",
@ -3063,6 +3080,7 @@
"entity-action": "Entity action",
"rule-engine-lifecycle-event": "Rule engine lifecycle event",
"new-platform-version": "New platform version",
"rate-limits": "Exceeded rate limits",
"trigger": "Trigger",
"trigger-required": "Trigger is required"
},

Loading…
Cancel
Save