diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index 985dea197c..3776eebc12 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -609,4 +609,4 @@ BEGIN END $$; --- TTL DROP PARTITIONS FUNCTIONS UPDATE END \ No newline at end of file +-- TTL DROP PARTITIONS FUNCTIONS UPDATE END diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 34f065d500..8a892b91a9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -103,7 +103,6 @@ import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.data.settings.UserDashboardAction; import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -130,12 +129,12 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.dao.user.UserSettingsService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; @@ -164,7 +163,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import javax.mail.MessagingException; import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolation; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -395,16 +396,19 @@ public abstract class BaseController { * Handles validation error for controller method arguments annotated with @{@link javax.validation.Valid} * */ @ExceptionHandler(MethodArgumentNotValidException.class) - public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) { - String errorMessage = "Validation error: " + e.getFieldErrors().stream() + public void handleValidationError(MethodArgumentNotValidException validationError, HttpServletResponse response) { + List> constraintsViolations = validationError.getFieldErrors().stream() .map(fieldError -> { - String property = fieldError.getField(); - if (property.equals("valid") || StringUtils.endsWith(property, ".valid")) { // when custom @AssertTrue is used - property = ""; + try { + return (ConstraintViolation) fieldError.unwrap(ConstraintViolation.class); + } catch (Exception e) { + log.warn("FieldError source is not of type ConstraintViolation"); + return null; // should not happen } - return (!property.isEmpty() ? (property + " ") : "") + fieldError.getDefaultMessage(); }) - .collect(Collectors.joining(", ")); + .filter(Objects::nonNull) + .collect(Collectors.toList()); + String errorMessage = "Validation error: " + ConstraintValidator.getErrorMessage(constraintsViolations); ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS); handleControllerException(thingsboardException, response); } diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index 6c27887e1a..23aaba0e48 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -64,7 +64,9 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import javax.validation.Valid; +import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -201,9 +203,6 @@ public class NotificationController extends BaseController { public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request, @RequestParam(defaultValue = "20") int recipientsPreviewSize, @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { - NotificationRequestPreview preview = new NotificationRequestPreview(); - - request.setOriginatorEntityId(user.getId()); NotificationTemplate template; if (request.getTemplateId() != null) { template = checkEntityId(request.getTemplateId(), notificationTemplateService::findNotificationTemplateById, Operation.READ); @@ -213,33 +212,23 @@ public class NotificationController extends BaseController { if (template == null) { throw new IllegalArgumentException("Template is missing"); } - NotificationProcessingContext tmpProcessingCtx = NotificationProcessingContext.builder() - .tenantId(user.getTenantId()) - .request(request) - .template(template) - .settings(null) - .build(); + request.setOriginatorEntityId(user.getId()); + List targets = request.getTargets().stream() + .map(NotificationTargetId::new) + .map(targetId -> notificationTargetService.findNotificationTargetById(user.getTenantId(), targetId)) + .sorted(Comparator.comparing(target -> target.getConfiguration().getType())) + .collect(Collectors.toList()); - Map processedTemplates = tmpProcessingCtx.getDeliveryMethods().stream() - .collect(Collectors.toMap(m -> m, deliveryMethod -> { - NotificationRecipient recipient = null; - if (NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods().contains(deliveryMethod)) { - recipient = userService.findUserById(user.getTenantId(), user.getId()); - } - return tmpProcessingCtx.getProcessedTemplate(deliveryMethod, recipient); - })); - preview.setProcessedTemplates(processedTemplates); + NotificationRequestPreview preview = new NotificationRequestPreview(); - // generic permission Set recipientsPreview = new LinkedHashSet<>(); - Map recipientsCountByTarget = new HashMap<>(); - - List targets = notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), - request.getTargets().stream().map(NotificationTargetId::new).collect(Collectors.toList())); + Map recipientsCountByTarget = new LinkedHashMap<>(); + Map firstRecipient = new HashMap<>(); for (NotificationTarget target : targets) { int recipientsCount; List recipientsPart; - if (target.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) { + NotificationTargetType targetType = target.getConfiguration().getType(); + if (targetType == NotificationTargetType.PLATFORM_USERS) { PageData recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize)); recipientsCount = (int) recipients.getTotalElements(); @@ -248,7 +237,7 @@ public class NotificationController extends BaseController { recipientsCount = 1; recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation()); } - + firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null); for (NotificationRecipient recipient : recipientsPart) { if (recipientsPreview.size() < recipientsPreviewSize) { recipientsPreview.add(recipient.getTitle()); @@ -258,11 +247,26 @@ public class NotificationController extends BaseController { } recipientsCountByTarget.put(target.getName(), recipientsCount); } - preview.setRecipientsPreview(recipientsPreview); preview.setRecipientsCountByTarget(recipientsCountByTarget); preview.setTotalRecipientsCount(recipientsCountByTarget.values().stream().mapToInt(Integer::intValue).sum()); + Set deliveryMethods = template.getConfiguration().getDeliveryMethodsTemplates().entrySet() + .stream().filter(entry -> entry.getValue().isEnabled()).map(Map.Entry::getKey).collect(Collectors.toSet()); + NotificationProcessingContext ctx = NotificationProcessingContext.builder() + .tenantId(user.getTenantId()) + .request(request) + .deliveryMethods(deliveryMethods) + .template(template) + .settings(null) + .build(); + Map processedTemplates = ctx.getDeliveryMethods().stream() + .collect(Collectors.toMap(m -> m, deliveryMethod -> { + NotificationTargetType targetType = NotificationTargetType.forDeliveryMethod(deliveryMethod); + return ctx.getProcessedTemplate(deliveryMethod, firstRecipient.get(targetType)); + })); + preview.setProcessedTemplates(processedTemplates); + return preview; } diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java index 4fd6b93022..6374b65710 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java @@ -71,7 +71,7 @@ public class NotificationTargetController extends BaseController { private final NotificationTargetService notificationTargetService; @ApiOperation(value = "Save notification target (saveNotificationTarget)", - notes = "Create or update notification target.\n\n" + + notes = "Create or update notification target." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PostMapping("/target") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index ce6537fd3f..05f56e93bd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -75,16 +75,15 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.transport.adaptor.JsonConverter; -import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.exception.InvalidParametersException; +import org.thingsboard.server.exception.UncheckedApiException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.telemetry.AttributeData; import org.thingsboard.server.service.telemetry.TsData; -import org.thingsboard.server.exception.InvalidParametersException; -import org.thingsboard.server.exception.UncheckedApiException; import javax.annotation.Nullable; import javax.annotation.PostConstruct; @@ -624,7 +623,6 @@ public class TelemetryController extends BaseController { } if (json.isObject()) { List attributes = extractRequestAttributes(json); - attributes.forEach(ConstraintValidator::validateFields); if (attributes.isEmpty()) { return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java index e995a8bd1f..9a0282c840 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java @@ -15,54 +15,81 @@ */ package org.thingsboard.server.service.apiusage.limits; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.Data; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.PostConstruct; +import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor +@Slf4j public class DefaultRateLimitService implements RateLimitService { private final TbTenantProfileCache tenantProfileCache; + @Value("${cache.rateLimits.timeToLiveInMinutes:60}") + private int rateLimitsTtl; + @Value("${cache.rateLimits.maxSize:100000}") + private int rateLimitsCacheMaxSize; - private final Map> rateLimits = new ConcurrentHashMap<>(); + private Cache rateLimits; + + @PostConstruct + private void init() { + rateLimits = Caffeine.newBuilder() + .expireAfterAccess(rateLimitsTtl, TimeUnit.MINUTES) + .maximumSize(rateLimitsCacheMaxSize) + .build(); + } + + @Override + public boolean checkRateLimit(LimitedApi api, TenantId tenantId) { + return checkRateLimit(api, tenantId, tenantId); + } @Override - public boolean checkRateLimit(TenantId tenantId, LimitedApi api) { + public boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId) { if (tenantId.isSysTenantId()) { return true; } + RateLimitKey key = new RateLimitKey(api, entityId); + String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration() .map(api::getLimitConfig).orElse(null); - - Map rateLimits = this.rateLimits.get(api); if (StringUtils.isEmpty(rateLimitConfig)) { - if (rateLimits != null) { - rateLimits.remove(tenantId); - if (rateLimits.isEmpty()) { - this.rateLimits.remove(api); - } - } + rateLimits.invalidate(key); return true; } + log.trace("[{}] Checking rate limit for {} ({})", entityId, api, rateLimitConfig); - if (rateLimits == null) { - rateLimits = new ConcurrentHashMap<>(); - this.rateLimits.put(api, rateLimits); - } - TbRateLimits rateLimit = rateLimits.get(tenantId); - if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) { - rateLimit = new TbRateLimits(rateLimitConfig); - rateLimits.put(tenantId, rateLimit); + TbRateLimits rateLimit = rateLimits.asMap().compute(key, (k, limit) -> { + if (limit == null || !limit.getConfiguration().equals(rateLimitConfig)) { + limit = new TbRateLimits(rateLimitConfig); + log.trace("[{}] Created new rate limit bucket for {} ({})", entityId, api, rateLimitConfig); + } + return limit; + }); + boolean success = rateLimit.tryConsume(); + if (!success) { + log.debug("[{}] Rate limit exceeded for {} ({})", entityId, api, rateLimitConfig); } + return success; + } - return rateLimit.tryConsume(); + @Data(staticConstructor = "of") + private static class RateLimitKey { + private final LimitedApi api; + private final EntityId entityId; } } 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 index f69ece6661..4f216ffaca 100644 --- 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 @@ -25,7 +25,8 @@ public enum LimitedApi { ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit), ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit), - NOTIFICATION_REQUEST(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit); + NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit), + NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit); private final Function configExtractor; diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java index c984ec8fa5..3fde98618e 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java @@ -15,10 +15,13 @@ */ package org.thingsboard.server.service.apiusage.limits; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; public interface RateLimitService { - boolean checkRateLimit(TenantId tenantId, LimitedApi api); + boolean checkRateLimit(LimitedApi api, TenantId tenantId); + + boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId); } diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index 80b51f7ad5..beecebe2ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -20,15 +20,16 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -36,15 +37,16 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.device.DeviceCredentialsService; -import org.thingsboard.server.dao.device.DeviceDao; -import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; @@ -59,12 +61,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.state.DeviceStateService; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Service @@ -77,35 +80,56 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { private static final String DEVICE_PROVISION_STATE = "provisionState"; private static final String PROVISIONED_STATE = "provisioned"; - @Autowired - TbClusterService clusterService; + private final TbClusterService clusterService; + private final DeviceProfileService deviceProfileService; + private final DeviceService deviceService; + private final DeviceCredentialsService deviceCredentialsService; + private final AttributesService attributesService; + private final AuditLogService auditLogService; + private final PartitionService partitionService; - @Autowired - DeviceDao deviceDao; - - @Autowired - DeviceProfileDao deviceProfileDao; - - @Autowired - DeviceService deviceService; - - @Autowired - DeviceCredentialsService deviceCredentialsService; - - @Autowired - AttributesService attributesService; - - @Autowired - DeviceStateService deviceStateService; - - @Autowired - AuditLogService auditLogService; - - @Autowired - PartitionService partitionService; - - public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) { + public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider, TbClusterService clusterService, DeviceProfileService deviceProfileService, DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, AttributesService attributesService, AuditLogService auditLogService, PartitionService partitionService) { ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); + this.clusterService = clusterService; + this.deviceProfileService = deviceProfileService; + this.deviceService = deviceService; + this.deviceCredentialsService = deviceCredentialsService; + this.attributesService = attributesService; + this.auditLogService = auditLogService; + this.partitionService = partitionService; + } + + @Override + public ProvisionResponse provisionDeviceViaX509Chain(DeviceProfile targetProfile, ProvisionRequest provisionRequest) throws ProvisionFailedException { + if (targetProfile == null) { + throw new ProvisionFailedException("Device profile is not specified!"); + } + if (!DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(targetProfile.getProfileData().getProvisionConfiguration().getType())) { + throw new ProvisionFailedException("Device profile provision strategy is not X509_CERTIFICATE_CHAIN!"); + } + X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration(); + String certificateValue = provisionRequest.getCredentialsData().getX509CertHash(); + String certificateRegEx = configuration.getCertificateRegExPattern(); + String commonName = getCNFromX509Certificate(targetProfile, certificateValue); + String deviceName = extractDeviceNameFromCNByRegEx(targetProfile, commonName, certificateRegEx); + provisionRequest.setDeviceName(deviceName); + Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName()); + X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration(); + if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(targetDevice.getTenantId(), targetDevice.getId()); + if (DeviceCredentialsType.X509_CERTIFICATE.equals(deviceCredentials.getCredentialsType())) { + String updatedDeviceCertificateValue = provisionRequest.getCredentialsData().getX509CertHash(); + deviceCredentials = updateDeviceCredentials(targetDevice.getTenantId(), deviceCredentials, + updatedDeviceCertificateValue, DeviceCredentialsType.X509_CERTIFICATE); + } + return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS); + } else if (x509Configuration.isAllowCreateNewDevicesByX509Certificate()) { + return createDevice(provisionRequest, targetProfile); + } else { + log.warn("[{}][{}] Device with name {} doesn't exist and cannot be created due incorrect configuration for X509CertificateChainProvisionConfiguration", + targetProfile.getTenantId(), targetProfile.getId(), provisionRequest.getDeviceName()); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } } @Override @@ -124,14 +148,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); } - DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey); + DeviceProfile targetProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequestKey); if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null || targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) { throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); } - Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null); + Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName()); switch (targetProfile.getProvisionType()) { case ALLOW_CREATE_NEW_DEVICES: @@ -155,6 +179,8 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { } } break; + case X509_CERTIFICATE_CHAIN: + throw new ProvisionFailedException("Invalid provision strategy type!"); } throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); } @@ -209,6 +235,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { } } + private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue, + DeviceCredentialsType credentialsType) { + log.trace("Updating device credentials [{}] with certificate value [{}]", deviceCredentials, certificateValue); + deviceCredentials.setCredentialsValue(certificateValue); + deviceCredentials.setCredentialsType(credentialsType); + return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + } + private ListenableFuture> saveProvisionStateAttribute(Device device) { return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), @@ -257,4 +291,27 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE; auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest); } + + private String getCNFromX509Certificate(DeviceProfile profile, String x509Value) { + try { + return SslUtil.parseCommonName(SslUtil.readCertFile(x509Value)); + } catch (Exception e) { + log.trace("[{}][{}] Failed to parse CN from X509 certificate {}", profile.getTenantId(), profile.getId(), x509Value); + return null; + } + } + + public String extractDeviceNameFromCNByRegEx(DeviceProfile profile, String commonName, String regex) throws ProvisionFailedException { + try { + log.trace("Extract device name from CN [{}] by regex pattern [{}]", commonName, regex); + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(commonName); + if (matcher.find()) { + return matcher.group(1); + } + } catch (Exception ignored) {} + log.trace("[{}][{}] Failed to match device name using [{}] from CN: [{}]", profile.getTenantId(), profile.getId(), regex, commonName); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index b6ed5eb664..f273517b74 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -98,11 +98,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private String privateKeyResource; @Value("${edges.state.persistToTelemetry:false}") private boolean persistToTelemetry; - @Value("${edges.rpc.client_max_keep_alive_time_sec}") + @Value("${edges.rpc.client_max_keep_alive_time_sec:1}") private int clientMaxKeepAliveTimeSec; @Value("${edges.rpc.max_inbound_message_size:4194304}") private int maxInboundMessageSize; - + @Value("${edges.rpc.keep_alive_time_sec:10}") + private int keepAliveTimeSec; + @Value("${edges.rpc.keep_alive_timeout_sec:5}") + private int keepAliveTimeoutSec; @Value("${edges.scheduler_pool_size}") private int schedulerPoolSize; @@ -131,6 +134,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i log.info("Initializing Edge RPC service!"); NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort) .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS) + .permitKeepAliveWithoutCalls(true) .maxInboundMessageSize(maxInboundMessageSize) .addService(this); if (sslEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index bd840b2f52..1e54d8d48f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -82,6 +82,9 @@ public class DefaultCacheCleanupService implements CacheCleanupService { log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ..."); clearCacheByName("repositorySettings"); break; + case "3.4.4": + log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0"); + clearCacheByName("deviceProfiles"); default: //Do nothing, since cache cleanup is optional. } 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 b4fba04212..91bc0863db 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 @@ -107,8 +107,10 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple @Override public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest request, Consumer callback) { - if (!rateLimitService.checkRateLimit(tenantId, LimitedApi.NOTIFICATION_REQUEST)) { - throw new TbRateLimitsException(EntityType.TENANT); + if (request.getRuleId() == null) { + if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS, tenantId)) { + throw new TbRateLimitsException(EntityType.TENANT); + } } NotificationTemplate notificationTemplate; @@ -119,21 +121,33 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple } if (notificationTemplate == null) throw new IllegalArgumentException("Template is missing"); + Set deliveryMethods = new HashSet<>(); List targets = request.getTargets().stream().map(NotificationTargetId::new) - .map(id -> notificationTargetService.findNotificationTargetById(tenantId, id)).collect(Collectors.toList()); + .map(id -> notificationTargetService.findNotificationTargetById(tenantId, id)) + .collect(Collectors.toList()); NotificationRuleId ruleId = request.getRuleId(); notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> { if (!template.isEnabled()) return; - if (!channels.get(deliveryMethod).check(tenantId)) { - throw new IllegalArgumentException("Unable to send notification via " + deliveryMethod.getName() + ": not configured or not working"); + try { + channels.get(deliveryMethod).check(tenantId); + } catch (Exception e) { + if (ruleId == null) { + throw new IllegalArgumentException(e.getMessage()); + } else { + return; // if originated by rule - just ignore delivery method + } } if (ruleId == null) { if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) { - throw new IllegalArgumentException("Target for " + deliveryMethod.getName() + " delivery method is missing"); + throw new IllegalArgumentException("Recipients for " + deliveryMethod.getName() + " delivery method not chosen"); } } + deliveryMethods.add(deliveryMethod); }); + if (deliveryMethods.isEmpty()) { + throw new IllegalArgumentException("No delivery methods to send notification with"); + } if (request.getAdditionalConfig() != null) { NotificationRequestConfig config = request.getAdditionalConfig(); @@ -153,6 +167,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple NotificationProcessingContext ctx = NotificationProcessingContext.builder() .tenantId(tenantId) .request(request) + .deliveryMethods(deliveryMethods) .template(notificationTemplate) .settings(settings) .build(); @@ -341,14 +356,20 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple @Override public Set getAvailableDeliveryMethods(TenantId tenantId) { return channels.values().stream() - .filter(channel -> channel.check(tenantId)) + .filter(channel -> { + try { + channel.check(tenantId); + return true; + } catch (Exception e) { + return false; + } + }) .map(NotificationChannel::getDeliveryMethod) .collect(Collectors.toSet()); } @Override - public boolean check(TenantId tenantId) { - return true; + public void check(TenantId tenantId) throws Exception { } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java index 8f9b7d048b..25ff1dd50e 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java @@ -48,20 +48,21 @@ public class NotificationProcessingContext { private final NotificationSettings settings; @Getter private final NotificationRequest request; - + @Getter + private final Set deliveryMethods; @Getter private final NotificationTemplate notificationTemplate; + private final Map templates; @Getter - private Set deliveryMethods; - @Getter private final NotificationRequestStats stats; - @Builder - public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, NotificationTemplate template, NotificationSettings settings) { + public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, Set deliveryMethods, + NotificationTemplate template, NotificationSettings settings) { this.tenantId = tenantId; this.request = request; + this.deliveryMethods = deliveryMethods; this.settings = settings; this.notificationTemplate = template; this.templates = new EnumMap<>(NotificationDeliveryMethod.class); @@ -77,7 +78,6 @@ public class NotificationProcessingContext { templates.put(deliveryMethod, template); } }); - deliveryMethods = templates.keySet(); } public C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) { diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java index 61e4c528dc..f5bbc57954 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java @@ -48,12 +48,11 @@ public class EmailNotificationChannel implements NotificationChannel sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx); - boolean check(TenantId tenantId); + void check(TenantId tenantId) throws Exception; NotificationDeliveryMethod getDeliveryMethod(); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java index 7ff48d4cb9..46afbd7270 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java @@ -22,12 +22,12 @@ import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; -import org.thingsboard.server.dao.notification.NotificationSettingsService; -import org.thingsboard.server.service.notification.NotificationProcessingContext; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.service.executors.ExternalCallExecutorService; +import org.thingsboard.server.service.notification.NotificationProcessingContext; @Component @RequiredArgsConstructor @@ -47,9 +47,11 @@ public class SlackNotificationChannel implements NotificationChannel { submitNotificationRequest(targets, rule, trigger.getOriginatorEntityId(), notificationInfo, delay); diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 1dae050f58..8e00c837f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -67,6 +67,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.resource.ResourceService; @@ -575,7 +576,7 @@ public class AccessValidator { ResponseEntity responseEntity; if (e instanceof ToErrorResponseEntity) { responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); - } else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException) { + } else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException || e instanceof DataValidationException) { responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } else { responseEntity = new ResponseEntity<>(defaultErrorStatus); 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 b646cb7ef6..793c25d625 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 @@ -73,7 +73,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS @Override public , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx ctx, I entityId) throws ThingsboardException { - if (!rateLimitService.checkRateLimit(ctx.getTenantId(), LimitedApi.ENTITY_EXPORT)) { + if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) { throw new ThingsboardException("Rate limit for entities export is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); } @@ -85,7 +85,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS @Override public , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException { - if (!rateLimitService.checkRateLimit(ctx.getTenantId(), LimitedApi.ENTITY_IMPORT)) { + if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_IMPORT, ctx.getTenantId())) { throw new ThingsboardException("Rate limit for entities import is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); } if (exportData.getEntity() == null || exportData.getEntity().getId() == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index f72ac8a11f..610f1ee0b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; -import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; @@ -87,7 +86,6 @@ public abstract class BaseEntityImportService importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException { EntityImportResult importResult = new EntityImportResult<>(); @@ -336,27 +334,37 @@ public abstract class BaseEntityImportService externalIdOpt = buildEntityId(entityType, externalUuid); - if (!externalIdOpt.isPresent()) { + Optional externalId = buildEntityId(entityType, externalUuid); + if (externalId.isEmpty()) { continue; } - EntityId internalId = ctx.getInternalId(externalIdOpt.get()); + EntityId internalId = ctx.getInternalId(externalId.get()); if (internalId != null) { return Optional.of(internalId); } } if (fetchAllUUIDs) { - for (EntityType entityType : hints) { - Optional internalId = lookupInDb(externalUuid, entityType); - if (internalId.isPresent()) return internalId; - } + Set processLast = Set.of(EntityType.TENANT); + List entityTypes = new ArrayList<>(hints); for (EntityType entityType : EntityType.values()) { - if (hints.contains(entityType)) { + if (!hints.contains(entityType) && !processLast.contains(entityType)) { + entityTypes.add(entityType); + } + } + entityTypes.addAll(processLast); + + for (EntityType entityType : entityTypes) { + Optional externalId = buildEntityId(entityType, externalUuid); + if (externalId.isEmpty() || ctx.isNotFound(externalId.get())) { continue; } - Optional internalId = lookupInDb(externalUuid, entityType); - if (internalId.isPresent()) return internalId; + EntityId internalId = getInternalId(externalId.get(), false); + if (internalId != null) { + return Optional.of(internalId); + } else { + ctx.registerNotFound(externalId.get()); + } } } @@ -364,20 +372,6 @@ public abstract class BaseEntityImportService lookupInDb(UUID externalUuid, EntityType entityType) { - Optional externalIdOpt = buildEntityId(entityType, externalUuid); - if (externalIdOpt.isEmpty() || ctx.isNotFound(externalIdOpt.get())) { - return Optional.empty(); - } - EntityId internalId = getInternalId(externalIdOpt.get(), false); - if (internalId != null) { - return Optional.of(internalId); - } else { - ctx.registerNotFound(externalIdOpt.get()); - } - return Optional.empty(); - } - private Optional buildEntityId(EntityType entityType, UUID externalUuid) { try { return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid)); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java index 611b536808..250026c71c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java @@ -65,10 +65,10 @@ public class DashboardImportService extends BaseEntityImportService exportData, IdProvider idProvider) { for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) { - replaceIdsRecursively(ctx, idProvider, entityAlias, Collections.emptySet(), HINTS); + replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), HINTS); } for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { - replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"), HINTS); + replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), HINTS); } return dashboard; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 8ffd4796fe..0e08ee9c48 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; @@ -69,6 +68,7 @@ import org.thingsboard.server.common.data.sync.vc.request.load.EntityTypeVersion import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest; +import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; @@ -259,7 +259,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont return gitServiceQueue.listEntitiesAtVersion(tenantId, versionId); } - @SuppressWarnings({"UnstableApiUsage", "rawtypes"}) + @SuppressWarnings({"rawtypes"}) @Override public UUID loadEntitiesVersion(User user, VersionLoadRequest request) throws Exception { EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId()); @@ -402,7 +402,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>()) .add(importResult.getSavedEntity().getId()); } - log.debug("Imported {} pack for tenant {}", entityType, ctx.getTenantId()); + log.debug("Imported {} pack ({}) for tenant {}", entityType, entityDataList.size(), ctx.getTenantId()); offset += limit; } while (entityDataList.size() == limit); } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 0c0ff3e0c9..57aa1e735e 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; @@ -66,11 +67,13 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -108,6 +111,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH; @@ -124,10 +128,13 @@ public class DefaultTransportApiService implements TransportApiService { private static final ObjectMapper mapper = new ObjectMapper(); + private static final Pattern X509_CERTIFICATE_TRIM_CHAIN_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----"); + private final TbDeviceProfileCache deviceProfileCache; private final TbTenantProfileCache tenantProfileCache; private final TbApiUsageStateService apiUsageStateService; private final DeviceService deviceService; + private final DeviceProfileService deviceProfileService; private final RelationService relationService; private final DeviceCredentialsService deviceCredentialsService; private final DbCallbackExecutorService dbCallbackExecutorService; @@ -159,6 +166,9 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE); + } else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) { + TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg(); + result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain()); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { @@ -226,6 +236,35 @@ public class DefaultTransportApiService implements TransportApiService { } } + protected ListenableFuture validateOrCreateDeviceX509Certificate(String certificateChain) { + List chain = X509_CERTIFICATE_TRIM_CHAIN_PATTERN.matcher(certificateChain).results().map(match -> + EncryptionUtil.certTrimNewLines(match.group())).collect(Collectors.toList()); + for (String certificateValue : chain) { + String certificateHash = EncryptionUtil.getSha3Hash(certificateValue); + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash); + if (credentials != null && DeviceCredentialsType.X509_CERTIFICATE.equals(credentials.getCredentialsType())) { + return getDeviceInfo(credentials); + } + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(certificateHash); + if (deviceProfile != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) { + String updatedDeviceProvisionSecret = chain.get(0); + ProvisionRequest provisionRequest = createProvisionRequest(updatedDeviceProvisionSecret); + try { + ProvisionResponse provisionResponse = deviceProvisionService.provisionDeviceViaX509Chain(deviceProfile, provisionRequest); + if (ProvisionResponseStatus.SUCCESS.equals(provisionResponse.getResponseStatus())) { + return getDeviceInfo(provisionResponse.getDeviceCredentials()); + } + } catch (ProvisionFailedException e) { + log.debug("[{}][{}] Failed to provision device with cert chain: {}", deviceProfile.getTenantId(), deviceProfile.getId(), provisionRequest, e); + return getEmptyTransportApiResponseFuture(); + } + } else if (deviceProfile != null) { + log.warn("[{}][{}] Device Profile provision configuration mismatched: expected {}, actual {}", deviceProfile.getTenantId(), deviceProfile.getId(), DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN, deviceProfile.getProvisionType()); + } + } + return getEmptyTransportApiResponseFuture(); + } + private ListenableFuture validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); if (credentials != null) { @@ -665,4 +704,11 @@ public class DefaultTransportApiService implements TransportApiService { private Long checkLong(Long l) { return l != null ? l : 0; } + + private ProvisionRequest createProvisionRequest(String certificateValue) { + return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, + new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue), + null); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java index 77d59fe069..941416669a 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java @@ -32,7 +32,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_COLUMN_ @TbCoreComponent @Slf4j @Service -@ConditionalOnExpression("${sql.ttl.edge_events.enabled:true} && ${sql.ttl.edge_events.edge_event_ttl:0} > 0") +@ConditionalOnExpression("${sql.ttl.edge_events.enabled:true} && ${sql.ttl.edge_events.edge_events_ttl:0} > 0") public class EdgeEventsCleanUpService extends AbstractCleanUpService { public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION = diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 7633c6feef..14bf47301b 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -131,6 +131,9 @@ security: loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}" githubMapper: emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}" + java_cacerts: + path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}${file.separator}lib${file.separator}security${file.separator}cacerts}" + password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}" # Usage statistics parameters usage: @@ -479,10 +482,14 @@ cache: entityCount: timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}" maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}" + # deliberately placed outside 'specs' group above notificationRules: timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_RULES_TTL:30}" maxSize: "${CACHE_SPECS_NOTIFICATION_RULES_MAX_SIZE:1000}" + rateLimits: + timeToLiveInMinutes: "${CACHE_SPECS_RATE_LIMITS_TTL:60}" + maxSize: "${CACHE_SPECS_RATE_LIMITS_MAX_SIZE:100000}" #Disable this because it is not required. spring.data.redis.repositories.enabled: false @@ -949,7 +956,9 @@ edges: enabled: "${EDGES_ENABLED:true}" rpc: port: "${EDGES_RPC_PORT:7070}" - client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:300}" + client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:1}" + keep_alive_time_sec: "${EDGES_RPC_KEEP_ALIVE_TIME_SEC:10}" + keep_alive_timeout_sec: "${EDGES_RPC_KEEP_ALIVE_TIMEOUT_SEC:5}" ssl: # Enable/disable SSL support enabled: "${EDGES_RPC_SSL_ENABLED:false}" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java index 0df044cb26..fb4f9bd322 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java @@ -299,6 +299,28 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); } + @Test + public void testSaveDeviceProfileWithSameCertificateHash() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + deviceProfile.setProvisionDeviceKey("Certificate hash"); + + doPost("/api/deviceProfile", deviceProfile) + .andExpect(status().isOk()); + + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2"); + deviceProfile2.setProvisionDeviceKey("Certificate hash"); + + Mockito.reset(tbClusterService, auditLogService); + + String msgError = "Device profile with such provision device key already exists!"; + doPost("/api/deviceProfile", deviceProfile2) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString(msgError))); + + testNotifyEntityEqualsOneTimeServiceNeverError(deviceProfile, savedTenant.getId(), + tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + } + @Test public void testChangeDeviceProfileTypeNull() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java new file mode 100644 index 0000000000..50ad731a79 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import org.junit.Test; +import org.stringtemplate.v4.ST; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseTelemetryControllerTest extends AbstractControllerTest { + + @Test + public void testConstraintValidator() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + String correctRequestBody = "{\"data\": \"value\"}"; + doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", correctRequestBody, String.class, status().isOk()); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", correctRequestBody, String.class, status().isOk()); + String invalidRequestBody = "{\"alert(document)\\\">\": \"data\"}"; + doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest()); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); + } + + private Device createDevice() throws Exception { + String testToken = "TEST_TOKEN"; + + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + deviceCredentials.setCredentialsId(testToken); + + SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials); + + return readResponse(doPost("/api/device-with-credentials", saveRequest).andExpect(status().isOk()), Device.class); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationText.java b/application/src/test/java/org/thingsboard/server/controller/sql/TelemetryControllerSqlTest.java similarity index 68% rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationText.java rename to application/src/test/java/org/thingsboard/server/controller/sql/TelemetryControllerSqlTest.java index 6cb0e673a5..b256f4fe1e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationText.java +++ b/application/src/test/java/org/thingsboard/server/controller/sql/TelemetryControllerSqlTest.java @@ -13,18 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.notification.template; +package org.thingsboard.server.controller.sql; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class NotificationText { - - private String body; - private String subject; +import org.thingsboard.server.controller.BaseTelemetryControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; +@DaoSqlTest +public class TelemetryControllerSqlTest extends BaseTelemetryControllerTest { } diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index b618ac7718..8f05e6810f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -108,6 +108,7 @@ public class EdgeImitator { updateEdgeClientFields("rpcPort", port); updateEdgeClientFields("timeoutSecs", 3); updateEdgeClientFields("keepAliveTimeSec", 300); + updateEdgeClientFields("keepAliveTimeoutSec", 5); updateEdgeClientFields("maxInboundMessageSize", 4194304); } diff --git a/application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java new file mode 100644 index 0000000000..388058eef4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java @@ -0,0 +1,266 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.device.provision; + + +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.common.transport.util.SslUtil; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionFailedException; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; +import org.thingsboard.server.dao.device.provision.ProvisionResponse; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.service.device.DeviceProvisionServiceImpl;;import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Slf4j +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = DeviceProvisionServiceImpl.class) +public class DeviceProvisionServiceTest { + + @MockBean + protected TbQueueProducerProvider producerProvider; + @MockBean + protected TbQueueProducer> ruleEngineMsgProducer; + @MockBean + protected TbClusterService clusterService; + @MockBean + protected DeviceProfileService deviceProfileService; + @MockBean + protected DeviceService deviceService; + @MockBean + protected DeviceCredentialsService deviceCredentialsService; + @MockBean + protected AttributesService attributesService; + @MockBean + protected AuditLogService auditLogService; + @MockBean + protected PartitionService partitionService; + @SpyBean + DeviceProvisionServiceImpl service; + + private String[] chain; + + @Before + public void setUp() { + String filePath = "src/test/resources/provision/x509ChainProvisionTest.pem"; + try { + String certificateChain = Files.readString(Paths.get(filePath)); + certificateChain = certTrimNewLinesForChainInDeviceProfile(certificateChain); + chain = fetchLeafCertificateFromChain(certificateChain); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + @Test + public void provisionDeviceViaX509Certificate() { + var tenant = createTenant(); + var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], true); + + var device = createDevice(tenant.getId(), deviceProfile.getId()); + when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device); + + var deviceCredentials = createDeviceCredentials(chain[0], device.getId()); + when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials); + when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); + + ProvisionResponse response = service.provisionDeviceViaX509Chain(deviceProfile, createProvisionRequest(chain[0])); + + verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); + verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); + verify(deviceCredentialsService, times(1)).updateDeviceCredentials(any(), any()); + + Assertions.assertThat(response.getResponseStatus()).isEqualTo(ProvisionResponseStatus.SUCCESS); + Assertions.assertThat(response.getDeviceCredentials()).isEqualTo(deviceCredentials); + } + + @Test + public void provisionDeviceWithIncorrectConfiguration() { + var tenant = createTenant(); + var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], false); + + Assertions.assertThatThrownBy(() -> + service.provisionDeviceViaX509Chain(deviceProfile, createProvisionRequest(chain[0]))) + .isInstanceOf(ProvisionFailedException.class); + + verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); + } + + @Test + public void matchDeviceNameFromX509CNCertificateByRegex() { + var tenant = createTenant(); + var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], true); + X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration(); + String CN = getCNFromX509Certificate(chain[0]); + String deviceName = service.extractDeviceNameFromCNByRegEx(deviceProfile, CN, configuration.getCertificateRegExPattern()); + + Assertions.assertThat(deviceName).isNotBlank(); + Assertions.assertThat(deviceName).isEqualTo("deviceCertificate"); + } + + @Test + public void matchDeviceNameFromCNByRegex() { + var CN = "DeviceA.company.com"; + var regex = "(.*)\\.company.com"; + var result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); + Assertions.assertThat(result).isNotBlank(); + Assertions.assertThat(result).isEqualTo("DeviceA"); + + CN = "DeviceA@company.com"; + regex = "(.*)@company.com"; + result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); + Assertions.assertThat(result).isNotBlank(); + Assertions.assertThat(result).isEqualTo("DeviceA"); + + CN = "prefixDeviceAsuffix@company.com"; + regex = "prefix(.*)suffix@company.com"; + result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); + Assertions.assertThat(result).isNotBlank(); + Assertions.assertThat(result).isEqualTo("DeviceA"); + + CN = "prefixDeviceAsufix@company.com"; + regex = "prefix(.*)sufix@company.com"; + result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); + Assertions.assertThat(result).isNotBlank(); + Assertions.assertThat(result).isEqualTo("DeviceA"); + + CN = "region.DeviceA.220423@company.com"; + regex = "\\D+\\.(.*)\\.\\d+@company.com"; + result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); + Assertions.assertThat(result).isNotBlank(); + Assertions.assertThat(result).isEqualTo("DeviceA"); + } + + private DeviceProfile createDeviceProfile(TenantId tenantId, String certificateValue, boolean isAllowToCreateNewDevices) { + X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration(); + provision.setProvisionDeviceSecret(certificateValue); + provision.setCertificateRegExPattern("([^@]+)"); + provision.setAllowCreateNewDevicesByX509Certificate(isAllowToCreateNewDevices); + + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setProvisionConfiguration(provision); + + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setId(new DeviceProfileId(UUID.randomUUID())); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setProvisionDeviceKey(EncryptionUtil.getSha3Hash(certificateValue)); + deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN); + deviceProfile.setTenantId(tenantId); + return deviceProfile; + } + + private Device createDevice(TenantId tenantId, DeviceProfileId deviceProfileId) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setId(new DeviceId(UUID.randomUUID())); + device.setDeviceProfileId(deviceProfileId); + device.setCustomerId(new CustomerId(UUID.randomUUID())); + return device; + } + + private Tenant createTenant() { + Tenant tenant = new Tenant(); + tenant.setId(new TenantId(UUID.randomUUID())); + return tenant; + } + + private DeviceCredentials createDeviceCredentials(String certificateValue, DeviceId deviceId) { + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(deviceId); + deviceCredentials.setCredentialsValue(certificateValue); + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(certificateValue)); + deviceCredentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE); + return deviceCredentials; + } + + private ProvisionRequest createProvisionRequest(String certificateValue) { + return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, + new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue), + null); + } + + public static String certTrimNewLinesForChainInDeviceProfile(String input) { + return input.replaceAll("\n", "") + .replaceAll("\r", "") + .replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n") + .replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n") + .trim(); + } + + private String[] fetchLeafCertificateFromChain(String value) { + List chain = new ArrayList<>(); + String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) { + chain.add(matcher.group(0)); + } + return chain.toArray(new String[0]); + } + + private String getCNFromX509Certificate(String x509Value) { + try { + return SslUtil.parseCommonName(SslUtil.readCertFile(x509Value)); + } catch (Exception e) { + return null; + } + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 9dd34296ef..f6a296ab7e 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.notification.targets.platform.Customer import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; +import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; @@ -49,7 +50,6 @@ import org.thingsboard.server.common.data.notification.template.SlackDeliveryMet import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.notification.NotificationDao; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.executors.DbCallbackExecutorService; @@ -226,13 +226,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest { NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText, 5); assertThat(notificationRequest.getStatus()).isEqualTo(NotificationRequestStatus.SCHEDULED); await().atLeast(4, TimeUnit.SECONDS) - .atMost(6, TimeUnit.SECONDS) + .atMost(15, TimeUnit.SECONDS) .until(() -> wsClient.getLastMsg() != null); Notification delayedNotification = wsClient.getLastDataUpdate().getUpdate(); assertThat(delayedNotification).extracting(Notification::getText).isEqualTo(notificationText); assertThat(delayedNotification.getCreatedTime() - notificationRequest.getCreatedTime()) - .isCloseTo(TimeUnit.SECONDS.toMillis(5), Offset.offset(500L)); + .isCloseTo(TimeUnit.SECONDS.toMillis(5), Offset.offset(10000L)); assertThat(findNotificationRequest(notificationRequest.getId()).getStatus()).isEqualTo(NotificationRequestStatus.SENT); } @@ -324,16 +324,17 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRequestPreview() throws Exception { - NotificationTarget target1 = new NotificationTarget(); - target1.setName("Me"); - PlatformUsersNotificationTargetConfig target1Config = new PlatformUsersNotificationTargetConfig(); + NotificationTarget tenantAdminTarget = new NotificationTarget(); + tenantAdminTarget.setName("Me"); + PlatformUsersNotificationTargetConfig tenantAdminTargetConfig = new PlatformUsersNotificationTargetConfig(); UserListFilter userListFilter = new UserListFilter(); - userListFilter.setUsersIds(DaoUtil.toUUIDs(List.of(tenantAdminUserId))); - target1Config.setUsersFilter(userListFilter); - target1.setConfiguration(target1Config); - target1 = saveNotificationTarget(target1); + userListFilter.setUsersIds(List.of(tenantAdminUserId.getId())); + tenantAdminTargetConfig.setUsersFilter(userListFilter); + tenantAdminTarget.setConfiguration(tenantAdminTargetConfig); + tenantAdminTarget = saveNotificationTarget(tenantAdminTarget); List recipients = new ArrayList<>(); recipients.add(TENANT_ADMIN_EMAIL); + String firstRecipientEmail = TENANT_ADMIN_EMAIL; createDifferentCustomer(); loginTenantAdmin(); @@ -347,21 +348,32 @@ public class NotificationApiTest extends AbstractNotificationApiTest { customerUser = createUser(customerUser, "12345678"); recipients.add(customerUser.getEmail()); } - NotificationTarget target2 = new NotificationTarget(); - target2.setName("Other customer users"); - PlatformUsersNotificationTargetConfig target2Config = new PlatformUsersNotificationTargetConfig(); + NotificationTarget customerUsersTarget = new NotificationTarget(); + customerUsersTarget.setName("Other customer users"); + PlatformUsersNotificationTargetConfig customerUsersTargetConfig = new PlatformUsersNotificationTargetConfig(); CustomerUsersFilter customerUsersFilter = new CustomerUsersFilter(); customerUsersFilter.setCustomerId(differentCustomerId.getId()); - target2Config.setUsersFilter(customerUsersFilter); - target2.setConfiguration(target2Config); - target2 = saveNotificationTarget(target2); - + customerUsersTargetConfig.setUsersFilter(customerUsersFilter); + customerUsersTarget.setConfiguration(customerUsersTargetConfig); + customerUsersTarget = saveNotificationTarget(customerUsersTarget); + + NotificationTarget slackTarget = new NotificationTarget(); + slackTarget.setName("Slack user"); + SlackNotificationTargetConfig slackTargetConfig = new SlackNotificationTargetConfig(); + slackTargetConfig.setConversationType(SlackConversationType.DIRECT); + SlackConversation slackConversation = new SlackConversation(); + slackConversation.setId("U1234567"); + slackConversation.setTitle("@jdoe (John Doe)"); + slackConversation.setWholeName("John Doe"); + slackTargetConfig.setConversation(slackConversation); + slackTarget.setConfiguration(slackTargetConfig); + slackTarget = saveNotificationTarget(slackTarget); + recipients.add(slackConversation.getTitle()); NotificationTemplate notificationTemplate = new NotificationTemplate(); notificationTemplate.setNotificationType(NotificationType.GENERAL); notificationTemplate.setName("Test template"); - String requestorEmail = TENANT_ADMIN_EMAIL; NotificationTemplateConfig templateConfig = new NotificationTemplateConfig(); HashMap templates = new HashMap<>(); templateConfig.setDeliveryMethodsTemplates(templates); @@ -369,69 +381,59 @@ public class NotificationApiTest extends AbstractNotificationApiTest { WebDeliveryMethodNotificationTemplate webNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); webNotificationTemplate.setEnabled(true); - webNotificationTemplate.setBody("Message for WEB: ${recipientEmail} ${unknownParam}"); - webNotificationTemplate.setSubject("Subject for WEB: ${recipientEmail}"); + webNotificationTemplate.setSubject("WEB SUBJECT: ${recipientEmail}"); + webNotificationTemplate.setBody("WEB: ${recipientEmail} ${unknownParam}"); templates.put(NotificationDeliveryMethod.WEB, webNotificationTemplate); SmsDeliveryMethodNotificationTemplate smsNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); smsNotificationTemplate.setEnabled(true); - smsNotificationTemplate.setBody("Message for SMS: ${recipientEmail}"); + smsNotificationTemplate.setBody("SMS: ${recipientEmail}"); templates.put(NotificationDeliveryMethod.SMS, smsNotificationTemplate); EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); emailNotificationTemplate.setEnabled(true); - emailNotificationTemplate.setSubject("Subject for EMAIL: ${recipientEmail}"); - emailNotificationTemplate.setBody("Message for EMAIL: ${recipientEmail}"); + emailNotificationTemplate.setSubject("EMAIL SUBJECT: ${recipientEmail}"); + emailNotificationTemplate.setBody("EMAIL: ${recipientEmail}"); templates.put(NotificationDeliveryMethod.EMAIL, emailNotificationTemplate); SlackDeliveryMethodNotificationTemplate slackNotificationTemplate = new SlackDeliveryMethodNotificationTemplate(); slackNotificationTemplate.setEnabled(true); - slackNotificationTemplate.setBody("Message for SLACK: ${recipientEmail}"); + slackNotificationTemplate.setBody("SLACK: ${recipientFirstName} ${recipientLastName}"); templates.put(NotificationDeliveryMethod.SLACK, slackNotificationTemplate); notificationTemplate = saveNotificationTemplate(notificationTemplate); - NotificationRequest notificationRequest = new NotificationRequest(); - notificationRequest.setTargets(List.of(target1.getUuidId(), target2.getUuidId())); + notificationRequest.setTargets(List.of(tenantAdminTarget.getUuidId(), customerUsersTarget.getUuidId(), slackTarget.getUuidId())); notificationRequest.setTemplateId(notificationTemplate.getId()); notificationRequest.setAdditionalConfig(new NotificationRequestConfig()); NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class); - assertThat(preview.getRecipientsCountByTarget().get(target1.getName())).isEqualTo(1); - assertThat(preview.getRecipientsCountByTarget().get(target2.getName())).isEqualTo(customerUsersCount); - assertThat(preview.getTotalRecipientsCount()).isEqualTo(1 + customerUsersCount); + assertThat(preview.getRecipientsCountByTarget().get(tenantAdminTarget.getName())).isEqualTo(1); + assertThat(preview.getRecipientsCountByTarget().get(customerUsersTarget.getName())).isEqualTo(customerUsersCount); + assertThat(preview.getRecipientsCountByTarget().get(slackTarget.getName())).isEqualTo(1); + + assertThat(preview.getTotalRecipientsCount()).isEqualTo(2 + customerUsersCount); assertThat(preview.getRecipientsPreview()).containsAll(recipients); Map processedTemplates = preview.getProcessedTemplates(); assertThat(processedTemplates.get(NotificationDeliveryMethod.WEB)).asInstanceOf(type(WebDeliveryMethodNotificationTemplate.class)) .satisfies(template -> { - assertThat(template.getBody()) - .startsWith("Message for WEB") - .endsWith(requestorEmail + " ${unknownParam}"); - assertThat(template.getSubject()) - .startsWith("Subject for WEB") - .endsWith(requestorEmail); + assertThat(template.getSubject()).isEqualTo("WEB SUBJECT: " + firstRecipientEmail); + assertThat(template.getBody()).isEqualTo("WEB: " + firstRecipientEmail + " ${unknownParam}"); }); assertThat(processedTemplates.get(NotificationDeliveryMethod.SMS)).asInstanceOf(type(SmsDeliveryMethodNotificationTemplate.class)) .satisfies(template -> { - assertThat(template.getBody()) - .startsWith("Message for SMS") - .endsWith(requestorEmail); + assertThat(template.getBody()).isEqualTo("SMS: " + firstRecipientEmail); }); assertThat(processedTemplates.get(NotificationDeliveryMethod.EMAIL)).asInstanceOf(type(EmailDeliveryMethodNotificationTemplate.class)) .satisfies(template -> { - assertThat(template.getBody()) - .startsWith("Message for EMAIL") - .endsWith(requestorEmail); - assertThat(template.getSubject()) - .startsWith("Subject for EMAIL") - .endsWith(requestorEmail); + assertThat(template.getSubject()).isEqualTo("EMAIL SUBJECT: " + firstRecipientEmail); + assertThat(template.getBody()).isEqualTo("EMAIL: " + firstRecipientEmail); }); assertThat(processedTemplates.get(NotificationDeliveryMethod.SLACK)).asInstanceOf(type(SlackDeliveryMethodNotificationTemplate.class)) .satisfies(template -> { - assertThat(template.getBody()) - .isEqualTo("Message for SLACK: ${recipientEmail}"); // ${recipientEmail} should not be processed + assertThat(template.getBody()).isEqualTo("SLACK: John Doe"); }); } 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 3e12cb6d05..8fec807d45 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 @@ -23,14 +23,12 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.util.Pair; -import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode; -import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; @@ -43,8 +41,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; -import org.thingsboard.server.common.data.id.NotificationRuleId; -import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationRequest; @@ -59,25 +56,21 @@ import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotific import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; -import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainMetaData; -import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.notification.NotificationRequestService; -import org.thingsboard.server.dao.notification.NotificationRuleService; -import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.service.apiusage.limits.LimitedApi; +import org.thingsboard.server.service.apiusage.limits.RateLimitService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.util.ArrayList; @@ -96,9 +89,6 @@ import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest -@TestPropertySource(properties = { - "js.evaluator=local" -}) public class NotificationRuleApiTest extends AbstractNotificationApiTest { @SpyBean @@ -106,18 +96,13 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Autowired private NotificationRequestService notificationRequestService; @Autowired - private NotificationRuleService notificationRuleService; + private TenantProfileService tenantProfileService; @Autowired - private NotificationTemplateService notificationTemplateService; - - @SpyBean - private AlarmService alarmService; + private RateLimitService rateLimitService; @Before public void beforeEach() throws Exception { loginTenantAdmin(); - notificationRuleService.deleteNotificationRulesByTenantId(tenantId); - notificationTemplateService.deleteNotificationTemplatesByTenantId(tenantId); } @Test @@ -208,7 +193,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { String alarmType = "myBoolIsTrue"; - DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType); + DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType); Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); clients.values().forEach(wsClient -> { @@ -272,7 +257,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM); String alarmType = "myBoolIsTrue"; - DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType); + DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType); Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig(); @@ -352,7 +337,56 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(ruleInfo.getDeliveryMethods()).containsOnly(deliveryMethods); } - private DeviceProfile createDeviceProfileWithAlarmRules(NotificationRuleId notificationRuleId, String alarmType) { + @Test + public void testNotificationRequestsPerRuleRateLimits() throws Exception { + int notificationRequestsLimit = 10; + TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + TenantProfileData profileData = tenantProfile.getProfileData(); + DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); + profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(notificationRequestsLimit + ":300"); + tenantProfile.setProfileData(profileData); + loginSysAdmin(); + doPost("/api/tenantProfile", tenantProfile).andExpect(status().isOk()); + loginTenantAdmin(); + + NotificationRule rule = new NotificationRule(); + rule.setName("Device created"); + rule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + NotificationTemplate template = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Device created", "Device created", + NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.SMS); + rule.setTemplateId(template.getId()); + EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); + triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE)); + triggerConfig.setCreated(true); + rule.setTriggerConfig(triggerConfig); + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + recipientsConfig.setTargets(List.of(target.getUuidId())); + rule.setRecipientsConfig(recipientsConfig); + rule = saveNotificationRule(rule); + + for (int i = 0; i < notificationRequestsLimit; i++) { + String name = "device " + i; + createDevice(name, name); + } + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit); + }); + for (int i = 0; i < 5; i++) { + String name = "device " + (notificationRequestsLimit + i); + createDevice(name, name); + } + + boolean rateLimitExceeded = !rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, tenantId, rule.getId()); + assertThat(rateLimitExceeded).isTrue(); + + TimeUnit.SECONDS.sleep(3); + assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit); + } + + private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) { DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); deviceProfile.setTenantId(tenantId); @@ -387,41 +421,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { return deviceProfile; } - private RuleChain createEmptyRuleChain(String name) { - RuleChain ruleChain = new RuleChain(); - ruleChain.setName(name); - ruleChain.setTenantId(tenantId); - ruleChain.setRoot(false); - ruleChain.setDebugMode(false); - ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); - - RuleChainMetaData metaData = new RuleChainMetaData(); - metaData.setRuleChainId(ruleChain.getId()); - metaData.setNodes(List.of()); - metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); - return ruleChain; - } - - private RuleNode addRuleNodeWithError(RuleChainId ruleChainId, String name) { - RuleChainMetaData metaData = new RuleChainMetaData(); - metaData.setRuleChainId(ruleChainId); - - RuleNode generatorNodeWithError = new RuleNode(); - generatorNodeWithError.setName(name); - generatorNodeWithError.setType(TbMsgGeneratorNode.class.getName()); - TbMsgGeneratorNodeConfiguration generatorNodeConfiguration = new TbMsgGeneratorNodeConfiguration(); - generatorNodeConfiguration.setScriptLang(ScriptLanguage.JS); - generatorNodeConfiguration.setPeriodInSeconds(1000); - generatorNodeConfiguration.setMsgCount(1); - generatorNodeConfiguration.setJsScript("[return"); - generatorNodeWithError.setConfiguration(mapper.valueToTree(generatorNodeConfiguration)); - - metaData.setNodes(List.of(generatorNodeWithError)); - metaData.setFirstNodeIndex(0); - metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); - return metaData.getNodes().get(0); - } - private NotificationRule saveNotificationRule(NotificationRule notificationRule) { return doPost("/api/notification/rule", notificationRule, NotificationRule.class); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java index 1b843d61b4..ce42298d9f 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.StringUtils; import org.junit.Before; import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultMatcher; import org.thingsboard.server.common.data.id.IdBased; @@ -30,8 +29,6 @@ import org.thingsboard.server.common.data.notification.template.NotificationTemp import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.notification.NotificationRuleService; -import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.List; @@ -43,16 +40,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public class NotificationTemplateApiTest extends AbstractNotificationApiTest { - @Autowired - private NotificationTemplateService templateService; - @Autowired - private NotificationRuleService notificationRuleService; - @Before public void beforeEach() throws Exception { loginTenantAdmin(); - notificationRuleService.deleteNotificationRulesByTenantId(tenantId); - templateService.deleteNotificationTemplatesByTenantId(tenantId); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 26e8b3e92a..e493916b8a 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -68,7 +68,6 @@ import org.thingsboard.server.service.ota.OtaPackageStateService; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -254,21 +253,25 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2"); Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1"); + DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing"); + String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c"; + String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa"; String entityAliases = "{\n" + - "\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" + - "\t\t\"alias\": \"assets\",\n" + - "\t\t\"filter\": {\n" + - "\t\t\t\"entityList\": [\n" + - "\t\t\t\t\"" + asset1.getId().toString() + "\",\n" + - "\t\t\t\t\"" + asset2.getId().toString() + "\"\n" + - "\t\t\t],\n" + - "\t\t\t\"entityType\": \"ASSET\",\n" + - "\t\t\t\"resolveMultiple\": true,\n" + - "\t\t\t\"type\": \"entityList\"\n" + - "\t\t},\n" + - "\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" + - "\t}\n" + + "\"" + aliasId + "\": {\n" + + "\"alias\": \"assets\",\n" + + "\"filter\": {\n" + + "\"entityList\": [\n" + + "\"" + asset1.getId().toString() + "\",\n" + + "\"" + asset2.getId().toString() + "\",\n" + + "\"" + tenantId1.getId().toString() + "\",\n" + + "\"" + existingDeviceProfile.getId().toString() + "\",\n" + + "\"" + unknownUuid + "\"\n" + + "],\n" + + "\"resolveMultiple\": true\n" + + "},\n" + + "\"id\": \"" + aliasId + "\"\n" + + "}\n" + "}"; ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); @@ -287,11 +290,23 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity(); Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity(); - Set entityAliasEntitiesIds = Streams.stream(importedDashboard.getConfiguration() - .get("entityAliases").elements().next().get("filter").get("entityList").elements()) - .map(JsonNode::asText).collect(Collectors.toSet()); - assertThat(entityAliasEntitiesIds).doesNotContain(asset1.getId().toString(), asset2.getId().toString()); - assertThat(entityAliasEntitiesIds).contains(importedAsset1.getId().toString(), importedAsset2.getId().toString()); + Map.Entry entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next(); + assertThat(entityAlias.getKey()).isEqualTo(aliasId); + assertThat(entityAlias.getValue().get("id").asText()).isEqualTo(aliasId); + + List aliasEntitiesIds = Streams.stream(entityAlias.getValue().get("filter").get("entityList").elements()) + .map(JsonNode::asText).collect(Collectors.toList()); + assertThat(aliasEntitiesIds).size().isEqualTo(5); + assertThat(aliasEntitiesIds).element(0).as("external asset 1 was replaced with imported one") + .isEqualTo(importedAsset1.getId().toString()); + assertThat(aliasEntitiesIds).element(1).as("external asset 2 was replaced with imported one") + .isEqualTo(importedAsset2.getId().toString()); + assertThat(aliasEntitiesIds).element(2).as("external tenant id was replaced with new tenant id") + .isEqualTo(tenantId2.toString()); + assertThat(aliasEntitiesIds).element(3).as("existing device profile id was left as is") + .isEqualTo(existingDeviceProfile.getId().toString()); + assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id") + .isEqualTo(tenantId2.toString()); } @@ -469,7 +484,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), - ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() diff --git a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java new file mode 100644 index 0000000000..9687225bd1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java @@ -0,0 +1,211 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.transport; + + +import com.google.common.util.concurrent.Futures; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.server.cache.ota.OtaPackageDataCache; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceProvisionService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionResponse; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.service.resource.TbResourceService; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Slf4j +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = DefaultTransportApiService.class) +public class DefaultTransportApiServiceTest { + + @MockBean + protected TbDeviceProfileCache deviceProfileCache; + @MockBean + protected TbTenantProfileCache tenantProfileCache; + @MockBean + protected TbApiUsageStateService apiUsageStateService; + @MockBean + protected DeviceService deviceService; + @MockBean + protected DeviceProfileService deviceProfileService; + @MockBean + protected RelationService relationService; + @MockBean + protected DeviceCredentialsService deviceCredentialsService; + @MockBean + protected DbCallbackExecutorService dbCallbackExecutorService; + @MockBean + protected TbClusterService tbClusterService; + @MockBean + protected DataDecodingEncodingService dataDecodingEncodingService; + @MockBean + protected DeviceProvisionService deviceProvisionService; + @MockBean + protected TbResourceService resourceService; + @MockBean + protected OtaPackageService otaPackageService; + @MockBean + protected OtaPackageDataCache otaPackageDataCache; + @MockBean + protected QueueService queueService; + @SpyBean + DefaultTransportApiService service; + + private String certificateChain; + private String[] chain; + + @Before + public void setUp() { + + String filePath = "src/test/resources/provision/x509ChainProvisionTest.pem"; + try { + certificateChain = Files.readString(Paths.get(filePath)); + certificateChain = certTrimNewLinesForChainInDeviceProfile(certificateChain); + chain = fetchLeafCertificateFromChain(certificateChain); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void validateExistingDeviceByX509CertificateStrategy() { + var device = createDevice(); + when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device)); + + var deviceCredentials = createDeviceCredentials(chain[0], device.getId()); + when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(deviceCredentials); + + service.validateOrCreateDeviceX509Certificate(certificateChain); + verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any()); + } + + @Test + public void provisionDeviceX509Certificate() { + var deviceProfile = createDeviceProfile(chain[1]); + when(deviceProfileService.findDeviceProfileByProvisionDeviceKey(any())).thenReturn(deviceProfile); + + var device = createDevice(); + when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device); + when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device)); + + var deviceCredentials = createDeviceCredentials(chain[0], device.getId()); + when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(null); + when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); + + var provisionResponse = createProvisionResponse(deviceCredentials); + when(deviceProvisionService.provisionDeviceViaX509Chain(any(), any())).thenReturn(provisionResponse); + + service.validateOrCreateDeviceX509Certificate(certificateChain); + verify(deviceProfileService, times(1)).findDeviceProfileByProvisionDeviceKey(any()); + verify(deviceService, times(1)).findDeviceByIdAsync(any(), any()); + verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any()); + verify(deviceProvisionService, times(1)).provisionDeviceViaX509Chain(any(), any()); + } + + private DeviceProfile createDeviceProfile(String certificateValue) { + X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration(); + provision.setProvisionDeviceSecret(certificateValue); + provision.setCertificateRegExPattern("([^@]+)"); + provision.setAllowCreateNewDevicesByX509Certificate(true); + + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setProvisionConfiguration(provision); + + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setProvisionDeviceKey(EncryptionUtil.getSha3Hash(certificateValue)); + deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN); + return deviceProfile; + } + + private DeviceCredentials createDeviceCredentials(String certificateValue, DeviceId deviceId) { + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(deviceId); + deviceCredentials.setCredentialsValue(certificateValue); + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(certificateValue)); + deviceCredentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE); + return deviceCredentials; + } + + private Device createDevice() { + Device device = new Device(); + device.setId(new DeviceId(UUID.randomUUID())); + return device; + } + + private ProvisionResponse createProvisionResponse(DeviceCredentials deviceCredentials) { + return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS); + } + + public static String certTrimNewLinesForChainInDeviceProfile(String input) { + return input.replaceAll("\n", "") + .replaceAll("\r", "") + .replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n") + .replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n") + .trim(); + } + + private String[] fetchLeafCertificateFromChain(String value) { + List chain = new ArrayList<>(); + String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) { + chain.add(matcher.group(0)); + } + return chain.toArray(new String[0]); + } +} diff --git a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java new file mode 100644 index 0000000000..6a8491304d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.system; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.springframework.util.ClassUtils; +import org.springframework.web.client.RestTemplate; + + +@Slf4j +public class RestTemplateConvertersTest { + + @Test + public void testJacksonXmlConverter() { + ClassLoader classLoader = RestTemplate.class.getClassLoader(); + boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); + Assertions.assertFalse(jackson2XmlPresent, "XmlMapper must not be present in classpath, please, exclude \"jackson-dataformat-xml\" dependency!"); + //If this xml mapper will be present in classpath then we will get "Unsupported Media Type" in RestTemplate + } + +} diff --git a/application/src/test/resources/provision/x509ChainProvisionTest.pem b/application/src/test/resources/provision/x509ChainProvisionTest.pem new file mode 100644 index 0000000000..b2ec300f78 --- /dev/null +++ b/application/src/test/resources/provision/x509ChainProvisionTest.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIw +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1ow +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qK +QiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaC +kQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8G +A1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZ +uccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIw +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlow +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72k +rDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMy +LydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8G +A1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZ +NGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A== +-----END CERTIFICATE----- diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 65a57a5cc5..5198c6fc8d 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -179,6 +179,10 @@ message ValidateDeviceX509CertRequestMsg { string hash = 1; } +message ValidateOrCreateDeviceX509CertRequestMsg { + string certificateChain = 1; +} + message ValidateBasicMqttCredRequestMsg { string clientId = 1; string userName = 2; @@ -942,6 +946,7 @@ message TransportApiRequestMsg { GetDeviceRequestMsg deviceRequestMsg = 12; GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13; GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14; + ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateX509CertRequestMsg = 15; } /* Response from ThingsBoard Core Service to Transport Service */ diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java index 8d17e84b01..c57b316a44 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.dao.device; +import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; -import com.fasterxml.jackson.databind.JsonNode; public interface DeviceCredentialsService { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index 5fc45afb57..e765cc5030 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -39,6 +39,8 @@ public interface DeviceProfileService extends EntityDaoService { PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType); + DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey); + DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName); DeviceProfile createDefaultDeviceProfile(TenantId tenantId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java index 77a8d3edbc..5fb6462c80 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; @@ -22,4 +23,6 @@ import org.thingsboard.server.dao.device.provision.ProvisionResponse; public interface DeviceProvisionService { ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) throws ProvisionFailedException; + + ProvisionResponse provisionDeviceViaX509Chain(DeviceProfile deviceProfile, ProvisionRequest provisionRequest) throws ProvisionFailedException; } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java index 2579b1f9cf..f3fc6f97fc 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java @@ -16,7 +16,11 @@ package org.thingsboard.server.dao.device.provision; public class ProvisionFailedException extends RuntimeException { + + private static final long serialVersionUID = 1673991117668477401L; + public ProvisionFailedException(String errorMsg) { super(errorMsg); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java index 7af4361698..f5ef1319a8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java @@ -18,5 +18,6 @@ package org.thingsboard.server.common.data; public enum DeviceProfileProvisionType { DISABLED, ALLOW_CREATE_NEW_DEVICES, - CHECK_PRE_PROVISIONED_DEVICES + CHECK_PRE_PROVISIONED_DEVICES, + X509_CERTIFICATE_CHAIN } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java index 4a825ab96f..c5882dc187 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -31,7 +31,8 @@ import java.io.Serializable; @JsonSubTypes({ @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), - @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")}) + @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES"), + @JsonSubTypes.Type(value = X509CertificateChainProvisionConfiguration.class, name = "X509_CERTIFICATE_CHAIN")}) public interface DeviceProfileProvisionConfiguration extends Serializable { String getProvisionDeviceSecret(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java new file mode 100644 index 0000000000..d5edafca69 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + +@Data +@NoArgsConstructor +public class X509CertificateChainProvisionConfiguration implements DeviceProfileProvisionConfiguration { + + private String provisionDeviceSecret; + private String certificateRegExPattern; + private boolean allowCreateNewDevicesByX509Certificate; + + @Override + public DeviceProfileProvisionType getType() { + return DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java index bdb8c69621..0f80f5ef89 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.kv; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; import java.util.Objects; import java.util.Optional; @@ -23,6 +24,7 @@ import java.util.Optional; public abstract class BasicKvEntry implements KvEntry { @Length(fieldName = "attribute key") + @NoXss private final String key; protected BasicKvEntry(String key) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java index fc64eed1ff..3c612d3b78 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.kv; +import javax.validation.Valid; import java.util.Objects; import java.util.Optional; public class BasicTsKvEntry implements TsKvEntry { private static final int MAX_CHARS_PER_DATA_POINT = 512; protected final long ts; + @Valid private final KvEntry kv; public BasicTsKvEntry(long ts, KvEntry kv) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java index 95712bdd70..0736b50b84 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java @@ -19,6 +19,7 @@ import java.util.Objects; import java.util.Optional; public class JsonDataEntry extends BasicKvEntry { + private final String value; public JsonDataEntry(String key, String value) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java index 06a2b5cac6..18a54327a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java @@ -65,7 +65,7 @@ public class StringDataEntry extends BasicKvEntry { public String toString() { return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString(); } - + @Override public String getValueAsString() { return value; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java index 529bc9a3ab..450fbdb23f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; @@ -43,6 +44,7 @@ public class NotificationRule extends BaseData implements Ha private TenantId tenantId; @NotBlank @NoXss + @Length(max = 255, message = "cannot be longer than 255 chars") private String name; @NotNull private NotificationTemplateId templateId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java index 9f267a514d..9a2f9a5306 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; @@ -35,6 +36,7 @@ public class NotificationTarget extends BaseData implement private TenantId tenantId; @NotBlank @NoXss + @Length(max = 255, message = "cannot be longer than 255 chars") private String name; @NotNull @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java index 2333062f65..231e419fb0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @JsonIgnoreProperties(ignoreUnknown = true) @@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.validation.NoXss; public abstract class NotificationTargetConfig { @NoXss + @Length(max = 500, message = "cannot be longer than 500 chars") private String description; @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java index 0c2441e37d..1254654ecf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java @@ -19,6 +19,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import java.util.Arrays; import java.util.Set; @RequiredArgsConstructor @@ -30,4 +31,10 @@ public enum NotificationTargetType { @Getter private final Set supportedDeliveryMethods; + public static NotificationTargetType forDeliveryMethod(NotificationDeliveryMethod deliveryMethod) { + return Arrays.stream(values()) + .filter(targetType -> targetType.getSupportedDeliveryMethods().contains(deliveryMethod)) + .findFirst().orElse(null); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java index bf8aca7913..fe909a104e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java @@ -21,6 +21,8 @@ import lombok.NoArgsConstructor; import lombok.ToString; import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.constraints.NotEmpty; @@ -30,6 +32,8 @@ import javax.validation.constraints.NotEmpty; @ToString(callSuper = true) public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + @NoXss(fieldName = "email subject") + @Length(fieldName = "email subject", max = 250, message = "cannot be longer than 250 chars") @NotEmpty private String subject; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java index 2eaff12a9b..71da2f862b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; @@ -36,6 +37,7 @@ public class NotificationTemplate extends BaseData imple private TenantId tenantId; @NoXss @NotEmpty + @Length(max = 255, message = "cannot be longer than 255 chars") private String name; @NoXss @NotNull diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java index 735eb3660e..8457677308 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java @@ -20,6 +20,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.validation.NoXss; @Data @NoArgsConstructor @@ -31,6 +32,12 @@ public class SlackDeliveryMethodNotificationTemplate extends DeliveryMethodNotif super(other); } + @NoXss(fieldName = "Slack message") + @Override + public String getBody() { + return super.getBody(); + } + @Override public NotificationDeliveryMethod getMethod() { return NotificationDeliveryMethod.SLACK; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java index 55d2582284..7dc2e494f3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java @@ -20,6 +20,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; @Data @NoArgsConstructor @@ -31,6 +33,13 @@ public class SmsDeliveryMethodNotificationTemplate extends DeliveryMethodNotific super(other); } + @NoXss(fieldName = "SMS message") + @Length(fieldName = "SMS message", max = 320, message = "cannot be longer than 320 chars") + @Override + public String getBody() { + return super.getBody(); + } + @Override public NotificationDeliveryMethod getMethod() { return NotificationDeliveryMethod.SMS; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java index ac1e0cfd8c..f26ed00568 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java @@ -25,6 +25,8 @@ import lombok.NoArgsConstructor; import lombok.ToString; import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.constraints.NotEmpty; import java.util.Optional; @@ -35,6 +37,8 @@ import java.util.Optional; @ToString(callSuper = true) public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + @NoXss(fieldName = "web notification subject") + @Length(fieldName = "web notification subject", max = 150, message = "cannot be longer than 150 chars") @NotEmpty private String subject; private JsonNode additionalConfig; @@ -45,6 +49,15 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific this.additionalConfig = other.additionalConfig != null ? other.additionalConfig.deepCopy() : null; } + @NoXss(fieldName = "web notification message") + @Length(fieldName = "web notification message", max = 250, message = "cannot be longer than 250 chars") + @Override + public String getBody() { + return super.getBody(); + } + + @NoXss(fieldName = "web notification button text") + @Length(fieldName = "web notification button text", max = 50, message = "cannot be longer than 50 chars") @JsonIgnore public String getButtonText() { return getButtonConfigProperty("text"); @@ -57,6 +70,8 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific }); } + @NoXss(fieldName = "web notification button link") + @Length(fieldName = "web notification button link", max = 300, message = "cannot be longer than 300 chars") @JsonIgnore public String getButtonLink() { return getButtonConfigProperty("link"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 030587f50f..66fba5f832 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -48,6 +48,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private String tenantEntityExportRateLimit; private String tenantEntityImportRateLimit; private String tenantNotificationRequestsRateLimit; + private String tenantNotificationRequestsPerRuleRateLimit; private long maxTransportMessages; private long maxTransportDataPoints; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java index 2b412d0ad7..cb7b9b1693 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java @@ -23,12 +23,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.METHOD}) @Constraint(validatedBy = {}) public @interface Length { String message() default "length must be equal or less than {max}"; - String fieldName(); + String fieldName() default ""; int max() default 255; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java index c99502ed7b..fb7b048f7e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java @@ -23,12 +23,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.METHOD}) @Constraint(validatedBy = {}) public @interface NoXss { + String message() default "is malformed"; + String fieldName() default ""; + Class[] groups() default {}; Class[] payload() default {}; + } diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 2acbe49f98..4e8548aa33 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -57,8 +57,10 @@ public class EdgeGrpcClient implements EdgeRpcClient { private int rpcPort; @Value("${cloud.rpc.timeout}") private int timeoutSecs; - @Value("${cloud.rpc.keep_alive_time_sec}") + @Value("${cloud.rpc.keep_alive_time_sec:10}") private int keepAliveTimeSec; + @Value("${cloud.rpc.keep_alive_timeout_sec:5}") + private int keepAliveTimeoutSec; @Value("${cloud.rpc.ssl.enabled}") private boolean sslEnabled; @Value("${cloud.rpc.ssl.cert:}") @@ -83,7 +85,9 @@ public class EdgeGrpcClient implements EdgeRpcClient { Consumer onError) { NettyChannelBuilder builder = NettyChannelBuilder.forAddress(rpcHost, rpcPort) .maxInboundMessageSize(maxInboundMessageSize) - .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS); + .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS) + .keepAliveWithoutCalls(true); if (sslEnabled) { try { SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient(); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java index 2c01e11712..e944a8dd92 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java @@ -35,6 +35,14 @@ public class EncryptionUtil { .replaceAll("-----END CERTIFICATE-----", ""); } + public static String certTrimNewLinesForChainInDeviceProfile(String input) { + return input.replaceAll("\n", "") + .replaceAll("\r", "") + .replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n") + .replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n") + .trim(); + } + public static String pubkTrimNewLines(String input) { return input.replaceAll("-----BEGIN PUBLIC KEY-----", "") .replaceAll("\n", "") diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 4b425e8446..bfa80ba615 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -24,9 +24,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; @@ -42,7 +41,6 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.concurrent.CountDownLatch; @@ -123,7 +121,7 @@ public class MqttSslHandlerProvider { static class ThingsboardMqttX509TrustManager implements X509TrustManager { private final X509TrustManager trustManager; - private TransportService transportService; + private final TransportService transportService; ThingsboardMqttX509TrustManager(X509TrustManager trustManager, TransportService transportService) { this.trustManager = trustManager; @@ -142,43 +140,59 @@ public class MqttSslHandlerProvider { } @Override - public void checkClientTrusted(X509Certificate[] chain, - String authType) throws CertificateException { - String credentialsBody = null; - for (X509Certificate cert : chain) { - try { - String strCert = SslUtil.getCertificateString(cert); - String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - final String[] credentialsBodyHolder = new String[1]; - CountDownLatch latch = new CountDownLatch(1); - transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponse msg) { - if (!StringUtils.isEmpty(msg.getCredentials())) { - credentialsBodyHolder[0] = msg.getCredentials(); - } - latch.countDown(); - } - - @Override - public void onError(Throwable e) { - log.error(e.getMessage(), e); - latch.countDown(); + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (!validateCertificateChain(chain)) { + throw new CertificateException("Invalid Chain of X509 Certificates. "); + } + String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]); + final String[] credentialsBodyHolder = new String[1]; + CountDownLatch latch = new CountDownLatch(1); + try { + String certificateChain = SslUtil.getCertificateChainString(chain); + transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg + .newBuilder().setCertificateChain(certificateChain).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + credentialsBodyHolder[0] = msg.getCredentials(); } - }); - latch.await(10, TimeUnit.SECONDS); - if (strCert.equals(credentialsBodyHolder[0])) { - credentialsBody = credentialsBodyHolder[0]; - break; + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + log.trace("Failed to process certificate chain: {}", certificateChain, e); + latch.countDown(); + } + }); + latch.await(10, TimeUnit.SECONDS); + if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) { + log.debug("Failed to find credentials for device certificate chain: {}", chain); + if (chain.length == 1) { + throw new CertificateException("Invalid Device Certificate"); + } else { + throw new CertificateException("Invalid Chain of X509 Certificates"); } - } catch (InterruptedException | CertificateEncodingException e) { - log.error(e.getMessage(), e); } + } catch (Exception e) { + log.error(e.getMessage(), e); } - if (credentialsBody == null) { - log.debug("Failed to find credentials for device certificate chain: {}", chain); - throw new CertificateException("Invalid Device Certificate"); + } + + private boolean validateCertificateChain(X509Certificate[] chain) { + try { + if (chain.length > 1) { + X509Certificate leafCert = chain[0]; + for (int i = 1; i < chain.length; i++) { + X509Certificate intermediateCert = chain[i]; + leafCert.verify(intermediateCert.getPublicKey()); + leafCert = intermediateCert; + } + } + return true; + } catch (Exception e) { + return false; } } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 9440ff8e79..abcd35ee23 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -54,8 +54,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMs import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg; import java.util.List; import java.util.concurrent.ExecutorService; @@ -87,6 +88,9 @@ public interface TransportService { void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback); + void process(DeviceTransportType transportType, ValidateOrCreateDeviceX509CertRequestMsg msg, + TransportServiceCallback callback); + void process(ValidateDeviceLwM2MCredentialsRequestMsg msg, TransportServiceCallback callback); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 1bc6687465..883522881f 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -72,7 +72,6 @@ import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGateway import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.common.transport.limits.TransportRateLimitService; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; @@ -97,6 +96,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbTransportQueueFactory; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbTransportComponent; import javax.annotation.PostConstruct; @@ -434,6 +434,13 @@ public class DefaultTransportService implements TransportService { doProcess(transportType, protoMsg, callback); } + @Override + public void process(DeviceTransportType transportType, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { + log.trace("Processing msg: {}", msg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateOrCreateX509CertRequestMsg(msg).build()); + doProcess(transportType, protoMsg, callback); + } + private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg protoMsg, TransportServiceCallback callback) { ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index 0431868965..089718ec05 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -16,11 +16,21 @@ package org.thingsboard.server.common.transport.util; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.springframework.util.Base64Utils; import org.thingsboard.server.common.msg.EncryptionUtil; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; /** * @author Valerii Sosliuk @@ -35,4 +45,44 @@ public class SslUtil { throws CertificateEncodingException { return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded())); } + + public static String getCertificateChainString(Certificate[] chain) + throws CertificateEncodingException { + String begin = "-----BEGIN CERTIFICATE-----"; + String end = "-----END CERTIFICATE-----"; + StringBuilder stringBuilder = new StringBuilder(); + for (Certificate cert: chain) { + stringBuilder.append(begin).append(EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()))).append(end).append("\n"); + } + return stringBuilder.toString(); + } + + public static X509Certificate readCertFile(String fileContent) { + X509Certificate certificate = null; + try { + if (fileContent != null && !fileContent.trim().isEmpty()) { + fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replaceAll("\\s", ""); + byte[] decoded = Base64.decodeBase64(fileContent); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + try (InputStream inStream = new ByteArrayInputStream(decoded)) { + certificate = (X509Certificate) certFactory.generateCertificate(inStream); + } + } + } catch (Exception ignored) {} + return certificate; + } + + public static String parseCommonName(X509Certificate certificate) { + X500Name x500name; + try { + x500name = new JcaX509CertificateHolder(certificate).getSubject(); + } catch (CertificateEncodingException e) { + log.warn("Cannot parse CN from device certificate"); + throw new RuntimeException(e); + } + RDN cn = x500name.getRDNs(BCStyle.CN)[0]; + return IETFUtils.valueToString(cn.getFirst().getValue()); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java index 168782c0fa..d1abeda5b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java @@ -19,6 +19,9 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.util.KvUtils; + +import java.util.List; public class AttributeUtils { @@ -27,10 +30,13 @@ public class AttributeUtils { Validator.validateString(scope, "Incorrect scope " + scope); } + public static void validate(List kvEntries) { + kvEntries.forEach(AttributeUtils::validate); + } + public static void validate(AttributeKvEntry kvEntry) { - if (kvEntry == null) { - throw new IncorrectParameterException("Key value entry can't be null"); - } else if (kvEntry.getDataType() == null) { + KvUtils.validate(kvEntry); + if (kvEntry.getDataType() == null) { throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null"); } else { Validator.validateString(kvEntry.getKey(), "Incorrect kvEntry. Key can't be empty"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index ef38de352a..abd55b6b12 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; @@ -89,7 +90,7 @@ public class BaseAttributesService implements AttributesService { @Override public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); - attributes.forEach(AttributeUtils::validate); + AttributeUtils.validate(attributes); List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); return Futures.allAsList(saveFutures); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 01a6d1201c..b95ce39d9a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -134,7 +134,7 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, Collection attributeKeys) { validate(entityId, scope); - attributeKeys = new LinkedHashSet<>(attributeKeys); // deduplicate the attributes + attributeKeys = new LinkedHashSet<>(attributeKeys); // deduplicate the attributes attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey)); Map> wrappedCachedAttributes = findCachedAttributes(entityId, scope, attributeKeys); @@ -220,7 +220,7 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); - attributes.forEach(AttributeUtils::validate); + AttributeUtils.validate(attributes); List> futures = new ArrayList<>(attributes.size()); for (var attribute : attributes) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 360085be20..6e79b953ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -16,15 +16,12 @@ package org.thingsboard.server.dao.device; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.util.SecurityUtil; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java index 07428064a8..d032282da9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.device; import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; @@ -30,34 +31,44 @@ public class DeviceProfileCacheKey implements Serializable { private final String name; private final DeviceProfileId deviceProfileId; private final boolean defaultProfile; + private final String provisionDeviceKey; - private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile) { + private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile, String provisionDeviceKey) { this.tenantId = tenantId; this.name = name; this.deviceProfileId = deviceProfileId; this.defaultProfile = defaultProfile; + this.provisionDeviceKey = provisionDeviceKey; } public static DeviceProfileCacheKey fromName(TenantId tenantId, String name) { - return new DeviceProfileCacheKey(tenantId, name, null, false); + return new DeviceProfileCacheKey(tenantId, name, null, false, null); } public static DeviceProfileCacheKey fromId(DeviceProfileId id) { - return new DeviceProfileCacheKey(null, null, id, false); + return new DeviceProfileCacheKey(null, null, id, false, null); } public static DeviceProfileCacheKey defaultProfile(TenantId tenantId) { - return new DeviceProfileCacheKey(tenantId, null, null, true); + return new DeviceProfileCacheKey(tenantId, null, null, true, null); } + public static DeviceProfileCacheKey fromProvisionDeviceKey(String provisionDeviceKey) { + return new DeviceProfileCacheKey(null, null, null, false, provisionDeviceKey); + } + + /** + * IMPORTANT: Method toString() has to return unique value, if you add additional field to this class, please also refactor toString(). + */ @Override public String toString() { if (deviceProfileId != null) { return deviceProfileId.toString(); } else if (defaultProfile) { return tenantId.toString(); - } else { - return tenantId + "_" + name; + } else if (StringUtils.isNotEmpty(provisionDeviceKey)) { + return provisionDeviceKey; } + return tenantId + "_" + name; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java index c5a699b035..6470bfc584 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java @@ -27,5 +27,6 @@ public class DeviceProfileEvictEvent { private final String oldName; private final DeviceProfileId deviceProfileId; private final boolean defaultProfile; + private final String provisionDeviceKey; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 98ca4fde7b..391695f2bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -34,12 +34,14 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileCon import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueService; @@ -47,12 +49,19 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import java.io.ByteArrayInputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateString; @Service("DeviceProfileDaoService") @Slf4j @@ -61,6 +70,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true); } @@ -107,33 +120,52 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService deviceProfileDao.findByName(tenantId, profileName), true); } + @Override + public DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey) { + log.trace("Executing findDeviceProfileByProvisionDeviceKey provisionKey [{}]", provisionDeviceKey); + validateString(provisionDeviceKey, INCORRECT_PROVISION_DEVICE_KEY + provisionDeviceKey); + return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromProvisionDeviceKey(provisionDeviceKey), + () -> deviceProfileDao.findByProvisionDeviceKey(provisionDeviceKey), false); + } + @Override public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) { log.trace("Executing findDeviceProfileById [{}]", deviceProfileId); - Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId)); } @Override public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { log.trace("Executing saveDeviceProfile [{}]", deviceProfile); + if (deviceProfile.getProfileData() != null && deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) { + X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration(); + if (x509Configuration.getProvisionDeviceSecret() != null) { + formatDeviceProfileCertificate(deviceProfile, x509Configuration); + } + } DeviceProfile oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); DeviceProfile savedDeviceProfile; try { savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile); publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(), - oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault())); + oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault(), + oldDeviceProfile != null ? oldDeviceProfile.getProvisionDeviceKey() : null)); } catch (Exception t) { handleEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), - oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault())); + oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault(), + oldDeviceProfile != null ? oldDeviceProfile.getProvisionDeviceKey() : null)); + String unqProvisionKeyErrorMsg = DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType()) + ? "Device profile with such certificate already exists!" + : "Device profile with such provision device key already exists!"; checkConstraintViolation(t, Map.of("device_profile_name_unq_key", DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS, - "device_provision_key_unq_key", "Device profile with such provision device key already exists!", + "device_provision_key_unq_key", unqProvisionKeyErrorMsg, "device_profile_external_id_unq_key", "Device profile with such external id already exists!")); throw t; } @@ -156,7 +188,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService 1) { + return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue); + } + } catch (CertificateException ignored) {} + return EncryptionUtil.certTrimNewLines(certificateValue); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index 9438c79b62..8319468c04 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -170,8 +170,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS newAlarmRuleTriggerConfig.setAlarmSeverities(null); newAlarmRuleTriggerConfig.setNotifyOn(Set.of(AlarmAction.CREATED)); createRule(tenantId, "New alarm", newAlarmNotificationTemplate.getId(), newAlarmRuleTriggerConfig, - List.of(tenantAdmins.getId(), originatorEntityOwnerUsers.getId()), "Send notification to tenant admins and alarm's customer users " + - "when an alarm is created"); + List.of(tenantAdmins.getId()), "Send notification to tenant admins when an alarm is created"); NotificationTemplate alarmUpdateNotificationTemplate = createTemplate(tenantId, "Alarm update notification", NotificationType.ALARM, "Alarm '${alarmType}' - ${action}", @@ -182,8 +181,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS alarmRuleTriggerConfig.setAlarmSeverities(null); alarmRuleTriggerConfig.setNotifyOn(Set.of(AlarmAction.SEVERITY_CHANGED, AlarmAction.ACKNOWLEDGED, AlarmAction.CLEARED)); createRule(tenantId, "Alarm update", alarmUpdateNotificationTemplate.getId(), alarmRuleTriggerConfig, - List.of(tenantAdmins.getId(), originatorEntityOwnerUsers.getId()), "Send notification to tenant admins and alarm's customer users " + - "when any alarm is updated or cleared"); + List.of(tenantAdmins.getId()), "Send notification to tenant admins when any alarm is updated or cleared"); NotificationTemplate deviceActionNotificationTemplate = createTemplate(tenantId, "Device action notification", NotificationType.ENTITY_ACTION, "${entityType} was ${actionType}", @@ -195,8 +193,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS deviceActionRuleTriggerConfig.setUpdated(false); deviceActionRuleTriggerConfig.setDeleted(false); createRule(tenantId, "Device created", deviceActionNotificationTemplate.getId(), deviceActionRuleTriggerConfig, - List.of(originatorEntityOwnerUsers.getId()), "Send notification to tenant admins or customer users " + - "when device is created"); + List.of(tenantAdmins.getId()), "Send notification to tenant admins when device is created"); NotificationTemplate deviceActivityNotificationTemplate = createTemplate(tenantId, "Device activity notification", NotificationType.DEVICE_ACTIVITY, "Device '${deviceName}' became ${eventType}", @@ -207,8 +204,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS deviceActivityRuleTriggerConfig.setDeviceProfiles(null); deviceActivityRuleTriggerConfig.setNotifyOn(Set.of(DeviceEvent.ACTIVE, DeviceEvent.INACTIVE)); createRule(tenantId, "Device activity status change", deviceActivityNotificationTemplate.getId(), deviceActivityRuleTriggerConfig, - List.of(originatorEntityOwnerUsers.getId()), "Send notification to tenant admins or customer users " + - "when any device changes its activity state"); + List.of(tenantAdmins.getId()), "Send notification to tenant admins when any device changes its activity state"); NotificationTemplate alarmCommentNotificationTemplate = createTemplate(tenantId, "Alarm comment notification", NotificationType.ALARM_COMMENT, "Comment on '${alarmType}' alarm", @@ -221,8 +217,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS alarmCommentRuleTriggerConfig.setOnlyUserComments(true); alarmCommentRuleTriggerConfig.setNotifyOnCommentUpdate(false); createRule(tenantId, "Comment on active alarm", alarmCommentNotificationTemplate.getId(), alarmCommentRuleTriggerConfig, - List.of(originatorEntityOwnerUsers.getId()), "Send notification to tenant admins or customer users " + - "when comment is added by user on active alarm"); + List.of(tenantAdmins.getId()), "Send notification to tenant admins when comment is added by user on active alarm"); NotificationTemplate alarmAssignedNotificationTemplate = createTemplate(tenantId, "Alarm assigned notification", NotificationType.ALARM_ASSIGNMENT, "Alarm '${alarmType}' (${alarmSeverity}) was assigned to user", diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index 41a618959a..b884de0094 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service; import com.google.common.collect.Iterators; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.hibernate.validator.HibernateValidator; import org.hibernate.validator.HibernateValidatorConfiguration; import org.hibernate.validator.cfg.ConstraintMapping; @@ -29,11 +30,13 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.dao.exception.DataValidationException; -import javax.validation.Path; +import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.constraints.AssertTrue; -import java.util.List; +import javax.validation.metadata.ConstraintDescriptor; +import java.util.Collection; +import java.util.Set; import java.util.stream.Collectors; @Slf4j @@ -51,26 +54,31 @@ public class ConstraintValidator { } public static void validateFields(Object data, String errorPrefix) { - List constraintsViolations = getConstraintsViolations(data); + Set> constraintsViolations = fieldsValidator.validate(data); if (!constraintsViolations.isEmpty()) { - throw new DataValidationException(errorPrefix + String.join(", ", constraintsViolations)); + throw new DataValidationException(errorPrefix + getErrorMessage(constraintsViolations)); } } - public static List getConstraintsViolations(Object data) { - return fieldsValidator.validate(data).stream() - .map(constraintViolation -> { - String property; - if (constraintViolation.getConstraintDescriptor().getAttributes().containsKey("fieldName")) { - property = constraintViolation.getConstraintDescriptor().getAttributes().get("fieldName").toString(); - } else { - Path propertyPath = constraintViolation.getPropertyPath(); - property = Iterators.getLast(propertyPath.iterator()).toString(); - } - return property + " " + constraintViolation.getMessage(); - }) - .distinct() - .collect(Collectors.toList()); + public static String getErrorMessage(Collection> constraintsViolations) { + return constraintsViolations.stream() + .map(ConstraintValidator::getErrorMessage) + .distinct().sorted().collect(Collectors.joining(", ")); + } + + public static String getErrorMessage(ConstraintViolation constraintViolation) { + ConstraintDescriptor constraintDescriptor = constraintViolation.getConstraintDescriptor(); + String property = (String) constraintDescriptor.getAttributes().get("fieldName"); + if (StringUtils.isEmpty(property) && !(constraintDescriptor.getAnnotation() instanceof AssertTrue)) { + property = Iterators.getLast(constraintViolation.getPropertyPath().iterator()).toString(); + } + + String error = ""; + if (StringUtils.isNotEmpty(property)) { + error += property + " "; + } + error += constraintViolation.getMessage(); + return error; } private static void initializeValidators() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index 400cc0e561..24e5ceb1b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -17,10 +17,13 @@ package org.thingsboard.server.dao.service.validator; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; +import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.util.SecurityUtil; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DeviceProfile; @@ -55,10 +58,22 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; import java.util.HashSet; import java.util.List; import java.util.Set; +@Slf4j @Component public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator { @@ -85,6 +100,12 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator 0 ? System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(edge_events_ttl) : 1629158400000L; + long startTime = edgeEventsTtl > 0 ? System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(edgeEventsTtl) : 1629158400000L; long currentTime = System.currentTimeMillis(); var partitionStepInMs = TimeUnit.HOURS.toMillis(partitionSizeInHours); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index c1e3ca85ce..748c57b5dc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.util.KvUtils; import java.util.Collection; import java.util.Collections; @@ -155,10 +156,8 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + KvUtils.validate(tsKvEntry); validate(entityId); - if (tsKvEntry == null) { - throw new IncorrectParameterException("Key value entry can't be null"); - } List> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, 0L); return Futures.transform(Futures.allAsList(futures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()); @@ -175,12 +174,10 @@ public class BaseTimeseriesService implements TimeseriesService { } private ListenableFuture doSave(TenantId tenantId, EntityId entityId, List tsKvEntries, long ttl, boolean saveLatest) { + KvUtils.validate(tsKvEntries); int inserts = saveLatest ? INSERTS_PER_ENTRY : INSERTS_PER_ENTRY_WITHOUT_LATEST; List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * inserts); for (TsKvEntry tsKvEntry : tsKvEntries) { - if (tsKvEntry == null) { - throw new IncorrectParameterException("Key value entry can't be null"); - } if (saveLatest) { saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, ttl); } else { @@ -192,11 +189,9 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntries) { + KvUtils.validate(tsKvEntries); List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size()); for (TsKvEntry tsKvEntry : tsKvEntries) { - if (tsKvEntry == null) { - throw new IncorrectParameterException("Key value entry can't be null"); - } futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)); } return Futures.allAsList(futures); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java new file mode 100644 index 0000000000..8e80bd992d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java @@ -0,0 +1,35 @@ +/** + * 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; + +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.service.ConstraintValidator; + +import java.util.List; + +public class KvUtils { + public static void validate(List tsKvEntries) { + tsKvEntries.forEach(KvUtils::validate); + } + + public static void validate(KvEntry tsKvEntry) { + if (tsKvEntry == null) { + throw new IncorrectParameterException("Key value entry can't be null"); + } + ConstraintValidator.validateFields(tsKvEntry); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java new file mode 100644 index 0000000000..24a32b6a61 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java @@ -0,0 +1,50 @@ +/** + * 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.service; + +import org.junit.Assert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.exception.DataValidationException; + +class ConstraintValidatorTest { + + private static final int MIN_IN_MS = 60000; + private static final int _1M = 1_000_000; + + @Test + void validateFields() { + StringDataEntry stringDataEntryValid = new StringDataEntry("key", "value"); + StringDataEntry stringDataEntryInvalid1 = new StringDataEntry("", "value"); + + Assert.assertThrows(DataValidationException.class, () -> ConstraintValidator.validateFields(stringDataEntryInvalid1)); + ConstraintValidator.validateFields(stringDataEntryValid); + } + + @Test + void validatePerMinute() { + StringDataEntry stringDataEntryValid = new StringDataEntry("key", "value"); + + long start = System.currentTimeMillis(); + for (int i = 0; i < _1M; i++) { + ConstraintValidator.validateFields(stringDataEntryValid); + } + long end = System.currentTimeMillis(); + + Assertions.assertTrue(MIN_IN_MS > end - start); + } +} \ No newline at end of file diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index b55fba1f3b..d89211cb2f 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -82,6 +82,8 @@ redis.connection.password= security.user_login_case_sensitive=true security.claim.allowClaimingByDefault=true security.claim.duration=60000 +security.java_cacerts.path=/path/to/cacerts/file +security.java_cacerts.password=myPassword database.ts_max_intervals=700 diff --git a/monitoring/src/main/conf/logback.xml b/monitoring/src/main/conf/logback.xml index 58e49c7639..3c1e1382b7 100644 --- a/monitoring/src/main/conf/logback.xml +++ b/monitoring/src/main/conf/logback.xml @@ -33,14 +33,19 @@ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + - + diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index 5b63ba712a..4a88d5e8cd 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -54,6 +54,7 @@ As result, in REPOSITORY column, next images should be present: mvn clean install -DblackBoxTests.skip=false -Dsuite=all - - - +### To run a separate test manually on a built UI: +1. Add the black-box-tests module in the [pom.xml](../pom.xml) or add as a Maven project +2. Add Vm Option "*-DrunLocal=true -Dtb.baseUiUrl=http://localhost:4200/*" in "Run" -> "Edit Configuration" -> "Edit Configuration Templates" -> "TestNG" +3. To run a specific test, go to the test class in the [UI tests package](../black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests) and run the test. Alternatively, go to the [resources](../black-box-tests/src/test/resources) in the black-box-tests module and run the test suite that you need. \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java index be9a5c422a..c9fed6a670 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java @@ -18,10 +18,6 @@ package org.thingsboard.server.msa; import lombok.extern.slf4j.Slf4j; import org.testcontainers.DockerClientFactory; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - @Slf4j public class TestProperties { @@ -31,41 +27,26 @@ public class TestProperties { private static final ContainerTestSuite instance = ContainerTestSuite.getInstance(); - private static Properties properties; - public static String getBaseUrl() { if (instance.isActive()) { return HTTPS_URL; } - return getProperties().getProperty("tb.baseUrl"); + return System.getProperty("tb.baseUrl", "http://localhost:8080"); } public static String getBaseUiUrl() { if (instance.isActive()) { - //return "https://host.docker.internal" // this alternative requires docker-selenium.yml extra_hosts: - "host.docker.internal:host-gateway" + //return "https://host.docker.internal"; // this alternative requires docker-selenium.yml extra_hosts: - "host.docker.internal:host-gateway" //return "https://" + DockerClientFactory.instance().dockerHostIpAddress(); //this alternative will get Docker IP from testcontainers return "https://haproxy"; //communicate inside current docker-compose network to the load balancer container } - return getProperties().getProperty("tb.baseUiUrl"); + return System.getProperty("tb.baseUiUrl", "http://localhost:8080"); } public static String getWebSocketUrl() { if (instance.isActive()) { return WSS_URL; } - return getProperties().getProperty("tb.wsUrl"); - } - - private static Properties getProperties() { - if (properties == null) { - try (InputStream input = TestProperties.class.getClassLoader().getResourceAsStream("config.properties")) { - properties = new Properties(); - properties.load(input); - } catch (IOException ex) { - log.error("Exception while reading test properties " + ex.getMessage()); - } - } - return properties; + return System.getProperty("tb.wsUrl", "ws://localhost:8080"); } - } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java index 5b380221b6..3c65bc8f0a 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java @@ -151,12 +151,12 @@ public class RuleChainEditMenuTest extends AbstractDriverBaseTest { ruleChainsPage.editPencilBtn().click(); ruleChainsPage.debugCheckboxEdit().click(); ruleChainsPage.doneBtnEditView().click(); - boolean debugMode = Boolean.parseBoolean(ruleChainsPage.debugCheckboxView().getAttribute("aria-checked")); + boolean debugMode = ruleChainsPage.debugCheckboxView().getAttribute("class").contains("selected"); ruleChainsPage.editPencilBtn().click(); ruleChainsPage.debugCheckboxEdit().click(); ruleChainsPage.doneBtnEditView().click(); - Assert.assertFalse(Boolean.parseBoolean(ruleChainsPage.debugCheckboxView().getAttribute("aria-checked"))); - Assert.assertTrue(debugMode); + Assert.assertFalse(ruleChainsPage.debugCheckboxView().getAttribute("class").contains("selected"), "Debug mode disable"); + Assert.assertTrue(debugMode, "Debug mode enable"); } } \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/config.properties b/msa/black-box-tests/src/test/resources/config.properties deleted file mode 100644 index 4d8a0880a9..0000000000 --- a/msa/black-box-tests/src/test/resources/config.properties +++ /dev/null @@ -1,3 +0,0 @@ -tb.baseUrl=http://localhost:8080 -tb.baseUiUrl=http://localhost:8080 -tb.wsUrl=ws://localhost:8080 \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java index e8816ff607..beae54dce6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -43,7 +43,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @RuleNode( - type = ComponentType.ACTION, + type = ComponentType.TRANSFORMATION, name = "deduplication", configClazz = TbMsgDeduplicationNodeConfiguration.class, nodeDescription = "Deduplicate messages for a configurable period based on a specified deduplication strategy.", @@ -153,7 +153,15 @@ public class TbMsgDeduplicationNode implements TbNode { } } } - deduplicationResults.add(resultMsg); + if (resultMsg != null) { + deduplicationResults.add(TbMsg.newMsg( + resultMsg.getQueueName(), + resultMsg.getType(), + resultMsg.getOriginator(), + resultMsg.getCustomerId(), + resultMsg.getMetaData(), + resultMsg.getData())); + } } packBoundsOpt = findValidPack(msgList, deduplicationTimeoutMs); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index 857a068e8c..1f18aea86e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -66,7 +66,17 @@ public class TbMsgDelayNode implements TbNode { if (msg.getType().equals(TB_MSG_DELAY_NODE_MSG)) { TbMsg pendingMsg = pendingMsgs.remove(UUID.fromString(msg.getData())); if (pendingMsg != null) { - ctx.enqueueForTellNext(pendingMsg, SUCCESS); + ctx.enqueueForTellNext( + TbMsg.newMsg( + pendingMsg.getQueueName(), + pendingMsg.getType(), + pendingMsg.getOriginator(), + pendingMsg.getCustomerId(), + pendingMsg.getMetaData(), + pendingMsg.getData() + ), + SUCCESS + ); } } else { if (pendingMsgs.size() < config.getMaxPendingMsgs()) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java deleted file mode 100644 index 1091a7d29c..0000000000 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java +++ /dev/null @@ -1,402 +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.rule.engine.action; - -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.stubbing.Answer; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; -import org.thingsboard.rule.engine.deduplication.DeduplicationStrategy; -import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode; -import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNodeConfiguration; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@Slf4j -public class TbMsgDeduplicationNodeTest { - - private static final String MAIN_QUEUE_NAME = "Main"; - private static final String HIGH_PRIORITY_QUEUE_NAME = "HighPriority"; - private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; - - private TbContext ctx; - - private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("de-duplication-node-test"); - private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(factory); - private final int deduplicationInterval = 1; - - private TenantId tenantId; - - private TbMsgDeduplicationNode node; - private TbMsgDeduplicationNodeConfiguration config; - private TbNodeConfiguration nodeConfiguration; - - private CountDownLatch awaitTellSelfLatch; - - @BeforeEach - public void init() throws TbNodeException { - ctx = mock(TbContext.class); - - tenantId = TenantId.fromUUID(UUID.randomUUID()); - RuleNodeId ruleNodeId = new RuleNodeId(UUID.randomUUID()); - - when(ctx.getSelfId()).thenReturn(ruleNodeId); - when(ctx.getTenantId()).thenReturn(tenantId); - - doAnswer((Answer) invocationOnMock -> { - String type = (String) (invocationOnMock.getArguments())[1]; - EntityId originator = (EntityId) (invocationOnMock.getArguments())[2]; - TbMsgMetaData metaData = (TbMsgMetaData) (invocationOnMock.getArguments())[3]; - String data = (String) (invocationOnMock.getArguments())[4]; - return TbMsg.newMsg(type, originator, metaData.copy(), data); - }).when(ctx).newMsg(isNull(), eq(TB_MSG_DEDUPLICATION_TIMEOUT_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class)); - node = spy(new TbMsgDeduplicationNode()); - config = new TbMsgDeduplicationNodeConfiguration().defaultConfiguration(); - } - - private void invokeTellSelf(int maxNumberOfInvocation) { - invokeTellSelf(maxNumberOfInvocation, false, 0); - } - - private void invokeTellSelf(int maxNumberOfInvocation, boolean delayScheduleTimeout, int delayMultiplier) { - AtomicLong scheduleTimeout = new AtomicLong(deduplicationInterval); - AtomicInteger scheduleCount = new AtomicInteger(0); - doAnswer((Answer) invocationOnMock -> { - scheduleCount.getAndIncrement(); - if (scheduleCount.get() <= maxNumberOfInvocation) { - TbMsg msg = (TbMsg) (invocationOnMock.getArguments())[0]; - executorService.schedule(() -> { - try { - node.onMsg(ctx, msg); - awaitTellSelfLatch.countDown(); - } catch (ExecutionException | InterruptedException | TbNodeException e) { - log.error("Failed to execute tellSelf method call due to: ", e); - } - }, scheduleTimeout.get(), TimeUnit.SECONDS); - if (delayScheduleTimeout) { - scheduleTimeout.set(scheduleTimeout.get() * delayMultiplier); - } - } - - return null; - }).when(ctx).tellSelf(ArgumentMatchers.any(TbMsg.class), ArgumentMatchers.anyLong()); - } - - @AfterEach - public void destroy() { - executorService.shutdown(); - node.destroy(); - } - - @Test - public void given_100_messages_strategy_first_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation); - - config.setInterval(deduplicationInterval); - config.setMaxPendingMsgs(msgCount); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); - for (TbMsg msg : inputMsgs) { - node.onMsg(ctx, msg); - } - - TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2); - node.onMsg(ctx, msgToReject); - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - Assertions.assertEquals(inputMsgs.get(0), newMsgCaptor.getValue()); - } - - @Test - public void given_100_messages_strategy_last_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation); - - config.setStrategy(DeduplicationStrategy.LAST); - config.setInterval(deduplicationInterval); - config.setMaxPendingMsgs(msgCount); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); - TbMsg msgWithLatestTs = getMsgWithLatestTs(inputMsgs); - - for (TbMsg msg : inputMsgs) { - node.onMsg(ctx, msg); - } - - TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2); - node.onMsg(ctx, msgToReject); - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - Assertions.assertEquals(msgWithLatestTs, newMsgCaptor.getValue()); - } - - @Test - public void given_100_messages_strategy_all_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation); - - config.setInterval(deduplicationInterval); - config.setStrategy(DeduplicationStrategy.ALL); - config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); - config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); - for (TbMsg msg : inputMsgs) { - node.onMsg(ctx, msg); - } - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - - Assertions.assertEquals(1, newMsgCaptor.getAllValues().size()); - TbMsg outMessage = newMsgCaptor.getAllValues().get(0); - Assertions.assertEquals(getMergedData(inputMsgs), outMessage.getData()); - Assertions.assertEquals(deviceId, outMessage.getOriginator()); - Assertions.assertEquals(config.getOutMsgType(), outMessage.getType()); - Assertions.assertEquals(config.getQueueName(), outMessage.getQueueName()); - } - - @Test - public void given_100_messages_strategy_all_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3); - - config.setInterval(deduplicationInterval); - config.setStrategy(DeduplicationStrategy.ALL); - config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); - config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500); - for (TbMsg msg : firstMsgPack) { - node.onMsg(ctx, msg); - } - long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); - - List secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); - for (TbMsg msg : secondMsgPack) { - node.onMsg(ctx, msg); - } - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - - List resultMsgs = newMsgCaptor.getAllValues(); - Assertions.assertEquals(2, resultMsgs.size()); - - TbMsg firstMsg = resultMsgs.get(0); - Assertions.assertEquals(getMergedData(firstMsgPack), firstMsg.getData()); - Assertions.assertEquals(deviceId, firstMsg.getOriginator()); - Assertions.assertEquals(config.getOutMsgType(), firstMsg.getType()); - Assertions.assertEquals(config.getQueueName(), firstMsg.getQueueName()); - - TbMsg secondMsg = resultMsgs.get(1); - Assertions.assertEquals(getMergedData(secondMsgPack), secondMsg.getData()); - Assertions.assertEquals(deviceId, secondMsg.getOriginator()); - Assertions.assertEquals(config.getOutMsgType(), secondMsg.getType()); - Assertions.assertEquals(config.getQueueName(), secondMsg.getQueueName()); - } - - @Test - public void given_100_messages_strategy_last_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException { - int wantedNumberOfTellSelfInvocation = 2; - int msgCount = 100; - awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); - invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3); - - config.setInterval(deduplicationInterval); - config.setStrategy(DeduplicationStrategy.LAST); - nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - node.init(ctx, nodeConfiguration); - - DeviceId deviceId = new DeviceId(UUID.randomUUID()); - long currentTimeMillis = System.currentTimeMillis(); - - List firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500); - for (TbMsg msg : firstMsgPack) { - node.onMsg(ctx, msg); - } - long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); - TbMsg msgWithLatestTsInFirstPack = getMsgWithLatestTs(firstMsgPack); - - List secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); - for (TbMsg msg : secondMsgPack) { - node.onMsg(ctx, msg); - } - TbMsg msgWithLatestTsInSecondPack = getMsgWithLatestTs(secondMsgPack); - - awaitTellSelfLatch.await(); - - ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); - ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); - - verify(ctx, times(msgCount)).ack(any()); - verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - - List resultMsgs = newMsgCaptor.getAllValues(); - Assertions.assertEquals(2, resultMsgs.size()); - Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInFirstPack)); - Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInSecondPack)); - } - - private TbMsg getMsgWithLatestTs(List firstMsgPack) { - int indexOfLastMsgInArray = firstMsgPack.size() - 1; - int indexToSetMaxTs = new Random().nextInt(indexOfLastMsgInArray) + 1; - TbMsg currentMaxTsMsg = firstMsgPack.get(indexOfLastMsgInArray); - TbMsg newLastMsgOfArray = firstMsgPack.get(indexToSetMaxTs); - firstMsgPack.set(indexOfLastMsgInArray, newLastMsgOfArray); - firstMsgPack.set(indexToSetMaxTs, currentMaxTsMsg); - return currentMaxTsMsg; - } - - private List getTbMsgs(DeviceId deviceId, int msgCount, long currentTimeMillis, int initTsStep) { - List inputMsgs = new ArrayList<>(); - var ts = currentTimeMillis + initTsStep; - for (int i = 0; i < msgCount; i++) { - inputMsgs.add(createMsg(deviceId, ts)); - ts += 2; - } - return inputMsgs; - } - - private TbMsg createMsg(DeviceId deviceId, long ts) { - ObjectNode dataNode = JacksonUtil.newObjectNode(); - dataNode.put("deviceId", deviceId.getId().toString()); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("ts", String.valueOf(ts)); - return TbMsg.newMsg( - MAIN_QUEUE_NAME, - SessionMsgType.POST_TELEMETRY_REQUEST.name(), - deviceId, - metaData, - JacksonUtil.toString(dataNode)); - } - - private String getMergedData(List msgs) { - ArrayNode mergedData = JacksonUtil.OBJECT_MAPPER.createArrayNode(); - msgs.forEach(msg -> { - ObjectNode msgNode = JacksonUtil.newObjectNode(); - msgNode.set("msg", JacksonUtil.toJsonNode(msg.getData())); - msgNode.set("metadata", JacksonUtil.valueToTree(msg.getMetaData().getData())); - mergedData.add(msgNode); - }); - return JacksonUtil.toString(mergedData); - } - -} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java index 475a98e2a2..1badecfa92 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java @@ -174,7 +174,16 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - Assertions.assertEquals(inputMsgs.get(0), newMsgCaptor.getValue()); + + TbMsg firstMsg = inputMsgs.get(0); + TbMsg actualMsg = newMsgCaptor.getValue(); + // msg ids should be different because we create new msg before enqueueForTellNext + Assertions.assertNotEquals(firstMsg.getId(), actualMsg.getId()); + Assertions.assertEquals(firstMsg.getOriginator(), actualMsg.getOriginator()); + Assertions.assertEquals(firstMsg.getCustomerId(), actualMsg.getCustomerId()); + Assertions.assertEquals(firstMsg.getData(), actualMsg.getData()); + Assertions.assertEquals(firstMsg.getMetaData(), actualMsg.getMetaData()); + Assertions.assertEquals(firstMsg.getType(), actualMsg.getType()); } @Test @@ -213,7 +222,15 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); - Assertions.assertEquals(msgWithLatestTs, newMsgCaptor.getValue()); + + TbMsg actualMsg = newMsgCaptor.getValue(); + // msg ids should be different because we create new msg before enqueueForTellNext + Assertions.assertNotEquals(msgWithLatestTs.getId(), actualMsg.getId()); + Assertions.assertEquals(msgWithLatestTs.getOriginator(), actualMsg.getOriginator()); + Assertions.assertEquals(msgWithLatestTs.getCustomerId(), actualMsg.getCustomerId()); + Assertions.assertEquals(msgWithLatestTs.getData(), actualMsg.getData()); + Assertions.assertEquals(msgWithLatestTs.getMetaData(), actualMsg.getMetaData()); + Assertions.assertEquals(msgWithLatestTs.getType(), actualMsg.getType()); } @Test @@ -350,8 +367,24 @@ public class TbMsgDeduplicationNodeTest { List resultMsgs = newMsgCaptor.getAllValues(); Assertions.assertEquals(2, resultMsgs.size()); - Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInFirstPack)); - Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInSecondPack)); + + // verify that newMsg is called but content of messages is the same as in the last msg for the first pack. + TbMsg actualMsg = resultMsgs.get(0); + Assertions.assertNotEquals(msgWithLatestTsInFirstPack.getId(), actualMsg.getId()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getOriginator(), actualMsg.getOriginator()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getCustomerId(), actualMsg.getCustomerId()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getData(), actualMsg.getData()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getMetaData(), actualMsg.getMetaData()); + Assertions.assertEquals(msgWithLatestTsInFirstPack.getType(), actualMsg.getType()); + + // verify that newMsg is called but content of messages is the same as in the last msg for the second pack. + actualMsg = resultMsgs.get(1); + Assertions.assertNotEquals(msgWithLatestTsInSecondPack.getId(), actualMsg.getId()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getOriginator(), actualMsg.getOriginator()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getCustomerId(), actualMsg.getCustomerId()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getData(), actualMsg.getData()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getMetaData(), actualMsg.getMetaData()); + Assertions.assertEquals(msgWithLatestTsInSecondPack.getType(), actualMsg.getType()); } private TbMsg getMsgWithLatestTs(List firstMsgPack) { diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 89074fd649..ec60357764 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -114,7 +114,7 @@ transport: # Server SSL credentials credentials: # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) - type: "${MQTT_SSL_CREDENTIALS_TYPE:PEM}" + type: "${MQTT_SSL_CREDENTIALS_TYPE:PEM}" # PEM server credentials pem: # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index de5e97f79f..2c29990466 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -31,6 +31,7 @@ "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "~0.6.0", "@geoman-io/leaflet-geoman-free": "^2.13.0", + "@iplab/ngx-color-picker": "^15.0.2", "@juggle/resize-observer": "^3.4.0", "@mat-datetimepicker/core": "~11.0.3", "@material-ui/core": "4.12.3", @@ -73,7 +74,6 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.42", "ngx-clipboard": "^15.1.0", - "ngx-color-picker": "^14.0.0", "ngx-daterangepicker-material": "^6.0.4", "ngx-drag-drop": "^15.0.1", "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/2.0.0", @@ -97,7 +97,7 @@ "screenfull": "^6.0.2", "split.js": "^1.6.5", "systemjs": "6.11.0", - "tinycolor2": "~1.4.2", + "tinycolor2": "^1.6.0", "tinymce": "~5.10.7", "tooltipster": "^4.2.8", "ts-transformer-keys": "^0.4.4", diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html index f01c550444..fb84ca4481 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html @@ -22,7 +22,7 @@ (focusin)="onFocus()" [matAutocomplete]="userAutocomplete"> search - @@ -41,10 +41,12 @@ - - - {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} - + +
+ + {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index cd711f9e12..afbaa924ac 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -27,36 +27,80 @@ {{ 'device-profile.provision-strategy-required' | translate }} -
- - device-profile.provision-device-key - - - - {{ 'device-profile.provision-device-key-required' | translate }} - - - - device-profile.provision-device-secret - - - - {{ 'device-profile.provision-device-secret-required' | translate }} - - -
+
+ + + + + + + +
+ + +
+ + {{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }} + +
+ + device-profile.provision-strategy-x509.certificate-value + + + {{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }} + + + + device-profile.provision-strategy-x509.cn-regex-variable + + + + {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} + + device-profile.provision-strategy-x509.cn-regex-variable-hint + +
+ +
+ + device-profile.provision-device-key + + + + {{ 'device-profile.provision-device-key-required' | translate }} + + + + device-profile.provision-device-secret + + + + {{ 'device-profile.provision-device-secret-required' | translate }} + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 872571dc20..3387d71704 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -32,7 +32,7 @@ import { DeviceProvisionType, deviceProvisionTypeTranslationMap } from '@shared/models/device.models'; -import { generateSecret, isDefinedAndNotNull } from '@core/utils'; +import { generateSecret, isBoolean, isDefinedAndNotNull } from '@core/utils'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -86,14 +86,36 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup = this.fb.group({ type: [DeviceProvisionType.DISABLED, Validators.required], provisionDeviceSecret: [{value: null, disabled: true}, Validators.required], - provisionDeviceKey: [{value: null, disabled: true}, Validators.required] + provisionDeviceKey: [{value: null, disabled: true}, Validators.required], + certificateValue: [{value: null, disabled: true}, Validators.required], + certificateRegExPattern: [{value: null, disabled: true}, Validators.required], + allowCreateNewDevicesByX509Certificate: [{value: null, disabled: true}, Validators.required] }); this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => { if (type === DeviceProvisionType.DISABLED) { - this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null, {emitEvent: false}); - this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null); + for (const field in this.provisionConfigurationFormGroup.controls) { + if (field !== 'type') { + const control = this.provisionConfigurationFormGroup.get(field); + control.disable({emitEvent: false}); + control.patchValue(null, {emitEvent: false}); + } + } + } else if (type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) { + const certificateValue: string = this.provisionConfigurationFormGroup.get('certificateValue').value; + if (!certificateValue || !certificateValue.length) { + this.provisionConfigurationFormGroup.get('certificateValue').patchValue(null, {emitEvent: false}); + } + const certificateRegExPattern: string = this.provisionConfigurationFormGroup.get('certificateRegExPattern').value; + if (!certificateRegExPattern || !certificateRegExPattern.length) { + this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue('(.*)', {emitEvent: false}); + } + const allowCreateNewDevicesByX509Certificate: boolean | null = this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').value; + if (!isBoolean(allowCreateNewDevicesByX509Certificate)) { + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(true, {emitEvent: false}); + } + this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('certificateRegExPattern').enable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').enable({emitEvent: false}); } else { const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value; if (!provisionDeviceSecret || !provisionDeviceSecret.length) { @@ -120,16 +142,19 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu } writeValue(value: DeviceProvisionConfiguration | null): void { - if (isDefinedAndNotNull(value)){ + if (isDefinedAndNotNull(value)) { + if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) { + value.certificateValue = value.provisionDeviceSecret; + } this.provisionConfigurationFormGroup.patchValue(value, {emitEvent: false}); } else { this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED}); } } - setDisabledState(isDisabled: boolean){ + setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; - if (this.disabled){ + if (this.disabled) { this.provisionConfigurationFormGroup.disable({emitEvent: false}); } else { if (this.provisionConfigurationFormGroup.get('type').value !== DeviceProvisionType.DISABLED) { @@ -150,8 +175,12 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu private updateModel(): void { let deviceProvisionConfiguration: DeviceProvisionConfiguration = null; + this.resetFormControls(this.provisionConfigurationFormGroup.value); if (this.provisionConfigurationFormGroup.valid) { deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue(); + if (deviceProvisionConfiguration.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) { + deviceProvisionConfiguration.provisionDeviceSecret = deviceProvisionConfiguration.certificateValue; + } } this.propagateChange(deviceProvisionConfiguration); } @@ -166,4 +195,15 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu horizontalPosition: 'right' })); } + + private resetFormControls(value: DeviceProvisionConfiguration) { + if (value.type === DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES || value.type === DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES) { + this.provisionConfigurationFormGroup.get('certificateValue').reset({value: null, disabled: true}, {emitEvent: false}); + this.provisionConfigurationFormGroup.get('certificateRegExPattern').reset({value: null, disabled: true}, {emitEvent: false}); + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').reset({value: null, disabled: true}, {emitEvent: false}); + } else if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) { + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').reset({value: null, disabled: true}, {emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').reset({value: null, disabled: true}, {emitEvent: false}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts index 9adcf9c168..db6539318d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -104,7 +104,9 @@ export class DeviceProfileComponent extends EntityComponent { const deviceProvisionConfiguration: DeviceProvisionConfiguration = { type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED, provisionDeviceKey: entity?.provisionDeviceKey, - provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret + provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret, + certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern, + allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate }; const form = this.fb.group( { @@ -185,7 +187,9 @@ export class DeviceProfileComponent extends EntityComponent { const deviceProvisionConfiguration: DeviceProvisionConfiguration = { type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED, provisionDeviceKey: entity?.provisionDeviceKey, - provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret + provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret, + certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern, + allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate }; this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({type: entity.type}, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html index 17feca514b..45cf2040b2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -169,6 +169,6 @@ {{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }} -
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index efa6ef7e13..0bbb6a5926 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -483,6 +483,9 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index e070d760ad..cbb5c7444e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -73,6 +73,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA tenantEntityExportRateLimit: [null, []], tenantEntityImportRateLimit: [null, []], tenantNotificationRequestsRateLimit: [null, []], + tenantNotificationRequestsPerRuleRateLimit: [null, []], maxTransportMessages: [null, [Validators.required, Validators.min(0)]], maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]], maxREExecutions: [null, [Validators.required, Validators.min(0)]], diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts index 09ae3f5139..26326e9f63 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts @@ -34,7 +34,8 @@ export enum RateLimitsType { CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION = 'CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION', TENANT_ENTITY_EXPORT_RATE_LIMIT = 'TENANT_ENTITY_EXPORT_RATE_LIMIT', TENANT_ENTITY_IMPORT_RATE_LIMIT = 'TENANT_ENTITY_IMPORT_RATE_LIMIT', - TENANT_NOTIFICATION_REQUEST_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUEST_RATE_LIMIT' + TENANT_NOTIFICATION_REQUEST_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUEST_RATE_LIMIT', + TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT' } export const rateLimitsLabelTranslationMap = new Map( @@ -52,6 +53,7 @@ export const rateLimitsLabelTranslationMap = new Map( [RateLimitsType.TENANT_ENTITY_EXPORT_RATE_LIMIT, 'tenant-profile.tenant-entity-export-rate-limit'], [RateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT, 'tenant-profile.tenant-entity-import-rate-limit'], [RateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT, 'tenant-profile.tenant-notification-request-rate-limit'], + [RateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT, 'tenant-profile.tenant-notification-requests-per-rule-rate-limit'], ] ); @@ -70,6 +72,7 @@ export const rateLimitsDialogTitleTranslationMap = new Map { - let message: string; - if (!result.error) { - if (result.done && !result.added && !result.modified && !result.removed) { - message = this.translate.instant('version-control.nothing-to-commit'); + this.versionCreateResultSubscription = this.versionCreateResult$.subscribe({ + next: (result) => { + let message: string; + if (!result.error) { + if (result.done && !result.added && !result.modified && !result.removed) { + message = this.translate.instant('version-control.nothing-to-commit'); + } else { + message = this.translate.instant('version-control.version-create-result', + {added: result.added, modified: result.modified, removed: result.removed}); + } } else { - message = this.translate.instant('version-control.version-create-result', - {added: result.added, modified: result.modified, removed: result.removed}); - } - } else { message = result.error; - } - this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(message); - this.versionCreateResult = result; - this.versionCreateBranch = request.branch; - this.cd.detectChanges(); - if (this.popoverComponent) { - this.popoverComponent.updatePosition(); - } - }, - (error) => { - this.hasError = true; - this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(parseHttpErrorMessage(error, this.translate).message); - this.cd.detectChanges(); - if (this.popoverComponent) { - this.popoverComponent.updatePosition(); + } + this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(message); + this.versionCreateResult = result; + this.versionCreateBranch = request.branch; + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } + }, + error: (error) => { + this.hasError = true; + this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(parseHttpErrorMessage(error, this.translate).message); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } } }); } diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts index e87d712534..b41e51c5c2 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts @@ -18,12 +18,12 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { AbstractControl, ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - NG_VALIDATORS, - NG_VALUE_ACCESSOR, Validator, Validators } from '@angular/forms'; @@ -104,6 +104,9 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements this.entityTypesVersionCreateFormGroup.disable({emitEvent: false}); } else { this.entityTypesVersionCreateFormGroup.enable({emitEvent: false}); + (this.entityTypesVersionCreateFormGroup.get('entityTypes') as UntypedFormArray).controls.forEach( + control => this.updateEntityTypeValidators(control) + ); } } @@ -147,7 +150,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements } ); this.updateEntityTypeValidators(entityTypeControl); - entityTypeControl.get('config').get('allEntities').valueChanges.subscribe(() => { + entityTypeControl.get('config.allEntities').valueChanges.subscribe(() => { this.updateEntityTypeValidators(entityTypeControl); }); return entityTypeControl; @@ -229,7 +232,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements allowedEntityTypes(entityTypeControl?: AbstractControl): Array { let res = [...exportableEntityTypes]; const currentEntityType: EntityType = entityTypeControl?.get('entityType')?.value; - const value: [{entityType: string, config: EntityTypeVersionCreateConfig}] = + const value: [{entityType: string; config: EntityTypeVersionCreateConfig}] = this.entityTypesVersionCreateFormGroup.get('entityTypes').value || []; const usedEntityTypes = value.map(val => val.entityType).filter(val => val); res = res.filter(entityType => !usedEntityTypes.includes(entityType) || entityType === currentEntityType); @@ -237,7 +240,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements } private updateModel() { - const value: [{entityType: string, config: EntityTypeVersionCreateConfig}] = + const value: [{entityType: string; config: EntityTypeVersionCreateConfig}] = this.entityTypesVersionCreateFormGroup.get('entityTypes').value || []; let modelValue: {[entityType: string]: EntityTypeVersionCreateConfig} = null; if (value && value.length) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts index 5229212418..31f40594ba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -18,13 +18,11 @@ import * as CanvasGauges from 'canvas-gauges'; import { FontSettings, getFontFamily } from '@home/components/widget/lib/settings.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { isDefined } from '@core/utils'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import Highlight = CanvasGauges.Highlight; import BaseGauge = CanvasGauges.BaseGauge; import GenericOptions = CanvasGauges.GenericOptions; -const tinycolor = tinycolor_; - export type AnimationRule = 'linear' | 'quad' | 'quint' | 'cycle' | 'bounce' | 'elastic' | 'dequad' | 'dequint' | 'decycle' | 'debounce' | 'delastic'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts index 6996155b4b..c0206f4f9b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts @@ -15,20 +15,17 @@ /// import * as CanvasGauges from 'canvas-gauges'; -import { JsonSettingsSchema } from '@shared/models/widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { TbAnalogueGauge } from '@home/components/widget/lib/analogue-gauge.models'; import { AnalogueLinearGaugeSettings } from '@home/components/widget/lib/analogue-linear-gauge.models'; import { isDefined } from '@core/utils'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import LinearGaugeOptions = CanvasGauges.LinearGaugeOptions; import LinearGauge = CanvasGauges.LinearGauge; import BaseGauge = CanvasGauges.BaseGauge; -const tinycolor = tinycolor_; - // @dynamic export class TbAnalogueLinearGauge extends TbAnalogueGauge{ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts index 903865a957..71987932a4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts @@ -16,14 +16,12 @@ import * as CanvasGauges from 'canvas-gauges'; import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import { ColorFormats } from 'tinycolor2'; import { isDefined, isDefinedAndNotNull, isString, isUndefined, padValue } from '@core/utils'; import GenericOptions = CanvasGauges.GenericOptions; import BaseGauge = CanvasGauges.BaseGauge; -const tinycolor = tinycolor_; - export type GaugeType = 'arc' | 'donut' | 'horizontalBar' | 'verticalBar'; export interface DigitalGaugeColorRange { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index fcb90b06a4..84e99b3b80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -23,7 +23,7 @@ import { DigitalGaugeSettings, FixedLevelColors } from '@home/components/widget/lib/digital-gauge.models'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { prepareFontSettings } from '@home/components/widget/lib/settings.models'; import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; @@ -41,8 +41,6 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { EMPTY, Observable } from 'rxjs'; import GenericOptions = CanvasGauges.GenericOptions; -const tinycolor = tinycolor_; - // @dynamic export class TbCanvasDigitalGauge { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index aae7d2c37a..03e29769ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -54,14 +54,13 @@ import { TooltipValueFormatFunction } from './flot-widget.models'; import * as moment_ from 'moment'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import { AggregationType } from '@shared/models/time/time.models'; import { CancelAnimationFrame } from '@core/services/raf.service'; import { UtilsService } from '@core/services/utils.service'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import Timeout = NodeJS.Timeout; -const tinycolor = tinycolor_; const moment = moment_; export class TbFlot { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html index a2169cc129..99740ac601 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html @@ -31,7 +31,7 @@ entity.entity - {{ entity.entityName }} + {{ entity.entityParseName }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts index 9822f6dca9..f2b3d09b5c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormattedData } from '@shared/models/widget.models'; export interface SelectEntityDialogData { @@ -33,14 +33,13 @@ export interface SelectEntityDialogData { styleUrls: ['./select-entity-dialog.component.scss'] }) export class SelectEntityDialogComponent extends DialogComponent { - - selectEntityFormGroup: UntypedFormGroup; + selectEntityFormGroup: FormGroup; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: SelectEntityDialogData, public dialogRef: MatDialogRef, - public fb: UntypedFormBuilder) { + public fb: FormBuilder) { super(store, router, dialogRef); this.selectEntityFormGroup = this.fb.group( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index a78dd27431..7b63176203 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -36,7 +36,7 @@ import { Observable, of } from 'rxjs'; import { Polyline } from './polyline'; import { Polygon } from './polygon'; import { Circle } from './circle'; -import { createTooltip, isCutPolygon, isJSON } from '@home/components/widget/lib/maps/maps-utils'; +import { createTooltip, entitiesParseName, isCutPolygon, isJSON } from '@home/components/widget/lib/maps/maps-utils'; import { checkLngLat, createLoadingDiv } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { @@ -182,20 +182,40 @@ export default abstract class LeafletMap { private selectEntityWithoutLocationDialog(shapes: L.PM.SUPPORTED_SHAPES): Observable { let entities; + let labelSettings; switch (shapes) { case 'Polygon': case 'Rectangle': entities = this.datasources.filter(pData => !this.isValidPolygonPosition(pData)); + labelSettings = { + showLabel: this.options.showPolygonLabel, + useLabelFunction: this.options.usePolygonLabelFunction, + parsedLabelFunction: this.options.parsedPolygonLabelFunction, + label: this.options.polygonLabel + }; break; case 'Marker': entities = this.datasources.filter(mData => !this.extractPosition(mData)); + labelSettings = { + showLabel: this.options.showLabel, + useLabelFunction: this.options.useLabelFunction, + parsedLabelFunction: this.options.parsedLabelFunction, + label: this.options.label + }; break; case 'Circle': entities = this.datasources.filter(mData => !this.isValidCircle(mData)); + labelSettings = { + showLabel: this.options.showCircleLabel, + useLabelFunction: this.options.useCircleLabelFunction, + parsedLabelFunction: this.options.parsedCircleLabelFunction, + label: this.options.circleLabel + }; break; default: return of(null); } + entities = entitiesParseName(entities, labelSettings); if (entities.length === 1) { return of(entities[0]); } @@ -219,38 +239,38 @@ export default abstract class LeafletMap { let customTranslation; switch (type) { case 'tbMarker': - tooltipText = this.translateService.instant('widgets.maps.tooltips.placeMarker', {entityName: data.entityName}); + tooltipText = this.translateService.instant('widgets.maps.tooltips.placeMarker', {entityName: data.entityParseName}); // @ts-ignore this.map.pm.Draw.tbMarker._hintMarker.setTooltipContent(tooltipText); break; case 'tbCircle': - tooltipText = this.translateService.instant('widgets.maps.tooltips.startCircle', {entityName: data.entityName}); + tooltipText = this.translateService.instant('widgets.maps.tooltips.startCircle', {entityName: data.entityParseName}); // @ts-ignore this.map.pm.Draw.tbCircle._hintMarker.setTooltipContent(tooltipText); customTranslation = { tooltips: { - finishCircle: this.translateService.instant('widgets.maps.tooltips.finishCircle', {entityName: data.entityName}) + finishCircle: this.translateService.instant('widgets.maps.tooltips.finishCircle', {entityName: data.entityParseName}) } }; break; case 'tbRectangle': - tooltipText = this.translateService.instant('widgets.maps.tooltips.firstVertex', {entityName: data.entityName}); + tooltipText = this.translateService.instant('widgets.maps.tooltips.firstVertex', {entityName: data.entityParseName}); // @ts-ignore this.map.pm.Draw.tbRectangle._hintMarker.setTooltipContent(tooltipText); customTranslation = { tooltips: { - finishRect: this.translateService.instant('widgets.maps.tooltips.finishRect', {entityName: data.entityName}) + finishRect: this.translateService.instant('widgets.maps.tooltips.finishRect', {entityName: data.entityParseName}) } }; break; case 'tbPolygon': - tooltipText = this.translateService.instant('widgets.maps.tooltips.firstVertex', {entityName: data.entityName}); + tooltipText = this.translateService.instant('widgets.maps.tooltips.firstVertex', {entityName: data.entityParseName}); // @ts-ignore this.map.pm.Draw.tbPolygon._hintMarker.setTooltipContent(tooltipText); customTranslation = { tooltips: { - continueLine: this.translateService.instant('widgets.maps.tooltips.continueLine', {entityName: data.entityName}), - finishPoly: this.translateService.instant('widgets.maps.tooltips.finishPoly', {entityName: data.entityName}) + continueLine: this.translateService.instant('widgets.maps.tooltips.continueLine', {entityName: data.entityParseName}), + finishPoly: this.translateService.instant('widgets.maps.tooltips.finishPoly', {entityName: data.entityParseName}) } }; break; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index 4b4de38712..4aad829eac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -16,9 +16,12 @@ import L from 'leaflet'; import { + GenericFunction, ShowTooltipAction, WidgetToolipSettings } from './map-models'; -import { Datasource } from '@app/shared/models/widget.models'; +import { Datasource, FormattedData } from '@app/shared/models/widget.models'; +import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils'; +import { parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; export function createTooltip(target: L.Layer, settings: Partial, @@ -83,3 +86,27 @@ export function isJSON(data: string): boolean { return false; } } + +interface labelSettings { + showLabel: boolean; + useLabelFunction: boolean; + parsedLabelFunction: GenericFunction; + label: string; +} + +export function entitiesParseName(entities: FormattedData[], labelSettings: labelSettings): FormattedData[] { + const div = document.createElement('div'); + for (const entity of entities) { + if (labelSettings?.showLabel) { + const pattern = labelSettings.useLabelFunction ? safeExecute(labelSettings.parsedLabelFunction, + [entity, entities, entity.dsIndex]) : labelSettings.label; + const markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); + const replaceInfoLabelMarker = processDataPattern(pattern, entity); + div.innerHTML = fillDataPattern(markerLabelText, replaceInfoLabelMarker, entity); + entity.entityParseName = div.textContent || div.innerText || ''; + } else { + entity.entityParseName = entity.entityName; + } + } + return entities; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html index 256f6e7f9f..0a757fd34f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -85,6 +85,39 @@ {{key.label}} +
+ + {{key.label}} + + + {{key.settings.icon}} + + icon + + + + + {{ getErrorMessageText(key.settings,'required') }} + + + {{ getErrorMessageText(key.settings,'invalidJSON') | translate }} + + +
any; @@ -90,6 +95,7 @@ interface MultipleInputWidgetDataKeySettings { invalidDateErrorMessage?: string; minValueErrorMessage?: string; maxValueErrorMessage?: string; + invalidJsonErrorMessage?: string; useCustomIcon: boolean; icon: string; customIcon: string ; @@ -103,6 +109,9 @@ interface MultipleInputWidgetDataKeySettings { useSetValueFunction?: boolean; setValueFunctionBody?: string; setValueFunction?: ConvertSetValueFunction; + dialogTitle?: string; + saveButtonLabel?: string; + cancelButtonLabel?: string; } interface MultipleInputWidgetDataKey extends DataKey { @@ -161,7 +170,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni private fb: UntypedFormBuilder, private attributeService: AttributeService, private translate: TranslateService, - private sanitizer: DomSanitizer) { + private sanitizer: DomSanitizer, + private dialog: MatDialog) { super(store); } @@ -425,6 +435,13 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni case 'select': value = keyValue !== null ? keyValue.toString() : null; break; + case 'JSON': + try { + value = JSON.parse(keyValue); + } catch (e) { + value = keyValue ? keyValue : null; + } + break; default: value = keyValue; } @@ -520,6 +537,10 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni errorMessage = keySettings.invalidDateErrorMessage; defaultMessage = 'widgets.input-widgets.invalid-date'; break; + case 'invalidJSON': + errorMessage = keySettings.invalidJsonErrorMessage; + defaultMessage = 'widgets.input-widgets.json-invalid'; + break; default: return ''; } @@ -670,8 +691,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } }); if (tasks.length) { - forkJoin(tasks).subscribe( - () => { + forkJoin(tasks).subscribe({ + next: () => { this.multipleInputFormGroup.markAsPristine(); this.ctx.detectChanges(); this.isSavingInProgress = false; @@ -680,13 +701,13 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni 1000, 'bottom', 'left', this.toastTargetId); } }, - () => { + error: () => { this.isSavingInProgress = false; if (this.settings.showResultMessage) { this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', this.toastTargetId); } - }); + }}); } else { this.multipleInputFormGroup.markAsPristine(); this.ctx.detectChanges(); @@ -718,4 +739,31 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni }); this.multipleInputFormGroup.markAsPristine(); } + + openEditJSONDialog($event: Event, key: MultipleInputWidgetDataKey, source: MultipleInputWidgetSource) { + if ($event) { + $event.stopPropagation(); + } + const formControl = this.multipleInputFormGroup.controls[key.formId]; + this.dialog.open(JsonObjectEditDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + jsonValue: formControl.value, + title: key.settings.dialogTitle, + saveLabel: key.settings.saveButtonLabel, + cancelLabel: key.settings.cancelButtonLabel + } + }).afterClosed().subscribe( + (res) => { + if (!isEqual(res, formControl.value)) { + formControl.patchValue(res); + formControl.markAsDirty(); + if(!this.settings.showActionButtons) { + this.inputChanged(source, key); + } + } + } + ); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts index 17f46fda3c..36a2bca18d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts @@ -22,12 +22,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { isDefined, isNumber } from '@core/utils'; import { CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import { ResizeObserver } from '@juggle/resize-observer'; import GenericOptions = CanvasGauges.GenericOptions; -const tinycolor = tinycolor_; - interface KnobSettings { minValue: number; maxValue: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts index 430aafb996..8a048100d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts @@ -20,7 +20,7 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { isDefined } from '@core/utils'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import { UtilsService } from '@core/services/utils.service'; import { IWidgetSubscription, SubscriptionInfo, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { DatasourceType, widgetType } from '@shared/models/widget.models'; @@ -28,8 +28,6 @@ import { EntityType } from '@shared/models/entity-type.models'; import { ResizeObserver } from '@juggle/resize-observer'; import Timeout = NodeJS.Timeout; -const tinycolor = tinycolor_; - const checkStatusPollingInterval = 10000; type RetrieveValueMethod = 'attribute' | 'timeseries'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html index 186583d5c1..3c62810000 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html @@ -66,6 +66,9 @@ {{ 'widgets.input-widgets.datakey-value-type-select' | translate }} + + {{ 'widgets.input-widgets.datakey-value-type-json' | translate }} + @@ -179,6 +182,28 @@ widgets.input-widgets.invalid-date-error-message + + widgets.input-widgets.invalid-JSON-error-message + + + +
+ widgets.input-widgets.dialog-editor-settings + + widgets.input-widgets.title + + +
+ + widgets.input-widgets.save-button-label + + + + widgets.input-widgets.cancel-button-label + + +
widgets.input-widgets.icon-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts index 56ab12605d..7eff1bdc80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts @@ -24,6 +24,7 @@ import { dataKeySelectOptionValidator } from '@home/components/widget/lib/settings/input/datakey-select-option.component'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { MultipleInputWidgetDataKeyValueType } from '@home/components/widget/lib/multiple-input-widget.component'; @Component({ selector: 'tb-update-multiple-attributes-key-settings', @@ -62,6 +63,11 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings minValueErrorMessage: '', maxValueErrorMessage: '', invalidDateErrorMessage: '', + invalidJsonErrorMessage: '', + + dialogTitle: '', + saveButtonLabel: '', + cancelButtonLabel: '', useCustomIcon: false, icon: '', @@ -107,6 +113,13 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings minValueErrorMessage: [settings.minValueErrorMessage, []], maxValueErrorMessage: [settings.maxValueErrorMessage, []], invalidDateErrorMessage: [settings.invalidDateErrorMessage, []], + invalidJsonErrorMessage: [settings.invalidJsonErrorMessage, []], + + // Dialog settings + + dialogTitle: [settings.dialogTitle, []], + saveButtonLabel: [settings.saveButtonLabel, []], + cancelButtonLabel: [settings.cancelButtonLabel, []], // Icon settings @@ -130,7 +143,8 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings protected updateValidators(emitEvent: boolean) { const dataKeyHidden: boolean = this.updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value; - const dataKeyValueType: string = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; + const dataKeyValueType: MultipleInputWidgetDataKeyValueType = + this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; const isEditable: string = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value; const useCustomIcon: boolean = this.updateMultipleAttributesKeySettingsForm.get('useCustomIcon').value; @@ -165,6 +179,11 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings this.updateMultipleAttributesKeySettingsForm.get('maxValueErrorMessage').enable({emitEvent: false}); } else if (dataKeyValueType === 'dateTime' || dataKeyValueType === 'date' || dataKeyValueType === 'time') { this.updateMultipleAttributesKeySettingsForm.get('invalidDateErrorMessage').enable({emitEvent: false}); + } else if (dataKeyValueType === 'JSON') { + this.updateMultipleAttributesKeySettingsForm.get('invalidJsonErrorMessage').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('dialogTitle').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('saveButtonLabel').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('cancelButtonLabel').enable({emitEvent: false}); } if (required) { this.updateMultipleAttributesKeySettingsForm.get('requiredErrorMessage').enable({emitEvent: false}); @@ -243,8 +262,9 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings displayErrorMessagesSection(): boolean { const dataKeyHidden: boolean = this.updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value; const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; - const dataKeyValueType: string = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; - return !dataKeyHidden && (required || (['integer', 'double', 'dateTime', 'date', 'time'].includes(dataKeyValueType))); + const dataKeyValueType: MultipleInputWidgetDataKeyValueType = + this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; + return !dataKeyHidden && (required || (['integer', 'double', 'dateTime', 'date', 'time', 'JSON'].includes(dataKeyValueType))); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 20cf5860b5..3cd343ea57 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -18,15 +18,13 @@ import { EntityId } from '@shared/models/id/entity-id'; import { DataKey, FormattedData, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models'; import { getDescendantProp, isDefined, isNotEmptyStr } from '@core/utils'; import { AlarmDataInfo, alarmFields } from '@shared/models/alarm.models'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -const tinycolor = tinycolor_; - type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile'; type ColumnSelectionOptions = 'enabled' | 'disabled'; 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 1f946e791b..50627212ee 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 @@ -42,14 +42,12 @@ import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; import { TenantId } from '@app/shared/models/id/tenant-id'; import { SharedModule } from '@shared/shared.module'; import { MODULES_MAP } from '@shared/public-api'; -import * as tinycolor_ from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import moment from 'moment'; import { IModulesMap } from '@modules/common/modules-map.models'; import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; -const tinycolor = tinycolor_; - @Injectable() export class WidgetComponentService { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index a4e34029f5..b16322bea5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -271,7 +271,7 @@ {{ 'widget-config.alarm-source' | translate }} -
+
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html index 15759508c2..ef0c452a7b 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html @@ -40,49 +40,48 @@ [fxShow]="!isEdit && (edgeScope === 'customer' || edgeScope === 'tenant') && isAssignedToCustomer(entity)"> {{ (entity?.customerIsPublic ? 'edge.make-private' : 'edge.unassign-from-customer') | translate }} + + + + + -
- - - - - -
-
+