From bf7454ad3c75f7f704d917081c9cfb54eb2ea8b8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 28 Apr 2023 14:28:16 +0300 Subject: [PATCH 1/5] Move rate limiting to one service --- .../config/RateLimitProcessingFilter.java | 64 +++------- .../server/controller/AuthController.java | 19 +-- .../controller/plugin/TbWebSocketHandler.java | 37 +++--- .../service/apiusage/limits/LimitedApi.java | 37 ------ .../DefaultNotificationCenter.java | 4 +- .../DefaultNotificationRuleProcessor.java | 4 +- .../auth/mfa/DefaultTwoFactorAuthService.java | 52 ++++----- .../DefaultEntitiesExportImportService.java | 4 +- .../service/limits/RateLimitServiceTest.java | 110 ++++++++++++++++++ .../notification/NotificationRuleApiTest.java | 4 +- .../TenantProfileNotFoundException.java | 31 +++++ .../CassandraBufferedRateReadExecutor.java | 9 +- .../CassandraBufferedRateWriteExecutor.java | 6 +- .../util/AbstractBufferedRateExecutor.java | 45 +++---- .../util}/limits/DefaultRateLimitService.java | 58 +++++---- .../server/dao/util/limits/LimitedApi.java | 67 +++++++++++ .../dao/util}/limits/RateLimitService.java | 9 +- 17 files changed, 338 insertions(+), 222 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java create mode 100644 application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantProfileNotFoundException.java rename {application/src/main/java/org/thingsboard/server/service/apiusage => dao/src/main/java/org/thingsboard/server/dao/util}/limits/DefaultRateLimitService.java (57%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java rename {application/src/main/java/org/thingsboard/server/service/apiusage => dao/src/main/java/org/thingsboard/server/dao/util}/limits/RateLimitService.java (75%) diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java index 0593b9765a..89f9b751fd 100644 --- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java @@ -15,64 +15,53 @@ */ package org.thingsboard.server.config; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.tools.TbRateLimits; +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.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.FilterChain; import javax.servlet.ServletException; -import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; @Slf4j @Component +@RequiredArgsConstructor public class RateLimitProcessingFilter extends OncePerRequestFilter { - @Autowired - private ThingsboardErrorResponseHandler errorResponseHandler; - - @Autowired - @Lazy - private TbTenantProfileCache tenantProfileCache; - - private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap perCustomerLimits = new ConcurrentHashMap<>(); + private final ThingsboardErrorResponseHandler errorResponseHandler; + private final RateLimitService rateLimitService; @Override public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { SecurityUser user = getCurrentUser(); if (user != null && !user.isSystemAdmin()) { - var profile = tenantProfileCache.get(user.getTenantId()); - if (profile == null) { + try { + if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId())) { + rateLimitExceeded(EntityType.TENANT, response); + return; + } + } catch (TenantProfileNotFoundException e) { log.debug("[{}] Failed to lookup tenant profile", user.getTenantId()); errorResponseHandler.handle(new BadCredentialsException("Failed to lookup tenant profile"), response); return; } - var profileConfiguration = profile.getDefaultProfileConfiguration(); - if (!checkRateLimits(user.getTenantId(), profileConfiguration.getTenantServerRestLimitsConfiguration(), perTenantLimits, response)) { - return; - } + if (user.isCustomerUser()) { - if (!checkRateLimits(user.getCustomerId(), profileConfiguration.getCustomerServerRestLimitsConfiguration(), perCustomerLimits, response)) { + if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId(), user.getCustomerId())) { + rateLimitExceeded(EntityType.CUSTOMER, response); return; } } @@ -90,23 +79,8 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter { return false; } - private boolean checkRateLimits(I ownerId, String rateLimitConfig, Map rateLimitsMap, ServletResponse response) { - if (StringUtils.isNotEmpty(rateLimitConfig)) { - TbRateLimits rateLimits = rateLimitsMap.get(ownerId); - if (rateLimits == null || !rateLimits.getConfiguration().equals(rateLimitConfig)) { - rateLimits = new TbRateLimits(rateLimitConfig); - rateLimitsMap.put(ownerId, rateLimits); - } - - if (!rateLimits.tryConsume()) { - errorResponseHandler.handle(new TbRateLimitsException(ownerId.getEntityType()), (HttpServletResponse) response); - return false; - } - } else { - rateLimitsMap.remove(ownerId); - } - - return true; + private void rateLimitExceeded(EntityType type, HttpServletResponse response) { + errorResponseHandler.handle(new TbRateLimitsException(type), response); } protected SecurityUser getCurrentUser() { diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 32b997c21f..566b6900a4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -42,19 +42,18 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; +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.common.msg.tools.TbRateLimits; -import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.dao.util.limits.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; import org.thingsboard.server.service.security.model.ChangePasswordRequest; -import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; import org.thingsboard.server.service.security.model.ResetPasswordRequest; import org.thingsboard.server.service.security.model.SecurityUser; @@ -65,8 +64,6 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; import java.net.URI; import java.net.URISyntaxException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; @RestController @TbCoreComponent @@ -77,12 +74,11 @@ public class AuthController extends BaseController { @Value("${server.rest.rate_limits.reset_password_per_user:5:3600}") private String defaultLimitsConfiguration; - private final ConcurrentMap resetPasswordRateLimits = new ConcurrentHashMap<>(); private final BCryptPasswordEncoder passwordEncoder; private final JwtTokenFactory tokenFactory; private final MailService mailService; private final SystemSecurityService systemSecurityService; - private final AuditLogService auditLogService; + private final RateLimitService rateLimitService; private final ApplicationEventPublisher eventPublisher; @@ -210,8 +206,7 @@ public class AuthController extends BaseController { UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); if (userCredentials != null) { - TbRateLimits tbRateLimits = getTbRateLimits(userCredentials.getUserId()); - if (!tbRateLimits.tryConsume()) { + if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build(); } try { @@ -314,8 +309,4 @@ public class AuthController extends BaseController { eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId())); } - private TbRateLimits getTbRateLimits(UserId userId) { - return resetPasswordRateLimits.computeIfAbsent(userId, - key -> new TbRateLimits(defaultLimitsConfiguration, true)); - } } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 7a396ff795..bc51c5fdd1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -28,16 +28,16 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.adapter.NativeWebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.msg.tools.TbRateLimits; 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.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -80,6 +80,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke @Autowired private TbTenantProfileCache tenantProfileCache; + @Autowired + private RateLimitService rateLimitService; + @Value("${server.ws.send_timeout:5000}") private long sendTimeout; @Value("${server.ws.ping_timeout:30000}") @@ -88,7 +91,6 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke private int wsMaxQueueMessagesPerSession; private final ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); - private final ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>(); private final ConcurrentMap> tenantSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap> customerSessionsMap = new ConcurrentHashMap<>(); @@ -331,24 +333,17 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (internalId != null) { SessionMetaData sessionMd = internalSessionMap.get(internalId); if (sessionMd != null) { - var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); - if (tenantProfileConfiguration != null) { - if (StringUtils.isNotEmpty(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())) { - TbRateLimits rateLimits = perSessionUpdateLimits.computeIfAbsent(sessionRef.getSessionId(), sid -> new TbRateLimits(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())); - if (!rateLimits.tryConsume()) { - if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { - log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" - , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); - sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); - } - return; - } else { - log.debug("[{}][{}][{}] Session is no longer blacklisted.", sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); - blacklistedSessions.remove(externalId); - } - } else { - perSessionUpdateLimits.remove(sessionRef.getSessionId()); + TenantId tenantId = sessionRef.getSecurityCtx().getTenantId(); + if (!rateLimitService.checkRateLimit(LimitedApi.WS_UPDATES_PER_SESSION, tenantId, (Object) sessionRef.getSessionId())) { + if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { + log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" + , tenantId, sessionRef.getSecurityCtx().getId(), externalId); + sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); } + return; + } else { + log.debug("[{}][{}][{}] Session is no longer blacklisted.", tenantId, sessionRef.getSecurityCtx().getId(), externalId); + blacklistedSessions.remove(externalId); } sessionMd.sendMsg(msg); } else { @@ -464,7 +459,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (tenantProfileConfiguration == null) return; String sessionId = session.getId(); - perSessionUpdateLimits.remove(sessionRef.getSessionId()); + rateLimitService.cleanUp(LimitedApi.WS_UPDATES_PER_SESSION, sessionRef.getSessionId()); blacklistedSessions.remove(sessionRef.getSessionId()); if (tenantProfileConfiguration.getMaxWsSessionsPerTenant() > 0) { Set tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java deleted file mode 100644 index 4f216ffaca..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java +++ /dev/null @@ -1,37 +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.service.apiusage.limits; - -import lombok.RequiredArgsConstructor; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; - -import java.util.function.Function; - -@RequiredArgsConstructor -public enum LimitedApi { - - ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit), - ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit), - NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit), - NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit); - - private final Function configExtractor; - - public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) { - return configExtractor.apply(profileConfiguration); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index 91bc0863db..79b424b9fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -65,8 +65,8 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.notification.channels.NotificationChannel; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java index 57dba39063..ab3a2f4ce6 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java @@ -42,8 +42,8 @@ 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.queue.discovery.PartitionService; -import org.thingsboard.server.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.notification.rule.cache.NotificationRulesCache; import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index 4231208850..a58c801583 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.security.auth.mfa; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.LockedException; import org.springframework.stereotype.Service; @@ -29,8 +30,9 @@ import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettin import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.util.limits.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; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider; @@ -41,8 +43,6 @@ import java.util.Collection; import java.util.EnumMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; @Service @RequiredArgsConstructor @@ -52,14 +52,13 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private final TwoFaConfigManager configManager; private final SystemSecurityService systemSecurityService; private final UserService userService; + private final RateLimitService rateLimitService; private final Map> providers = new EnumMap<>(TwoFaProviderType.class); private static final ThingsboardException ACCOUNT_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS); private static final ThingsboardException PROVIDER_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS); private static final ThingsboardException PROVIDER_NOT_AVAILABLE_ERROR = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL); - - private final ConcurrentMap> verificationCodeSendingRateLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap> verificationCodeCheckingRateLimits = new ConcurrentHashMap<>(); + private static final ThingsboardException TOO_MANY_REQUESTS_ERROR = new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); @Override public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) { @@ -91,7 +90,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { if (minVerificationCodeSendPeriod != null && minVerificationCodeSendPeriod > 4) { rateLimit = "1:" + minVerificationCodeSendPeriod; } - checkRateLimits(user.getId(), accountConfig.getProviderType(), rateLimit, verificationCodeSendingRateLimits); + if (!rateLimitService.checkRateLimit(LimitedApi.TWO_FA_VERIFICATION_CODE_SEND, + Pair.of(user.getId(), accountConfig.getProviderType()), rateLimit)) { + throw TOO_MANY_REQUESTS_ERROR; + } } TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) @@ -116,7 +118,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { PlatformTwoFaSettings twoFaSettings = configManager.getPlatformTwoFaSettings(user.getTenantId(), true) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (checkLimits) { - checkRateLimits(user.getId(), accountConfig.getProviderType(), twoFaSettings.getVerificationCodeCheckRateLimit(), verificationCodeCheckingRateLimits); + if (!rateLimitService.checkRateLimit(LimitedApi.TWO_FA_VERIFICATION_CODE_CHECK, + Pair.of(user.getId(), accountConfig.getProviderType()), twoFaSettings.getVerificationCodeCheckRateLimit())) { + throw TOO_MANY_REQUESTS_ERROR; + } } TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); @@ -131,43 +136,28 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { try { systemSecurityService.validateTwoFaVerification(user, verificationSuccess, twoFaSettings); } catch (LockedException e) { - verificationCodeCheckingRateLimits.remove(user.getId()); - verificationCodeSendingRateLimits.remove(user.getId()); + cleanUpRateLimits(user.getId()); throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION); } if (verificationSuccess) { - verificationCodeCheckingRateLimits.remove(user.getId()); - verificationCodeSendingRateLimits.remove(user.getId()); + cleanUpRateLimits(user.getId()); } } return verificationSuccess; } - private void checkRateLimits(UserId userId, TwoFaProviderType providerType, String rateLimitConfig, - ConcurrentMap> rateLimits) throws ThingsboardException { - if (StringUtils.isNotEmpty(rateLimitConfig)) { - ConcurrentMap providersRateLimits = rateLimits.computeIfAbsent(userId, i -> new ConcurrentHashMap<>()); - - TbRateLimits rateLimit = providersRateLimits.get(providerType); - if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) { - rateLimit = new TbRateLimits(rateLimitConfig, true); - providersRateLimits.put(providerType, rateLimit); - } - if (!rateLimit.tryConsume()) { - throw new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); - } - } else { - rateLimits.remove(userId); - } - } - - @Override public TwoFaAccountConfig generateNewAccountConfig(User user, TwoFaProviderType providerType) throws ThingsboardException { TwoFaProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType); return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig); } + private void cleanUpRateLimits(UserId userId) { + for (TwoFaProviderType providerType : TwoFaProviderType.values()) { + rateLimitService.cleanUp(LimitedApi.TWO_FA_VERIFICATION_CODE_SEND, Pair.of(userId, providerType)); + rateLimitService.cleanUp(LimitedApi.TWO_FA_VERIFICATION_CODE_CHECK, Pair.of(userId, providerType)); + } + } private TwoFaProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException { return configManager.getPlatformTwoFaSettings(tenantId, true) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 793c25d625..b1c5ea31be 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -32,8 +32,8 @@ import org.thingsboard.server.common.data.sync.ie.EntityImportResult; 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.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.dao.util.limits.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; import org.thingsboard.server.service.sync.ie.exporting.impl.BaseEntityExportService; diff --git a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java new file mode 100644 index 0000000000..05ee66f327 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java @@ -0,0 +1,110 @@ +/** + * 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.limits; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +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.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +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; +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.reset; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RateLimitServiceTest { + + private RateLimitService rateLimitService; + private TbTenantProfileCache tenantProfileCache; + private TenantId tenantId; + + @Before + public void beforeEach() { + tenantProfileCache = Mockito.mock(TbTenantProfileCache.class); + rateLimitService = new DefaultRateLimitService(tenantProfileCache, 60, 100); + tenantId = new TenantId(UUID.randomUUID()); + } + + @Test + public void testRateLimits() { + int max = 2; + String rateLimit = max + ":600"; + DefaultTenantProfileConfiguration profileConfiguration = new DefaultTenantProfileConfiguration(); + profileConfiguration.setTenantEntityExportRateLimit(rateLimit); + profileConfiguration.setTenantEntityImportRateLimit(rateLimit); + profileConfiguration.setTenantNotificationRequestsRateLimit(rateLimit); + profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(rateLimit); + profileConfiguration.setTenantServerRestLimitsConfiguration(rateLimit); + profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit); + profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit); + profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit); + updateTenantProfileConfiguration(profileConfiguration); + + for (LimitedApi limitedApi : List.of( + LimitedApi.ENTITY_EXPORT, + LimitedApi.ENTITY_IMPORT, + LimitedApi.NOTIFICATION_REQUESTS, + LimitedApi.REST_REQUESTS, + LimitedApi.CASSANDRA_QUERIES + )) { + testRateLimits(limitedApi, max, tenantId); + } + + CustomerId customerId = new CustomerId(UUID.randomUUID()); + testRateLimits(LimitedApi.REST_REQUESTS, max, customerId); + + NotificationRuleId notificationRuleId = new NotificationRuleId(UUID.randomUUID()); + testRateLimits(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, max, notificationRuleId); + + String wsSessionId = UUID.randomUUID().toString(); + testRateLimits(LimitedApi.WS_UPDATES_PER_SESSION, max, wsSessionId); + } + + private void testRateLimits(LimitedApi limitedApi, int max, Object level) { + for (int i = 1; i <= max; i++) { + boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level); + assertTrue(success); + } + boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level); + assertFalse(success); + } + + private void updateTenantProfileConfiguration(DefaultTenantProfileConfiguration profileConfiguration) { + reset(tenantProfileCache); + TenantProfile tenantProfile = new TenantProfile(); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(profileConfiguration); + tenantProfile.setProfileData(profileData); + when(tenantProfileCache.get(eq(tenantId))).thenReturn(tenantProfile); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index dae2d06075..1c871f06cf 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -69,8 +69,8 @@ import org.thingsboard.server.common.data.security.Authority; 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.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.util.ArrayList; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantProfileNotFoundException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantProfileNotFoundException.java new file mode 100644 index 0000000000..565ee493ea --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantProfileNotFoundException.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.exception; + +import lombok.Getter; +import org.thingsboard.server.common.data.id.TenantId; + +public class TenantProfileNotFoundException extends RuntimeException { + + @Getter + private final TenantId tenantId; + + public TenantProfileNotFoundException(TenantId tenantId) { + super("Profile for tenant with id " + tenantId + " not found"); + this.tenantId = tenantId; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java index 4c13fe3b25..598e180ce8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java @@ -22,15 +22,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.stats.DefaultCounter; -import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.entity.EntityService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor; import org.thingsboard.server.dao.util.AsyncTaskContext; import org.thingsboard.server.dao.util.NoSqlAnyDao; +import org.thingsboard.server.dao.util.limits.RateLimitService; import javax.annotation.PreDestroy; @@ -55,9 +52,9 @@ public class CassandraBufferedRateReadExecutor extends AbstractBufferedRateExecu @Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq, @Autowired StatsFactory statsFactory, @Autowired EntityService entityService, - @Autowired TbTenantProfileCache tenantProfileCache) { + @Autowired RateLimitService rateLimitService) { super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, printQueriesFreq, statsFactory, - entityService, tenantProfileCache, printTenantNames); + entityService, rateLimitService, printTenantNames); } @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java index 55b1b5410d..1cb2eaf3df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java @@ -24,10 +24,10 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.entity.EntityService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor; import org.thingsboard.server.dao.util.AsyncTaskContext; import org.thingsboard.server.dao.util.NoSqlAnyDao; +import org.thingsboard.server.dao.util.limits.RateLimitService; import javax.annotation.PreDestroy; @@ -52,9 +52,9 @@ public class CassandraBufferedRateWriteExecutor extends AbstractBufferedRateExec @Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq, @Autowired StatsFactory statsFactory, @Autowired EntityService entityService, - @Autowired TbTenantProfileCache tenantProfileCache) { + @Autowired RateLimitService rateLimitService) { super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, printQueriesFreq, statsFactory, - entityService, tenantProfileCache, printTenantNames); + entityService, rateLimitService, printTenantNames); } @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index c518de6a5c..64b4eb3666 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -30,25 +30,21 @@ import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsCounter; 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.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; @@ -74,7 +70,6 @@ public abstract class AbstractBufferedRateExecutor perTenantLimits = new ConcurrentHashMap<>(); private final AtomicInteger printQueriesIdx = new AtomicInteger(0); @@ -82,14 +77,14 @@ public abstract class AbstractBufferedRateExecutor tenantNamesCache = new HashMap<>(); public AbstractBufferedRateExecutor(int queueLimit, int concurrencyLimit, long maxWaitTime, int dispatcherThreads, int callbackThreads, long pollMs, int printQueriesFreq, StatsFactory statsFactory, - EntityService entityService, TbTenantProfileCache tenantProfileCache, boolean printTenantNames) { + EntityService entityService, RateLimitService rateLimitService, boolean printTenantNames) { this.maxWaitTime = maxWaitTime; this.pollMs = pollMs; this.concurrencyLimit = concurrencyLimit; @@ -103,7 +98,7 @@ public abstract class AbstractBufferedRateExecutor new TbRateLimits(tenantProfileConfiguration.getCassandraQueryTenantRateLimitsConfiguration()) - ); - if (!rateLimits.tryConsume()) { - stats.incrementRateLimitedTenant(task.getTenantId()); - stats.getTotalRateLimited().increment(); - settableFuture.setException(new TenantRateLimitException()); - perTenantLimitReached = true; - } + TenantId tenantId = task.getTenantId(); + if (tenantId != null && !tenantId.isSysTenantId()) { + if (!rateLimitService.checkRateLimit(LimitedApi.CASSANDRA_QUERIES, tenantId)) { + stats.incrementRateLimitedTenant(tenantId); + stats.getTotalRateLimited().increment(); + settableFuture.setException(new TenantRateLimitException()); + perTenantLimitReached = true; } - } else if (!TenantId.SYS_TENANT_ID.equals(task.getTenantId())) { - perTenantLimits.remove(task.getTenantId()); + } else if (tenantId == null) { + log.info("Invalid task received: {}", task); } if (!perTenantLimitReached) { diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java b/dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java similarity index 57% rename from application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java rename to dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java index 9a0282c840..ba5a50ab1d 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java @@ -13,83 +13,95 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.apiusage.limits; +package org.thingsboard.server.dao.util.limits; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import javax.annotation.PostConstruct; import java.util.concurrent.TimeUnit; @Service -@RequiredArgsConstructor @Slf4j public class DefaultRateLimitService implements RateLimitService { private final TbTenantProfileCache tenantProfileCache; - @Value("${cache.rateLimits.timeToLiveInMinutes:60}") - private int rateLimitsTtl; - @Value("${cache.rateLimits.maxSize:100000}") - private int rateLimitsCacheMaxSize; - private Cache rateLimits; - - @PostConstruct - private void init() { - rateLimits = Caffeine.newBuilder() + public DefaultRateLimitService(TbTenantProfileCache tenantProfileCache, + @Value("${cache.rateLimits.timeToLiveInMinutes:60}") int rateLimitsTtl, + @Value("${cache.rateLimits.maxSize:100000}") int rateLimitsCacheMaxSize) { + this.tenantProfileCache = tenantProfileCache; + this.rateLimits = Caffeine.newBuilder() .expireAfterAccess(rateLimitsTtl, TimeUnit.MINUTES) .maximumSize(rateLimitsCacheMaxSize) .build(); } + private final Cache rateLimits; + @Override public boolean checkRateLimit(LimitedApi api, TenantId tenantId) { return checkRateLimit(api, tenantId, tenantId); } @Override - public boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId) { + public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level) { if (tenantId.isSysTenantId()) { return true; } - RateLimitKey key = new RateLimitKey(api, entityId); + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + if (tenantProfile == null) { + throw new TenantProfileNotFoundException(tenantId); + } + + String rateLimitConfig = tenantProfile.getProfileConfiguration() + .map(profileConfiguration -> api.getLimitConfig(profileConfiguration, level)) + .orElse(null); + return checkRateLimit(api, level, rateLimitConfig); + } - String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration() - .map(api::getLimitConfig).orElse(null); + @Override + public boolean checkRateLimit(LimitedApi api, Object level, String rateLimitConfig) { + RateLimitKey key = new RateLimitKey(api, level); if (StringUtils.isEmpty(rateLimitConfig)) { rateLimits.invalidate(key); return true; } - log.trace("[{}] Checking rate limit for {} ({})", entityId, api, rateLimitConfig); + log.trace("[{}] Checking rate limit for {} ({})", level, api, rateLimitConfig); TbRateLimits rateLimit = rateLimits.asMap().compute(key, (k, limit) -> { if (limit == null || !limit.getConfiguration().equals(rateLimitConfig)) { - limit = new TbRateLimits(rateLimitConfig); - log.trace("[{}] Created new rate limit bucket for {} ({})", entityId, api, rateLimitConfig); + limit = new TbRateLimits(rateLimitConfig, api.isRefillRateLimitIntervally()); + log.trace("[{}] Created new rate limit bucket for {} ({})", level, api, rateLimitConfig); } return limit; }); boolean success = rateLimit.tryConsume(); if (!success) { - log.debug("[{}] Rate limit exceeded for {} ({})", entityId, api, rateLimitConfig); + log.debug("[{}] Rate limit exceeded for {} ({})", level, api, rateLimitConfig); } return success; } + @Override + public void cleanUp(LimitedApi api, Object level) { + RateLimitKey key = new RateLimitKey(api, level); + rateLimits.invalidate(key); + } + @Data(staticConstructor = "of") private static class RateLimitKey { private final LimitedApi api; - private final EntityId entityId; + private final Object level; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java b/dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java new file mode 100644 index 0000000000..ee79230d8a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java @@ -0,0 +1,67 @@ +/** + * 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 configExtractor; + @Getter + private final boolean refillRateLimitIntervally; + + LimitedApi(Function configExtractor) { + this((profileConfiguration, level) -> configExtractor.apply(profileConfiguration)); + } + + LimitedApi(BiFunction 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"); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java b/dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java similarity index 75% rename from application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java rename to dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java index 3fde98618e..c3f2fd179f 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java @@ -13,15 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.apiusage.limits; +package org.thingsboard.server.dao.util.limits; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; public interface RateLimitService { boolean checkRateLimit(LimitedApi api, TenantId tenantId); - boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId); + boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level); + + boolean checkRateLimit(LimitedApi api, Object level, String rateLimitConfig); + + void cleanUp(LimitedApi api, Object level); } From e3b9099bb16f780914aaf973af40e334020e9448 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 17 May 2023 20:43:33 +0200 Subject: [PATCH 2/5] fixed cookie deserialization vulnerability --- .../security/auth/oauth2/CookieUtils.java | 25 ++++++- ...th2AuthorizationRequestRepositoryTest.java | 66 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java index 429e185b3e..4deccb35a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java @@ -15,14 +15,20 @@ */ package org.thingsboard.server.service.security.auth.oauth2; +import lombok.extern.slf4j.Slf4j; import org.springframework.util.SerializationUtils; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; import java.util.Base64; import java.util.Optional; +@Slf4j public class CookieUtils { public static Optional getCookie(HttpServletRequest request, String name) { @@ -67,7 +73,22 @@ public class CookieUtils { } public static T deserialize(Cookie cookie, Class cls) { - return cls.cast(SerializationUtils.deserialize( - Base64.getUrlDecoder().decode(cookie.getValue()))); + byte[] decodedBytes = Base64.getUrlDecoder().decode(cookie.getValue()); + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decodedBytes)) { + @Override + protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { + String name = desc.getName(); + if (!cls.getName().equals(name)) { + throw new ClassNotFoundException("Class not allowed for deserialization: " + name); + } + return super.resolveClass(desc); + } + }) { + + return cls.cast(ois.readObject()); + } catch (Exception e) { + log.debug("Failed to deserialize class from cookie.", e.getCause()); + return null; + } } } diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java new file mode 100644 index 0000000000..3691952b22 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java @@ -0,0 +1,66 @@ +/** + * 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.security.auth.oauth2; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +import static org.junit.Assert.assertEquals; +import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME; + +public class HttpCookieOAuth2AuthorizationRequestRepositoryTest { + + private static final String SERIALIZED_ATTACK_STRING = + "rO0ABXNyAHVvcmcudGhpbmdzYm9hcmQuc2VydmVyLnNlcnZpY2Uuc2VjdXJpdHkuYXV0aC5vYXV0aDIuSHR0cENvb2tpZU9BdXRoMkF1dGhvcml6YXRpb25SZXF1ZXN0UmVwb3NpdG9yeVRlc3QkTWFsaWNpb3VzQ2xhc3MAAAAAAAAAAAIAAHhw"; + + private static int maliciousMethodInvocationCounter; + + @Before + public void resetInvocationCounter() { + maliciousMethodInvocationCounter = 0; + } + + @Test + public void whenLoadAuthorizationRequest_thenMaliciousMethodNotInvoked() { + HttpCookieOAuth2AuthorizationRequestRepository cookieRequestRepo = new HttpCookieOAuth2AuthorizationRequestRepository(); + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Cookie cookie = new Cookie(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, SERIALIZED_ATTACK_STRING); + Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + + cookieRequestRepo.loadAuthorizationRequest(request); + + assertEquals(0, maliciousMethodInvocationCounter); + } + + private static class MaliciousClass implements Serializable { + private static final long serialVersionUID = 0L; + + public void maliciousMethod() { + maliciousMethodInvocationCounter++; + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + maliciousMethod(); + } + } +} From ef735934f291b9df5d70eee98bc48cdc864d8de8 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 18 May 2023 22:25:33 +0200 Subject: [PATCH 3/5] added ability to disable swagger --- .../org/thingsboard/server/config/SwaggerConfiguration.java | 3 +++ application/src/main/resources/thingsboard.yml | 1 + 2 files changed, 4 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 568c08ec56..4415f1a0bf 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -87,6 +87,8 @@ import static springfox.documentation.builders.PathSelectors.regex; @Profile("!test") public class SwaggerConfiguration { + @Value("${swagger.enabled:true}") + private boolean enabled; @Value("${swagger.api_path_regex}") private String apiPathRegex; @Value("${swagger.security_path_regex}") @@ -116,6 +118,7 @@ public class SwaggerConfiguration { public Docket thingsboardApi() { TypeResolver typeResolver = new TypeResolver(); return new Docket(DocumentationType.OAS_30) + .enable(enabled) .groupName("thingsboard") .apiInfo(apiInfo()) .additionalModels( diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 16fc305ed2..f776887287 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -989,6 +989,7 @@ edges: persistToTelemetry: "${EDGES_PERSIST_STATE_TO_TELEMETRY:false}" swagger: + enabled: "${SWAGGER_ENABLED:true}" api_path_regex: "${SWAGGER_API_PATH_REGEX:/api/.*}" security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api/.*}" non_security_path_regex: "${SWAGGER_NON_SECURITY_PATH_REGEX:/api/(?:noauth|v1)/.*}" From 1e9895bdaf1974e46ec01cd8add8f719e0902eb1 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 22 May 2023 15:30:57 +0300 Subject: [PATCH 4/5] UI: Fix invalid widget template errors processing. --- ui-ngx/package.json | 30 +-- ui-ngx/patches/@angular+core+15.2.9.patch | 20 ++ ui-ngx/src/app/core/http/widget.service.ts | 2 +- .../widget/dynamic-widget.component.ts | 1 + .../widget/widget-component.service.ts | 5 + .../components/widget/widget.component.ts | 26 ++- ui-ngx/yarn.lock | 188 +++++++++--------- 7 files changed, 155 insertions(+), 117 deletions(-) create mode 100644 ui-ngx/patches/@angular+core+15.2.9.patch diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 2c29990466..5766cac3ad 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -14,17 +14,17 @@ }, "private": true, "dependencies": { - "@angular/animations": "^15.2.5", - "@angular/cdk": "^15.2.5", - "@angular/common": "^15.2.5", - "@angular/compiler": "^15.2.5", - "@angular/core": "^15.2.5", + "@angular/animations": "^15.2.9", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.2.9", + "@angular/compiler": "^15.2.9", + "@angular/core": "^15.2.9", "@angular/flex-layout": "^15.0.0-beta.42", - "@angular/forms": "^15.2.5", - "@angular/material": "^15.2.5", - "@angular/platform-browser": "^15.2.5", - "@angular/platform-browser-dynamic": "^15.2.5", - "@angular/router": "^15.2.5", + "@angular/forms": "^15.2.9", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.2.9", + "@angular/platform-browser-dynamic": "^15.2.9", + "@angular/router": "^15.2.9", "@auth0/angular-jwt": "^5.1.2", "@date-io/core": "1.3.7", "@date-io/date-fns": "1.3.7", @@ -108,16 +108,16 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "~15.0.0", - "@angular-devkit/build-angular": "^15.2.4", + "@angular-devkit/build-angular": "^15.2.8", "@angular-eslint/builder": "15.2.1", "@angular-eslint/eslint-plugin": "15.2.1", "@angular-eslint/eslint-plugin-template": "15.2.1", "@angular-eslint/schematics": "15.2.1", "@angular-eslint/template-parser": "15.2.1", - "@angular/cli": "^15.2.4", - "@angular/compiler-cli": "^15.2.5", - "@angular/language-service": "^15.2.5", - "@ngtools/webpack": "^15.1.6", + "@angular/cli": "^15.2.8", + "@angular/compiler-cli": "^15.2.9", + "@angular/language-service": "^15.2.9", + "@ngtools/webpack": "15.2.1", "@types/ace-diff": "^2.1.1", "@types/canvas-gauges": "^2.1.4", "@types/flot": "^0.0.32", diff --git a/ui-ngx/patches/@angular+core+15.2.9.patch b/ui-ngx/patches/@angular+core+15.2.9.patch new file mode 100644 index 0000000000..3797e6b48c --- /dev/null +++ b/ui-ngx/patches/@angular+core+15.2.9.patch @@ -0,0 +1,20 @@ +diff --git a/node_modules/@angular/core/fesm2020/core.mjs b/node_modules/@angular/core/fesm2020/core.mjs +index 3e93015..9efcb96 100755 +--- a/node_modules/@angular/core/fesm2020/core.mjs ++++ b/node_modules/@angular/core/fesm2020/core.mjs +@@ -11053,13 +11053,13 @@ function findDirectiveDefMatches(tView, tNode) { + if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) { + matches || (matches = []); + if (isComponentDef(def)) { +- if (ngDevMode) { ++ // if (ngDevMode) { + assertTNodeType(tNode, 2 /* TNodeType.Element */, `"${tNode.value}" tags cannot be used as component hosts. ` + + `Please use a different tag to activate the ${stringify(def.type)} component.`); + if (isComponentHost(tNode)) { + throwMultipleComponentError(tNode, matches.find(isComponentDef).type, def.type); + } +- } ++ // } + // Components are inserted at the front of the matches array so that their lifecycle + // hooks run before any directive lifecycle hooks. This appears to be for ViewEngine + // compatibility. This logic doesn't make sense with host directives, because it diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 2b33057454..5f5f0f42df 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -280,7 +280,7 @@ export class WidgetService { this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID); } - private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { + public deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); this.widgetsInfoInMemoryCache.delete(key); } diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index 4831157536..22fac750b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -106,6 +106,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid } ngOnDestroy(): void { + super.ngOnDestroy(); if (this.ctx.telemetrySubscribers) { this.ctx.telemetrySubscribers.forEach(item => item.unsubscribe()); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 50627212ee..9b73b5566d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -224,6 +224,11 @@ export class WidgetComponentService { ); } + public clearWidgetInfo(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): void { + this.dynamicComponentFactoryService.destroyDynamicComponentFactory(widgetInfo.componentFactory); + this.widgetService.deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); + } + private getWidgetInfoInternal(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable { const widgetInfoSubject = new ReplaySubject(); const widgetInfo = this.widgetService.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 5a3b55bf2d..8424999155 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -516,6 +516,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.onInit(); }, (err) => { + this.widgetContext.inited = true; // console.log(err); } ); @@ -739,9 +740,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.createDefaultSubscription().subscribe( () => { this.subscriptionInited = true; - this.configureDynamicWidgetComponent(); - initSubject.next(); - initSubject.complete(); + try { + this.configureDynamicWidgetComponent(); + initSubject.next(); + initSubject.complete(); + } catch (err) { + initSubject.error(err); + } }, (err) => { this.subscriptionInited = true; @@ -751,9 +756,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } else { this.loadingData = false; this.subscriptionInited = true; - this.configureDynamicWidgetComponent(); - initSubject.next(); - initSubject.complete(); + try { + this.configureDynamicWidgetComponent(); + initSubject.next(); + initSubject.complete(); + } catch (err) { + initSubject.error(err); + } } return initSubject.asObservable(); } @@ -807,12 +816,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector); this.cd.detectChanges(); } catch (e) { - console.error(e); if (this.dynamicWidgetComponentRef) { this.dynamicWidgetComponentRef.destroy(); this.dynamicWidgetComponentRef = null; } this.widgetContentContainer.clear(); + this.handleWidgetException(e); + this.widgetComponentService.clearWidgetInfo(this.widgetInfo, this.widget.bundleAlias, this.widget.typeAlias, + this.widget.isSystemType); + throw e; } if (this.dynamicWidgetComponentRef) { diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 7764dcbe5d..8da9cb382a 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -31,12 +31,12 @@ "@angular-devkit/core" "15.2.1" rxjs "6.6.7" -"@angular-devkit/architect@0.1502.4": - version "0.1502.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1502.4.tgz#a2ceef215f75b03ec275f5ff68e87fc43e44b113" - integrity sha512-bDBcaRMBfXFfK9MpvfNO926F1rL0PEw+mveXxq3/SSql+1XP/hrc5TVGwnoim4g6DqsGmu9upS5DyJ6PnL/hHA== +"@angular-devkit/architect@0.1502.8": + version "0.1502.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1502.8.tgz#9fd3fd27b3a7fc5f8eb65c92500b4d9d15b879e8" + integrity sha512-rTltw2ABHrcKc8EGimALvXmrDTP5hlNbEy6nYolJoXEI9EwHgriWrVLVPs3OEF+/ed47dbJi9EGOXUOgzgpB5A== dependencies: - "@angular-devkit/core" "15.2.4" + "@angular-devkit/core" "15.2.8" rxjs "6.6.7" "@angular-devkit/build-angular@^15.0.0": @@ -107,15 +107,15 @@ optionalDependencies: esbuild "0.17.8" -"@angular-devkit/build-angular@^15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-15.2.4.tgz#ac9230fef0d57d1402796ec0e9be3b3afd90ee51" - integrity sha512-wt0S4oz0vxuW0/Ak5X0vQ7s7TSPynmktVNJblu9SFRgwCD3kplV2B693F+M6t8eLzSy0UCSbZp9h3Ae8gLEiEw== +"@angular-devkit/build-angular@^15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-15.2.8.tgz#5412125b810fee084eb8afc20b9911606ad66170" + integrity sha512-TGDnXhhOG6h6TOrWWzfnkha7wYBOXi7iJc1o1w1VKCayE3T6TZZdF847aK66vL9KG7AKYVdGhWEGw2WBHUBUpg== dependencies: "@ampproject/remapping" "2.2.0" - "@angular-devkit/architect" "0.1502.4" - "@angular-devkit/build-webpack" "0.1502.4" - "@angular-devkit/core" "15.2.4" + "@angular-devkit/architect" "0.1502.8" + "@angular-devkit/build-webpack" "0.1502.8" + "@angular-devkit/core" "15.2.8" "@babel/core" "7.20.12" "@babel/generator" "7.20.14" "@babel/helper-annotate-as-pure" "7.18.6" @@ -127,7 +127,7 @@ "@babel/runtime" "7.20.13" "@babel/template" "7.20.7" "@discoveryjs/json-ext" "0.5.7" - "@ngtools/webpack" "15.2.4" + "@ngtools/webpack" "15.2.8" ansi-colors "4.1.3" autoprefixer "10.4.13" babel-loader "9.1.2" @@ -183,12 +183,12 @@ "@angular-devkit/architect" "0.1502.1" rxjs "6.6.7" -"@angular-devkit/build-webpack@0.1502.4": - version "0.1502.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1502.4.tgz#0fa89e684692d2444d4e7996f1d2c974be2b6e2b" - integrity sha512-Bs/pxcY3517QAVyAalDxJgjc93KWQos+dFdgEQrKxj/VTs1BTYnLbb2M8Y7MoxVnfH4S+qqxGe5B57T+TlB3Eg== +"@angular-devkit/build-webpack@0.1502.8": + version "0.1502.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1502.8.tgz#1b375480deef1b0920e1a63d952795bd33bbfb38" + integrity sha512-jWtNv+S03FFLDe/C8SPCcRvkz3bSb2R+919IT086Q9axIPQ1VowOEwzt2k3qXPSSrC7GSYuASM+X92dB47NTQQ== dependencies: - "@angular-devkit/architect" "0.1502.4" + "@angular-devkit/architect" "0.1502.8" rxjs "6.6.7" "@angular-devkit/core@15.2.1", "@angular-devkit/core@^15.0.0": @@ -202,10 +202,10 @@ rxjs "6.6.7" source-map "0.7.4" -"@angular-devkit/core@15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.2.4.tgz#f7696f09c66d01568a07f0e71672e887fdf57280" - integrity sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg== +"@angular-devkit/core@15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.2.8.tgz#ff494ae7af137b0f0109deb8ee34f1550ed5cc1d" + integrity sha512-Lo4XrbDMtXarKnMrFgWLmQdSX+3QPNAg4otG8cmp/U4jJyjV4dAYKEAsb1sCNGUSM4h4v09EQU/5ugVjDU29lQ== dependencies: ajv "8.12.0" ajv-formats "2.1.1" @@ -213,12 +213,12 @@ rxjs "6.6.7" source-map "0.7.4" -"@angular-devkit/schematics@15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.2.4.tgz#85129ebabcdb362f4b65a6e290bb2ae846f3d64c" - integrity sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw== +"@angular-devkit/schematics@15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.2.8.tgz#c7dfc692e3f54e43085a8845d8c9f390a2519aa3" + integrity sha512-w6EUGC96kVsH9f8sEzajzbONMawezyVBiSo+JYp5r25rQArAz/a+KZntbuETWHQ0rQOEsKmUNKxwmr11BaptSQ== dependencies: - "@angular-devkit/core" "15.2.4" + "@angular-devkit/core" "15.2.8" jsonc-parser "3.2.0" magic-string "0.29.0" ora "5.4.1" @@ -281,31 +281,31 @@ "@angular-eslint/bundled-angular-compiler" "15.2.1" "@typescript-eslint/utils" "5.48.2" -"@angular/animations@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-15.2.5.tgz#d42e5f185ed3d8044d38b1c938c14044aa0345a8" - integrity sha512-areHbwTrJNEYWe+1xND7SEI8fY7Pi2wckuLtbGonjtoaAc8MPs0ov56TnpXHS7bIXKVszqUlBTBAElQSdtjCyw== +"@angular/animations@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-15.2.9.tgz#f0773d2071a5a17c03478d5838029b03bbab9a03" + integrity sha512-GQujLhI0cQFcl4Q8y0oSYKSRnW23GIeSL+Arl4eFufziJ9hGAAQNuesaNs/7i+9UlTHDMkPH3kd5ScXuYYz6wg== dependencies: tslib "^2.3.0" -"@angular/cdk@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-15.2.5.tgz#53208ea5e654b678af011b3ef6034e1382339630" - integrity sha512-6/vA4xATcx//AULwXnRRH0Of0aaNlfrFxdlhvRVfnfmxi/hZAkjjEtil/wv8uBw+dAwXBx5yZMsAol9nUFS/BQ== +"@angular/cdk@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-15.2.9.tgz#e22df07b296fec6dccf66d569c3acc3c504c2058" + integrity sha512-koaM07N1AIQ5oHU27l0/FoQSSoYAwlAYwVZ4Di3bYrJsTBNCN2Xsby7wI8gZxdepMnV4Fe9si382BDBov+oO4Q== dependencies: tslib "^2.3.0" optionalDependencies: parse5 "^7.1.2" -"@angular/cli@^15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-15.2.4.tgz#5163efb3f3e5b2a8ab0598d46d4dc200a51e7197" - integrity sha512-nQFnrt9aWwqkyhJMaZvjHXXuxeR1pZNcpZEzTA5nXtpVeNs8U7EBFCyJ+cYecFX0LHe36SoxXcbfnNEKHt3NVQ== +"@angular/cli@^15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-15.2.8.tgz#612ffd69591aea0109db0a6dd8faec8044a4b80d" + integrity sha512-3VlTfm6DUZfFHBY43vQSAaqmFTxy3VtRd/iDBCHcEPhHwYLWBvNwReJuJfNja8O105QQ6DBiYVBExEBtPmjQ4w== dependencies: - "@angular-devkit/architect" "0.1502.4" - "@angular-devkit/core" "15.2.4" - "@angular-devkit/schematics" "15.2.4" - "@schematics/angular" "15.2.4" + "@angular-devkit/architect" "0.1502.8" + "@angular-devkit/core" "15.2.8" + "@angular-devkit/schematics" "15.2.8" + "@schematics/angular" "15.2.8" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.3" ini "3.0.1" @@ -321,17 +321,17 @@ symbol-observable "4.0.0" yargs "17.6.2" -"@angular/common@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-15.2.5.tgz#7ae960f17c260a92e939ad82fb115d908094aeda" - integrity sha512-6Wl1ak96NZvnL8p0eVsxHhaddv0/qYq2pQniKaKBfL9MVOQtAe07sPUDEZ6w0ApkmV63Giu4zFakaQMsmvxU0Q== +"@angular/common@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-15.2.9.tgz#5e1d47ce831935bcf545b172f88307aedacf1535" + integrity sha512-LM9/UHG2dRrOzlu2KovrFwWIziFMjRxHzSP3Igw6Symw/wIl0kXGq8Fn6RpFP78zmLqnv+IQOoRiby9MCXsI4g== dependencies: tslib "^2.3.0" -"@angular/compiler-cli@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-15.2.5.tgz#3c1882cc837a71fd0f93b880bfec6e851ec213e8" - integrity sha512-NNrnvL0JMg6dZMuTgBTb/IVIFJwy2my5XTNLngTNBfDCVu0SL1TKrYnpqp2qRi2ip9XdNqCtNF7JFDEklVbK2A== +"@angular/compiler-cli@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-15.2.9.tgz#d9e6013d6a8658e4a210aca7997e70d06f6976a8" + integrity sha512-zsbI8G2xHOeYWI0hjFzrI//ZhZV9il/uQW5dAimfwJp06KZDeXZ3PdwY9JQslf6F+saLwOObxy6QMrIVvfjy9w== dependencies: "@babel/core" "7.19.3" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -344,17 +344,17 @@ tslib "^2.3.0" yargs "^17.2.1" -"@angular/compiler@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-15.2.5.tgz#4902db0699fd9ff8af15a3cd0d8be9aacef09b7f" - integrity sha512-0mAFF6Ud4ojsb8qGLQCWYh/LgKKrnn1Fz788LzfYcLYhi7UZPiCFrQJrEcCXBKtQsk8sG335CA9Qk0I/qc35zg== +"@angular/compiler@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-15.2.9.tgz#3f55e206b0e380c28336d2a233b7132f21d72644" + integrity sha512-MoKugbjk+E0wRBj12uvIyDLELlVLonnqjA2+XiF+7FxALIeyds3/qQeEoMmYIqAbN3NnTT5pV92RxWwG4tHFwA== dependencies: tslib "^2.3.0" -"@angular/core@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-15.2.5.tgz#761d339551a3daf97d9b9786882a5dd42ee19445" - integrity sha512-GtmjJmwtzCuF4FDXIY+9UFMAcfPlJzJDBDF7mgEmD2YKf/HV5PSyU91lfv9yDPnAkdzlDAL9u4YxnGgoURY8/g== +"@angular/core@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-15.2.9.tgz#7cb12cc83fcc92f23196ceac82e07b67b2e02203" + integrity sha512-w46Z1yUXCQfKV7XfnamOoLA2VD0MVUUYVrUjO73mHSskDXSXxfZAEHO9kfUS71Cj35PvhP3mbkqWscpea2WeYg== dependencies: tslib "^2.3.0" @@ -365,22 +365,22 @@ dependencies: tslib "^2.3.0" -"@angular/forms@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-15.2.5.tgz#e40cf0bc27eae6e7ae1409ff618a3fe7cea19cfc" - integrity sha512-F4YLh0S1VyWp4IyfORTpzkZJcg7nB4q/wIirVZocR9sK3FJMEKYcAGwLbkWDYGZ66UCDw7EEqCTXkjr99/6sPg== +"@angular/forms@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-15.2.9.tgz#c3b4b0108f4eb4966ddc5a7ec9913c2ca2c94f00" + integrity sha512-sk0pC2EFi2Ohg5J0q0NYptbT+2WOkoiERSMYA39ncDvlSZBWsNlxpkbGUSck7NIxjK2QfcVN1ldGbHlZTFvtqg== dependencies: tslib "^2.3.0" -"@angular/language-service@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-15.2.5.tgz#fff42544e1dad609293a33ae93371a11e2df0c44" - integrity sha512-knXe4D0gc2KiU3Y3LZCRvcIkddxUMHI6/44/QAGZD3UDx6ZnpizS0eb8q8AO1pWiBTUY5ETUAlihaGWqDgQ+Wg== +"@angular/language-service@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-15.2.9.tgz#7a94e3394093a425c757f7b385b4a94edb09178a" + integrity sha512-B7lP4q/eHge2lZezOXS96EYzVf4stMCWfOnz7+pUUi0HbF+A5QCV65SWQddS/M+NM2jj8N2L/j+6UCH8lJjTQA== -"@angular/material@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-15.2.5.tgz#a0531b629334dfd2997dd01e549bd0c40680f4e9" - integrity sha512-Lk6l1VsBA2ActVN9YsX2W5lVFYHJEVwswbbUdBPjWaXJzp7TdgghTFczac7vsNA6y3DM3Rd+CvRsJPD2kK4g4A== +"@angular/material@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-15.2.9.tgz#d49f18660053bf9eae008e4738ebc65334a28491" + integrity sha512-emuFF/7+91Jq+6kVCl3FiVoFLtAZoh+woFQWNuK8nhx0HmD4ckLFI8d9a6ERYR3zRuKhq5deSRE2kYsfpjrrsQ== dependencies: "@material/animation" "15.0.0-canary.684e33d25.0" "@material/auto-init" "15.0.0-canary.684e33d25.0" @@ -431,24 +431,24 @@ "@material/typography" "15.0.0-canary.684e33d25.0" tslib "^2.3.0" -"@angular/platform-browser-dynamic@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.5.tgz#f715dafd227606142295fd717a7c3fb0b67ae79f" - integrity sha512-NoGVeaR7K+RMcLpS2gI4hyMVeUqP057rw8Yfk15dHy3cM8icj5zVfyez3AADcO1XthNhE1sI1d+2LD4/GxwIKQ== +"@angular/platform-browser-dynamic@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.9.tgz#aa31ba63d535ee49fdf3a60fe771503565b4e3c9" + integrity sha512-ZIYDM6MShblb8OyV1m4+18lJJ2LCeICmeg2uSbpFYptYBSOClrTiYOOFVDJvn7HLvNzljLs16XPrgyaYVqNpcw== dependencies: tslib "^2.3.0" -"@angular/platform-browser@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-15.2.5.tgz#b5e1a4da920577d147f162463b2448588543a172" - integrity sha512-D19HL3UsvPX4s8yC4C4gphTNyQU75VuzcyGZ+6y1o1SQbVjF6UwSrztmb//4MCkpclb+avv669z5AEiTuBTnVQ== +"@angular/platform-browser@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-15.2.9.tgz#9150645843cc18b084fb5bf7025e6e320c2abe1e" + integrity sha512-ufCHeSX+U6d43YOMkn3igwfqtlozoCXADcbyfUEG8m2y9XASobqmCKvdSk/zfl62oyiA8msntWBJVBE2l4xKXg== dependencies: tslib "^2.3.0" -"@angular/router@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-15.2.5.tgz#879839960d9a3d52a20cdffc51062e5d288c3440" - integrity sha512-NSEfx2wgPCS8EdXLbKUjNCXC29cMqEGDUrSv8+P/O+tY21Szkqp6uzGTOZJuSTOoKQmDEw9CRmuwxMKm2l6y/Q== +"@angular/router@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-15.2.9.tgz#c3879be22bda236eacf97a18a1e8619b51a53d47" + integrity sha512-UCbh5DLSDhybv0xKYT7kGQMfOVdyhHIHOZz5EYVebbhste6S+W1LE57vTHq7QtxJsyKBa/WSkaUkCLXD6ntCAg== dependencies: tslib "^2.3.0" @@ -2619,15 +2619,15 @@ dependencies: tslib "^2.0.0" -"@ngtools/webpack@15.2.1", "@ngtools/webpack@^15.1.6": +"@ngtools/webpack@15.2.1": version "15.2.1" resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.1.tgz#439ac075b2dcb9f304f0b7009182cc5049c7988a" integrity sha512-YtA8rWAglPuf4CSStrFAxaprTSYE+DREGrJFc3WvZLcF5XrwVK+H4CC4Pmz07iYsG1TXShR4bWp1fbGw1cmBKw== -"@ngtools/webpack@15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.4.tgz#bb8afe2800e79120985657180d2b33f64ecd763d" - integrity sha512-cQ7MsRoGJgPOVnpvFgWhygeSe6zJ0ITiUhjmmuOgpNDfYkrgYxN3Ot/qvQefFei+oGZ1JJ9bRb8lcPKL/apoBQ== +"@ngtools/webpack@15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.8.tgz#df8fb9300ccf94cab8f8ad69fb16fd31181e6c82" + integrity sha512-BJexeT4FxMtToVBGa3wdl6rrkYXgilP0kkSH4Qzu4MPlLPbeBSr4XQalQriewlpC2uzG0r2SJfrAe2eDhtSykA== "@ngx-translate/core@^14.0.0": version "14.0.0" @@ -2733,13 +2733,13 @@ read-package-json-fast "^3.0.0" which "^3.0.0" -"@schematics/angular@15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-15.2.4.tgz#b61b9cb3becf601e904f60490846c1d95bd63700" - integrity sha512-P9axwKDXEDrGXYcoCuhwv4guhvtIjDCvyZMsQw8IMpfAacTYajka+T3mZrzyTe96Uhu0JwcAW5iUlOZAJhPEdQ== +"@schematics/angular@15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-15.2.8.tgz#d845903f1cc477d299f968eb5bc40a9855cfd911" + integrity sha512-F49IEzCFxQlpaMIgTO/wF1l/CLQKif7VaiDdyiTKOeT22IMmyd61FUmWDyZYfCBqMlvBmvDGx64HaHWes1HYCg== dependencies: - "@angular-devkit/core" "15.2.4" - "@angular-devkit/schematics" "15.2.4" + "@angular-devkit/core" "15.2.8" + "@angular-devkit/schematics" "15.2.8" jsonc-parser "3.2.0" "@socket.io/component-emitter@~3.1.0": From 21a49ff2d5c8e5c232682bf6a71ac02eb3d27eed Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 22 May 2023 15:48:20 +0300 Subject: [PATCH 5/5] UI: Angular 15 migration fix --- .../app/shared/components/widgets-bundle-search.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html b/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html index 4790d7e93b..a1b63e76c9 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html +++ b/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html @@ -23,7 +23,7 @@ (focus)="toggleFocus()" (blur)="toggleFocus()" [placeholder]=placeholder> -