Browse Source

Merge branch 'master' into improvement/rule-chain/note

pull/15121/head
Viacheslav Klimov 5 months ago
committed by GitHub
parent
commit
265a2aff71
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .gitignore
  2. 2
      application/src/main/data/json/system/widget_types/label_widget.json
  3. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 64
      application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersCustomizer.java
  5. 56
      application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersProperties.java
  6. 267
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  7. 12
      application/src/main/java/org/thingsboard/server/config/TbRuleEngineSecurityConfiguration.java
  8. 19
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  9. 2
      application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java
  10. 22
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  11. 6
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  12. 2
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  13. 11
      application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java
  14. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  15. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
  16. 8
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  17. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
  18. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java
  19. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java
  20. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java
  21. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java
  22. 134
      application/src/main/resources/thingsboard.yml
  23. 3
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  24. 8
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  25. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
  26. 16
      common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java
  27. 11
      common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java
  28. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java
  29. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  30. 8
      common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
  31. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
  32. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java
  33. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java
  34. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java
  35. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java
  36. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java
  37. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java
  38. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java
  39. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
  40. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
  41. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
  42. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java
  43. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java
  44. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java
  45. 43
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
  46. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java
  47. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java
  48. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java
  49. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java
  50. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
  51. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
  52. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
  53. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
  54. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
  55. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java
  56. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java
  57. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
  58. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java
  59. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java
  60. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java
  61. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java
  62. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java
  63. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java
  64. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java
  65. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
  66. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java
  67. 3
      common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java
  68. 83
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java
  69. 10
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java
  70. 14
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
  71. 4
      common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java
  72. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java
  73. 114
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
  74. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java
  75. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java
  76. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java
  77. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java
  78. 2
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
  79. 50
      common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java
  80. 52
      common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java
  81. 56
      common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java
  82. 242
      common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java
  83. 4
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  84. 8
      dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java
  85. 14
      edqs/src/main/resources/edqs.yml
  86. 64
      msa/js-executor/config/default.yml
  87. 12
      msa/js-executor/yarn.lock
  88. 24
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  89. 14
      msa/web-ui/config/custom-environment-variables.yml
  90. 14
      msa/web-ui/config/default.yml
  91. 30
      msa/web-ui/server.ts
  92. 12
      msa/web-ui/yarn.lock
  93. 29
      pom.xml
  94. 174
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java
  95. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  96. 161
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java
  97. 30
      transport/coap/src/main/resources/tb-coap-transport.yml
  98. 30
      transport/http/src/main/resources/tb-http-transport.yml
  99. 30
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  100. 30
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml

1
.gitignore

@ -38,3 +38,4 @@ rebuild-docker.sh
.run/** .run/**
.run .run
.claude .claude
.planning

2
application/src/main/data/json/system/widget_types/label_widget.json

File diff suppressed because one or more lines are too long

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

@ -622,11 +622,21 @@ public class ActorSystemContext {
@Value("${actors.rule.external.ssrf_additional_blocked_hosts:}") @Value("${actors.rule.external.ssrf_additional_blocked_hosts:}")
private List<String> ssrfAdditionalBlockedHosts; private List<String> ssrfAdditionalBlockedHosts;
@Value("${actors.rule.external.ssrf_allowed_hosts:}")
private List<String> ssrfAllowedHosts;
@PostConstruct @PostConstruct
public void init() { public void init() {
this.localCacheType = "caffeine".equals(cacheType); this.localCacheType = "caffeine".equals(cacheType);
SsrfProtectionValidator.setEnabled(ssrfProtectionEnabled); SsrfProtectionValidator.setEnabled(ssrfProtectionEnabled);
SsrfProtectionValidator.setAdditionalBlockedHosts(ssrfAdditionalBlockedHosts); SsrfProtectionValidator.setAdditionalBlockedHosts(ssrfAdditionalBlockedHosts);
SsrfProtectionValidator.setAllowedHosts(ssrfAllowedHosts);
if (!ssrfProtectionEnabled) {
log.warn("SSRF protection for external rule nodes is DISABLED. This allows rule chains to make HTTP requests to " +
"internal/private network addresses including cloud metadata endpoints. It is strongly recommended to " +
"enable SSRF protection by setting SSRF_PROTECTION_ENABLED=true. If your rule chains need to access " +
"devices on local networks, use SSRF_ALLOWED_HOSTS to whitelist specific addresses or ranges.");
}
} }
@Value("${actors.tenant.create_components_on_init:true}") @Value("${actors.tenant.create_components_on_init:true}")

64
application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersCustomizer.java

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2026 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.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Slf4j
@Component
@RequiredArgsConstructor
public class HttpSecurityHeadersCustomizer {
private final HttpSecurityHeadersProperties properties;
public void customize(HeadersConfigurer<?> headers) {
if (properties.getXContentTypeOptions().isEnabled()) {
headers.contentTypeOptions(config -> {});
}
if (properties.getReferrerPolicy().isEnabled()) {
headers.addHeaderWriter(new StaticHeadersWriter("Referrer-Policy", properties.getReferrerPolicy().getValue()));
}
if (properties.getXFrameOptions().isEnabled()) {
String value = properties.getXFrameOptions().getValue();
if ("DENY".equalsIgnoreCase(value)) {
headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny);
} else {
if (!"SAMEORIGIN".equalsIgnoreCase(value)) {
log.warn("Unrecognized X-Frame-Options value '{}', falling back to SAMEORIGIN. Valid values: DENY, SAMEORIGIN", value);
}
headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin);
}
}
if (properties.getContentSecurityPolicy().isEnabled() && StringUtils.hasText(properties.getContentSecurityPolicy().getValue())) {
headers.contentSecurityPolicy(csp -> {
csp.policyDirectives(properties.getContentSecurityPolicy().getValue());
if (properties.getContentSecurityPolicy().isReportOnly()) {
csp.reportOnly();
}
});
}
}
}

56
application/src/main/java/org/thingsboard/server/config/HttpSecurityHeadersProperties.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2026 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.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "security.headers")
@Data
public class HttpSecurityHeadersProperties {
private XContentTypeOptions xContentTypeOptions = new XContentTypeOptions();
private ReferrerPolicy referrerPolicy = new ReferrerPolicy();
private XFrameOptions xFrameOptions = new XFrameOptions();
private ContentSecurityPolicy contentSecurityPolicy = new ContentSecurityPolicy();
@Data
public static class XContentTypeOptions {
private boolean enabled = true;
}
@Data
public static class ReferrerPolicy {
private boolean enabled = true;
private String value = "strict-origin-when-cross-origin";
}
@Data
public static class XFrameOptions {
private boolean enabled = false;
private String value = "SAMEORIGIN";
}
@Data
public static class ContentSecurityPolicy {
private boolean enabled = false;
private String value = "";
private boolean reportOnly = false;
}
}

267
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.config; package org.thingsboard.server.config;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
@ -47,6 +48,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer; import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer; import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer;
import org.springdoc.core.utils.SpringDocUtils;
import org.springdoc.core.models.GroupedOpenApi; import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigProperties; import org.springdoc.core.properties.SwaggerUiConfigProperties;
@ -68,15 +70,20 @@ import org.thingsboard.server.service.security.auth.rest.LoginResponse;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -90,6 +97,8 @@ public class SwaggerConfiguration {
@PostConstruct @PostConstruct
public void configureModelResolver() { public void configureModelResolver() {
ModelResolver.enumsAsRef = true; ModelResolver.enumsAsRef = true;
SpringDocUtils.getConfig().replaceWithSchema(ByteBuffer.class,
new Schema<String>().type("string").format("byte"));
} }
public static final String LOGIN_ENDPOINT = "/api/auth/login"; public static final String LOGIN_ENDPOINT = "/api/auth/login";
@ -102,6 +111,11 @@ public class SwaggerConfiguration {
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false);
private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true);
// Populated by mapAwareConverter, consumed by customOpenApiCustomizer.
// Keyed by the schema name that swagger-core generates (see resolveSchemaName).
private final Map<String, List<String>> schemaPropertyOrders = new ConcurrentHashMap<>();
private final Map<String, Set<String>> schemaOwnProps = new ConcurrentHashMap<>();
@Value("${swagger.api_path:/api/**}") @Value("${swagger.api_path:/api/**}")
private String apiPath; private String apiPath;
@Value("${swagger.security_path_regex}") @Value("${swagger.security_path_regex}")
@ -304,23 +318,21 @@ public class SwaggerConfiguration {
schema.setProperties(null); schema.setProperties(null);
} }
} }
} else if (schema != null && schema.getProperties() != null && !schema.getProperties().isEmpty()) { } else {
// Precompute property order and own-prop names for this class.
// The actual reordering happens later in the OpenApiCustomizer,
// which has access to the final state of all component schemas
// (including ones where the ModelConverter only sees a $ref).
try { try {
var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType);
var orderedNames = resolvePropertyOrder(cls, beanDesc); String schemaName = resolveSchemaName(javaType);
if (!orderedNames.isEmpty()) { schemaPropertyOrders.put(schemaName, resolvePropertyOrder(cls, beanDesc));
@SuppressWarnings("unchecked") Set<String> ownProps = computeOwnPropNames(cls, beanDesc);
Map<String, Schema> current = schema.getProperties(); if (!ownProps.isEmpty()) {
var reordered = new LinkedHashMap<String, Schema>(); schemaOwnProps.put(schemaName, ownProps);
for (String name : orderedNames) {
Schema prop = current.get(name);
if (prop != null) reordered.put(name, prop);
}
current.forEach((k, v) -> reordered.putIfAbsent(k, v));
schema.setProperties(reordered);
} }
} catch (Exception ignored) { } catch (Exception e) {
log.trace("Failed to resolve property order for {}", cls.getName(), ignored); log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage());
} }
} }
} }
@ -406,6 +418,21 @@ public class SwaggerConfiguration {
} }
}); });
// Deduplicate allOf child schemas: remove properties that are already defined
// in the referenced parent schema to avoid duplication (e.g. EntityId children).
schemas.forEach((schemaName, schema) -> {
Set<String> ownProps = schemaOwnProps.getOrDefault(schemaName, Set.of());
deduplicateAllOfProperties(schema, schemas, ownProps);
});
// Reorder properties for all component schemas. This runs after all
// schemas are finalized so it covers schemas the ModelConverter only
// saw as a $ref (e.g. interface-based discriminator types like EntityId).
schemas.forEach((schemaName, schema) -> {
List<String> propOrder = schemaPropertyOrders.getOrDefault(schemaName, List.of());
reorderSchemaProperties(schema, propOrder);
});
// Fix polymorphic request/response bodies: replace inline oneOf with base type $ref // Fix polymorphic request/response bodies: replace inline oneOf with base type $ref
paths.values().stream() paths.values().stream()
.flatMap(pathItem -> pathItem.readOperationsMap().values().stream()) .flatMap(pathItem -> pathItem.readOperationsMap().values().stream())
@ -530,6 +557,9 @@ public class SwaggerConfiguration {
if (prop.getDescription() != null) { if (prop.getDescription() != null) {
refSchema.setDescription(prop.getDescription()); refSchema.setDescription(prop.getDescription());
} }
if (prop.getReadOnly() != null) {
refSchema.setReadOnly(prop.getReadOnly());
}
schema.getProperties().put(propName, refSchema); schema.getProperties().put(propName, refSchema);
log.debug("Replaced oneOf with $ref to {} in property {}", baseType, propName); log.debug("Replaced oneOf with $ref to {} in property {}", baseType, propName);
} }
@ -540,7 +570,7 @@ public class SwaggerConfiguration {
private String tagItemFromPathItem(PathItem item) { private String tagItemFromPathItem(PathItem item) {
var operations = item.readOperationsMap().values(); var operations = item.readOperationsMap().values();
var operation = operations.stream().findAny(); var operation = operations.stream().findFirst();
if (operation.isPresent()) { if (operation.isPresent()) {
var tags = operation.get().getTags(); var tags = operation.get().getTags();
if (tags != null && !tags.isEmpty()) { if (tags != null && !tags.isEmpty()) {
@ -676,15 +706,206 @@ public class SwaggerConfiguration {
return new ApiResponse().description(description).content(content); return new ApiResponse().description(description).content(content);
} }
/**
* Recursively collects all property names reachable from {@code schemaName}, walking the
* ancestor chain through allOf $ref entries (to handle multi-level inheritance).
* {@code visited} prevents infinite loops in case of circular references.
*/
@SuppressWarnings("unchecked")
private void collectAllProperties(String schemaName, Map<String, Schema> allSchemas,
Set<String> result, Set<String> visited) {
if (!visited.add(schemaName)) {
return;
}
Schema<?> schema = allSchemas.get(schemaName);
if (schema == null) {
return;
}
if (schema.getProperties() != null) {
result.addAll(schema.getProperties().keySet());
}
if (schema.getAllOf() != null) {
for (Schema<?> allOfElement : schema.getAllOf()) {
String ref = allOfElement.get$ref();
if (ref != null) {
String refName = ref.substring(ref.lastIndexOf('/') + 1);
collectAllProperties(refName, allSchemas, result, visited);
} else if (allOfElement.getProperties() != null) {
result.addAll(allOfElement.getProperties().keySet());
}
}
}
}
@SuppressWarnings("unchecked")
private void deduplicateAllOfProperties(Schema<?> schema, Map<String, Schema> allSchemas, Set<String> ownProps) {
if (schema.getAllOf() == null) {
return;
}
// Collect properties defined in any $ref'd parent within the allOf, recursively
// walking the ancestor chain (each parent may itself use allOf to extend a grandparent).
Set<String> parentProperties = new LinkedHashSet<>();
for (Schema<?> allOfElement : schema.getAllOf()) {
String ref = allOfElement.get$ref();
if (ref != null) {
String refName = ref.substring(ref.lastIndexOf('/') + 1);
collectAllProperties(refName, allSchemas, parentProperties, new LinkedHashSet<>());
}
}
if (parentProperties.isEmpty()) {
return;
}
// Properties to strip: in parent schema AND not declared as own-class fields.
// This removes inherited properties (from superclasses or pure interface getters)
// while keeping properties the class declares as its own fields.
Set<String> toStrip = new LinkedHashSet<>(parentProperties);
toStrip.removeAll(ownProps);
if (toStrip.isEmpty()) {
return;
}
// Strip from inline (non-$ref) allOf elements
schema.getAllOf().removeIf(allOfElement -> {
if (allOfElement.get$ref() != null) {
return false;
}
if (allOfElement.getProperties() != null) {
Map<String, Schema> filtered = new LinkedHashMap<>(allOfElement.getProperties());
filtered.keySet().removeAll(toStrip);
allOfElement.setProperties(filtered.isEmpty() ? null : filtered);
}
return allOfElement.getProperties() == null
&& allOfElement.getRequired() == null
&& allOfElement.getType() == null;
});
// Remove stripped properties from the schema's required list
if (schema.getRequired() != null) {
List<String> required = new ArrayList<>(schema.getRequired());
required.removeAll(toStrip);
schema.setRequired(required.isEmpty() ? null : required);
}
}
/**
* Computes the schema name that swagger-core will use for the given JavaType.
* For simple types, this is just the class simple name (e.g. {@code Device}).
* For parameterized types, type parameter names are appended
* (e.g. {@code PageData<Device>} becomes {@code PageDataDevice}).
* This matches the naming convention used by swagger-core's {@code TypeNameResolver}.
*/
private static String resolveSchemaName(JavaType javaType) {
StringBuilder sb = new StringBuilder(javaType.getRawClass().getSimpleName());
if (javaType.hasGenericTypes()) {
for (int i = 0; i < javaType.containedTypeCount(); i++) {
JavaType param = javaType.containedType(i);
if (param != null) {
sb.append(param.getRawClass().getSimpleName());
}
}
}
return sb.toString();
}
/**
* Returns the JSON property names that are backed by fields declared directly in {@code cls}
* (not inherited from a superclass). Used to distinguish "own" from "inherited" properties
* when deduplicating allOf inline elements.
*/
private static Set<String> computeOwnPropNames(Class<?> cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) {
Map<String, String> allFieldToJson = new LinkedHashMap<>();
for (var prop : beanDesc.findProperties()) {
if (prop.getField() != null && prop.couldSerialize()) {
allFieldToJson.put(prop.getField().getName(), prop.getName());
}
}
Set<String> own = new LinkedHashSet<>();
for (Field f : cls.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
String jsonName = allFieldToJson.get(f.getName());
if (jsonName != null) own.add(jsonName);
}
return own;
}
@SuppressWarnings("unchecked")
private static void reorderSchemaProperties(Schema<?> schema, List<String> propOrder) {
if (schema.getProperties() != null && schema.getProperties().size() > 1) {
schema.setProperties(reorderProperties(schema.getProperties(), propOrder));
}
if (schema.getAllOf() != null) {
for (Schema<?> allOfElement : schema.getAllOf()) {
if (allOfElement.get$ref() != null) continue;
if (allOfElement.getProperties() != null && allOfElement.getProperties().size() > 1) {
allOfElement.setProperties(reorderProperties(allOfElement.getProperties(), propOrder));
}
}
}
}
private static LinkedHashMap<String, Schema> reorderProperties(Map<String, Schema> current, List<String> propOrder) {
var reordered = new LinkedHashMap<String, Schema>();
for (String name : propOrder) {
Schema prop = current.get(name);
if (prop != null) reordered.put(name, prop);
}
// Any properties not covered by propOrder are appended
// alphabetically to guarantee a deterministic stable order.
new TreeMap<>(current).forEach((k, v) -> reordered.putIfAbsent(k, v));
return reordered;
}
/**
* Resolves the property ordering for a schema class.
*
* <p>Returns a list of JSON property names in the order they should appear in the
* OpenAPI schema. The caller uses this list to reorder the schema's property map;
* any properties <b>not</b> present in the returned list are appended alphabetically
* by the caller's {@code TreeMap} fallback, guaranteeing a stable, deterministic order.
*
* <p><b>Resolution strategy (first match wins):</b>
* <ol>
* <li>If {@code @JsonPropertyOrder} with an explicit {@code value()} is found on the
* class or any interface in its ancestry, that list is returned as-is. Note: if the
* annotation lists only a subset of fields, those fields are ordered first and the
* remaining properties fall through to the caller's alphabetical fallback consistent
* with Jackson's own behaviour for partial {@code @JsonPropertyOrder}.</li>
* <li>Otherwise, field-backed properties are returned in declaration order (superclass
* fields first). Getter-only properties are intentionally excluded to avoid
* non-deterministic ordering across restarts.</li>
* </ol>
*/
private static List<String> resolvePropertyOrder(Class<?> cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) { private static List<String> resolvePropertyOrder(Class<?> cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) {
// If an explicit @JsonPropertyOrder is present on the class or any interface in its
// ancestry, honour it directly. Walk up the class hierarchy; for each class also walk
// the full interface hierarchy (including super-interfaces) via BFS.
for (Class<?> c = cls; c != null && c != Object.class; c = c.getSuperclass()) {
JsonPropertyOrder propOrder = c.getAnnotation(JsonPropertyOrder.class);
if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) {
return Arrays.asList(propOrder.value());
}
Deque<Class<?>> ifaceQueue = new ArrayDeque<>(Arrays.asList(c.getInterfaces()));
Set<Class<?>> visitedIfaces = new LinkedHashSet<>();
while (!ifaceQueue.isEmpty()) {
Class<?> iface = ifaceQueue.poll();
if (!visitedIfaces.add(iface)) continue;
propOrder = iface.getAnnotation(JsonPropertyOrder.class);
if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) {
return Arrays.asList(propOrder.value());
}
ifaceQueue.addAll(Arrays.asList(iface.getInterfaces()));
}
}
// Map backing field names to their JSON property names (respects @JsonProperty) // Map backing field names to their JSON property names (respects @JsonProperty)
Map<String, String> fieldToJsonName = new LinkedHashMap<>(); Map<String, String> fieldToJsonName = new LinkedHashMap<>();
LinkedHashSet<String> getterOnlyNames = new LinkedHashSet<>();
for (var prop : beanDesc.findProperties()) { for (var prop : beanDesc.findProperties()) {
if (prop.getField() != null) { if (prop.getField() != null && prop.couldSerialize()) {
fieldToJsonName.put(prop.getField().getName(), prop.getName()); fieldToJsonName.put(prop.getField().getName(), prop.getName());
} else {
getterOnlyNames.add(prop.getName());
} }
} }
@ -702,8 +923,12 @@ public class SwaggerConfiguration {
} }
} }
// Append getter-only properties (no backing field) at the end // Return only field-backed properties in declaration order.
ordered.addAll(getterOnlyNames); // Getter-only properties (no backing field) are intentionally excluded: their set can vary
// between restarts (e.g. Optional-typed getters depend on Jackson module registration order),
// so including them here would make their position non-deterministic when some are in orderedNames
// and others are only in the schema map. The converter's TreeMap fallback handles ALL
// non-field-backed properties together in one alphabetical pass, guaranteeing stable order.
return ordered; return ordered;
} }

12
application/src/main/java/org/thingsboard/server/config/TbRuleEngineSecurityConfiguration.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.config; package org.thingsboard.server.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -33,11 +34,16 @@ import org.springframework.security.web.SecurityFilterChain;
@ConditionalOnExpression("'${service.type:null}'=='tb-rule-engine'") @ConditionalOnExpression("'${service.type:null}'=='tb-rule-engine'")
public class TbRuleEngineSecurityConfiguration { public class TbRuleEngineSecurityConfiguration {
@Autowired
private HttpSecurityHeadersCustomizer httpSecurityHeadersCustomizer;
@Bean @Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers http.headers(headers -> {
.cacheControl(config -> {}) headers.defaultsDisabled();
.frameOptions(config -> {}).disable()) headers.cacheControl(config -> {});
httpSecurityHeadersCustomizer.customize(headers);
})
.cors(cors -> {}) .cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(config -> config .authorizeHttpRequests(config -> config

19
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -143,6 +143,9 @@ public class ThingsboardSecurityConfiguration {
@Autowired @Autowired
private AuthExceptionHandler authExceptionHandler; private AuthExceptionHandler authExceptionHandler;
@Autowired
private HttpSecurityHeadersCustomizer httpSecurityHeadersCustomizer;
@Bean @Bean
protected PayloadSizeFilter payloadSizeFilter() { protected PayloadSizeFilter payloadSizeFilter() {
return new PayloadSizeFilter(maxPayloadSizeConfig); return new PayloadSizeFilter(maxPayloadSizeConfig);
@ -231,9 +234,11 @@ public class ThingsboardSecurityConfiguration {
http http
.securityMatchers(matchers -> matchers .securityMatchers(matchers -> matchers
.requestMatchers("/*.js", "/*.css", "/*.ico", "/assets/**", "/static/**")) .requestMatchers("/*.js", "/*.css", "/*.ico", "/assets/**", "/static/**"))
.headers(header -> header .headers(headers -> {
.defaultsDisabled() headers.defaultsDisabled();
.addHeaderWriter(new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, public"))) headers.addHeaderWriter(new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, public"));
httpSecurityHeadersCustomizer.customize(headers);
})
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll()) .authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
.requestCache(RequestCacheConfigurer::disable) .requestCache(RequestCacheConfigurer::disable)
.securityContext(AbstractHttpConfigurer::disable) .securityContext(AbstractHttpConfigurer::disable)
@ -243,8 +248,12 @@ public class ThingsboardSecurityConfiguration {
@Bean @Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers.defaultsDisabled() http.headers(headers -> {
.crossOriginOpenerPolicy(coop -> coop.policy(CrossOriginOpenerPolicy.SAME_ORIGIN))) headers.defaultsDisabled();
headers.cacheControl(config -> {});
headers.crossOriginOpenerPolicy(coop -> coop.policy(CrossOriginOpenerPolicy.SAME_ORIGIN));
httpSecurityHeadersCustomizer.customize(headers);
})
.cors(cors -> {}) .cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(config -> {}) .exceptionHandling(config -> {})

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

@ -68,7 +68,7 @@ public class ApiKeyController extends BaseController {
private final ApiKeyService apiKeyService; private final ApiKeyService apiKeyService;
@ApiOperation(value = "Save API key for user (saveApiKey)", @ApiOperation(value = "Save API key for user (saveApiKey)",
notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey <value>'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey {value}'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping(value = "/apiKey") @PostMapping(value = "/apiKey")
public ApiKey saveApiKey( public ApiKey saveApiKey(

22
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -206,16 +206,16 @@ public class DeviceController extends BaseController {
notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK + notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK +
"Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" + "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" +
"You may find the example of device with different type of credentials below: \n\n" + "You may find the example of device with different type of credentials below: \n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device profile ID</b> below: \n\n" + "- Credentials type: **\"Access token\"** with **device profile ID** below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device profile default</b> below: \n\n" + "- Credentials type: **\"Access token\"** with **device profile default** below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" + "- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + "Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" + "- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" + "- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " +
@ -322,14 +322,14 @@ public class DeviceController extends BaseController {
"Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" + "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" +
"The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" + "The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" +
"You may find the example of device with different type of credentials below: \n\n" + "You may find the example of device with different type of credentials below: \n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device ID</b> and with <b>device ID</b> below: \n\n" + "- Credentials type: **\"Access token\"** with **device ID** and with **device ID** below: \n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" + "- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + "Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" + "- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" + "- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Update to real value:\n" + "Update to real value:\n" +

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

@ -214,19 +214,19 @@ public class EntitiesVersionControlController extends BaseController {
" \"timestamp\": 1655198593000,\n" + " \"timestamp\": 1655198593000,\n" +
" \"id\": \"fd82625bdd7d6131cf8027b44ee967012ecaf990\",\n" + " \"id\": \"fd82625bdd7d6131cf8027b44ee967012ecaf990\",\n" +
" \"name\": \"Devices and assets - v2.0\",\n" + " \"name\": \"Devices and assets - v2.0\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" },\n" + " },\n" +
" {\n" + " {\n" +
" \"timestamp\": 1655198528000,\n" + " \"timestamp\": 1655198528000,\n" +
" \"id\": \"682adcffa9c8a2f863af6f00c4850323acbd4219\",\n" + " \"id\": \"682adcffa9c8a2f863af6f00c4850323acbd4219\",\n" +
" \"name\": \"Update my device\",\n" + " \"name\": \"Update my device\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" },\n" + " },\n" +
" {\n" + " {\n" +
" \"timestamp\": 1655198280000,\n" + " \"timestamp\": 1655198280000,\n" +
" \"id\": \"d2a6087c2b30e18cc55e7cdda345a8d0dfb959a4\",\n" + " \"id\": \"d2a6087c2b30e18cc55e7cdda345a8d0dfb959a4\",\n" +
" \"name\": \"Devices and assets - v1.0\",\n" + " \"name\": \"Devices and assets - v1.0\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" }\n" + " }\n" +
" ],\n" + " ],\n" +
" \"totalPages\": 1,\n" + " \"totalPages\": 1,\n" +

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@ -166,6 +167,7 @@ public class TwoFactorAuthController extends BaseController {
@Builder @Builder
public static class TwoFaProviderInfo { public static class TwoFaProviderInfo {
private TwoFaProviderType type; private TwoFaProviderType type;
@JsonProperty("default")
private boolean isDefault; private boolean isDefault;
private String contact; private String contact;
private Integer minVerificationCodeSendPeriod; private Integer minVerificationCodeSendPeriod;

11
application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java

@ -29,6 +29,7 @@ import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.info.NotificationInfo;
@ -109,10 +110,13 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
adaptiveCard.getActions().add(actionOpenUrl); adaptiveCard.getActions().add(actionOpenUrl);
} }
URI webhookUri = new URI(targetConfig.getWebhookUrl());
SsrfProtectionValidator.validateUri(webhookUri);
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON); headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsAdaptiveCard), headers); HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsAdaptiveCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class); restTemplate.postForEntity(webhookUri, request, String.class);
} }
private void sendTeamsMessageCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException, URISyntaxException { private void sendTeamsMessageCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException, URISyntaxException {
@ -139,10 +143,13 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
teamsMessageCard.setPotentialAction(List.of(actionCard)); teamsMessageCard.setPotentialAction(List.of(actionCard));
} }
URI webhookUri = new URI(targetConfig.getWebhookUrl());
SsrfProtectionValidator.validateUri(webhookUri);
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON); headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsMessageCard), headers); HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsMessageCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class); restTemplate.postForEntity(webhookUri, request, String.class);
} }
private String getButtonUri(MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException { private String getButtonUri(MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException {

4
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -23,12 +23,15 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import java.net.URI;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@Service(value = "customOAuth2ClientMapper") @Service(value = "customOAuth2ClientMapper")
@ -64,6 +67,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
throw new RuntimeException("Can't convert principal to JSON string", e); throw new RuntimeException("Can't convert principal to JSON string", e);
} }
try { try {
SsrfProtectionValidator.validateUri(new URI(custom.getUrl()));
return restTemplate.postForEntity(custom.getUrl(), request, OAuth2User.class).getBody(); return restTemplate.postForEntity(custom.getUrl(), request, OAuth2User.class).getBody();
} catch (Exception e) { } catch (Exception e) {
log.error("There was an error during connection to custom mapper endpoint", e); log.error("There was an error during connection to custom mapper endpoint", e);

4
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java

@ -42,10 +42,6 @@ public abstract class BaseEntityExportService<I extends EntityId, E extends Expo
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, E mainEntity, D exportData) { protected void setRelatedEntities(EntitiesExportCtx<?> ctx, E mainEntity, D exportData) {
} }
protected D newExportData() {
return (D) new EntityExportData<E>();
}
public abstract Set<EntityType> getSupportedEntityTypes(); public abstract Set<EntityType> getSupportedEntityTypes();
protected void replaceUuidsRecursively(EntitiesExportCtx<?> ctx, JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern) { protected void replaceUuidsRecursively(EntitiesExportCtx<?> ctx, JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern) {

8
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java

@ -70,7 +70,8 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
@Override @Override
public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException { public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
D exportData = newExportData(); @SuppressWarnings("unchecked")
D exportData = (D) EntityExportData.newInstance(entityId.getEntityType());
E entity = exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entityId); E entity = exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entityId);
if (entity == null) { if (entity == null) {
@ -78,7 +79,6 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
} }
exportData.setEntity(entity); exportData.setEntity(entity);
exportData.setEntityType(entityId.getEntityType());
setAdditionalExportData(ctx, entity, exportData); setAdditionalExportData(ctx, entity, exportData);
if (entity instanceof HasVersion hasVersion) { if (entity instanceof HasVersion hasVersion) {
hasVersion.setVersion(null); hasVersion.setVersion(null);
@ -223,8 +223,4 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
return internalUuid; return internalUuid;
} }
protected D newExportData() {
return (D) new EntityExportData<E>();
}
} }

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java

@ -48,11 +48,6 @@ public class DeviceExportService extends BaseEntityExportService<DeviceId, Devic
} }
} }
@Override
protected DeviceExportData newExportData() {
return new DeviceExportData();
}
@Override @Override
public Set<EntityType> getSupportedEntityTypes() { public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.DEVICE); return Set.of(EntityType.DEVICE);

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java

@ -36,11 +36,6 @@ public class OtaPackageExportService extends BaseEntityExportService<OtaPackageI
otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId())); otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId()));
} }
@Override
protected OtaPackageExportData newExportData() {
return new OtaPackageExportData();
}
@Override @Override
public Set<EntityType> getSupportedEntityTypes() { public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.OTA_PACKAGE); return Set.of(EntityType.OTA_PACKAGE);

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java

@ -61,11 +61,6 @@ public class RuleChainExportService extends BaseEntityExportService<RuleChainId,
} }
} }
@Override
protected RuleChainExportData newExportData() {
return new RuleChainExportData();
}
@Override @Override
public Set<EntityType> getSupportedEntityTypes() { public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.RULE_CHAIN); return Set.of(EntityType.RULE_CHAIN);

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java

@ -38,11 +38,6 @@ public class WidgetTypeExportService extends BaseEntityExportService<WidgetTypeI
} }
} }
@Override
protected WidgetTypeExportData newExportData() {
return new WidgetTypeExportData();
}
@Override @Override
public Set<EntityType> getSupportedEntityTypes() { public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.WIDGET_TYPE); return Set.of(EntityType.WIDGET_TYPE);

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java

@ -45,11 +45,6 @@ public class WidgetsBundleExportService extends BaseEntityExportService<WidgetsB
exportData.setFqns(fqns); exportData.setFqns(fqns);
} }
@Override
protected WidgetsBundleExportData newExportData() {
return new WidgetsBundleExportData();
}
@Override @Override
public Set<EntityType> getSupportedEntityTypes() { public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.WIDGETS_BUNDLE); return Set.of(EntityType.WIDGETS_BUNDLE);

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

@ -15,6 +15,7 @@
# #
# Server common parameters # Server common parameters
# Configures HTTP/HTTPS bind address, port, SSL, WebSocket, and REST API settings.
server: server:
# Server bind-address # Server bind-address
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
@ -109,23 +110,25 @@ server:
response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}" response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}"
# Application info parameters # Application info parameters
# Exposes application metadata such as version string injected at build time.
app: app:
# Application version # Application version
version: "@project.version@" version: "@project.version@"
# Zookeeper connection parameters # ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -133,6 +136,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cluster parameters # Cluster parameters
# Controls cluster statistics — tracks the number of messages exchanged between cluster nodes.
cluster: cluster:
stats: stats:
# Enable/Disable the cluster statistics. Calculates the number of messages sent between cluster nodes based on each type # Enable/Disable the cluster statistics. Calculates the number of messages sent between cluster nodes based on each type
@ -141,11 +145,13 @@ cluster:
print_interval_ms: "${TB_CLUSTER_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_CLUSTER_STATS_PRINT_INTERVAL_MS:10000}"
# Plugins configuration parameters # Plugins configuration parameters
# Defines classpath scan packages used to discover and register ThingsBoard extension plugins.
plugins: plugins:
# Comma-separated package list used during classpath scanning for plugins # Comma-separated package list used during classpath scanning for plugins
scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}" scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}"
# Security parameters # Security parameters
# Configures JWT tokens, user login, device claiming, OAuth2, and CA certificate trust store.
security: security:
# JWT Token parameters # JWT Token parameters
jwt: # Since 3.4.2 values are persisted in the database during installation or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN jwt: # Since 3.4.2 values are persisted in the database during installation or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN
@ -182,8 +188,60 @@ security:
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}/lib/security/cacerts}" path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}/lib/security/cacerts}"
# The password of the cacerts keystore file # The password of the cacerts keystore file
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}" password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
# HTTP security response headers configuration.
# These headers are set on responses from the ThingsBoard backend (tb-node).
# In microservice deployments, the web-ui (Express.js) has its own header configuration
# under msa/web-ui/config/ using the same environment variable names.
headers:
# X-Content-Type-Options header prevents browsers from MIME-sniffing the Content-Type.
# Safe to enable. Only disable if you intentionally serve resources with mismatched Content-Type.
x-content-type-options:
# Enable/disable X-Content-Type-Options header. Prevents browsers from MIME-sniffing the Content-Type
enabled: "${SECURITY_HEADERS_X_CONTENT_TYPE_OPTIONS_ENABLED:true}"
# Referrer-Policy header controls how much referrer info the browser sends with requests.
# The default 'strict-origin-when-cross-origin' matches the browser's built-in default,
# so enabling this does not change existing behavior — it just makes the policy explicit.
# Valid values: no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin,
# same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
referrer-policy:
# Enable/disable Referrer-Policy header
enabled: "${SECURITY_HEADERS_REFERRER_POLICY_ENABLED:true}"
# Referrer-Policy header value
value: "${SECURITY_HEADERS_REFERRER_POLICY_VALUE:strict-origin-when-cross-origin}"
# X-Frame-Options header protects against clickjacking attacks by preventing the page
# from being loaded in iframes on other domains.
# Disabled by default because ThingsBoard supports multi-domain deployments where
# the platform may be embedded in iframes on customer domains.
# WARNING: Enabling with DENY will block ALL iframe embedding including dashboards
# embedded on external sites. Use SAMEORIGIN to allow same-domain iframes only.
x-frame-options:
# Enable/disable X-Frame-Options header. Protects against clickjacking attacks
enabled: "${SECURITY_HEADERS_X_FRAME_OPTIONS_ENABLED:false}"
# Valid values: DENY, SAMEORIGIN
value: "${SECURITY_HEADERS_X_FRAME_OPTIONS_VALUE:SAMEORIGIN}"
# Content-Security-Policy header mitigates XSS and data injection attacks by restricting
# which resources the browser is allowed to load.
# Disabled by default because ThingsBoard supports multi-domain deployments and
# because custom HTML Card widgets may use inline scripts, inline styles, and
# external resources that a restrictive CSP would block.
# WARNING when enabling: A strict CSP (e.g. script-src 'self') will break:
# - HTML Card widgets with inline JavaScript
# - Custom widget types with inline scripts/styles
# - Widgets loading external resources (images, fonts, scripts)
# - Dashboard embedding via iframes (if frame-ancestors is restrictive)
# Use 'report-only: true' first to test the impact before enforcing.
# Example value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'"
content-security-policy:
# Enable/disable Content-Security-Policy header. Mitigates XSS and data injection attacks
enabled: "${SECURITY_HEADERS_CONTENT_SECURITY_POLICY_ENABLED:false}"
# Full CSP directive string
value: "${SECURITY_HEADERS_CONTENT_SECURITY_POLICY_VALUE:}"
# If true, uses Content-Security-Policy-Report-Only header instead — the browser
# reports violations but does not enforce them. Use for testing before enforcing.
report-only: "${SECURITY_HEADERS_CONTENT_SECURITY_POLICY_REPORT_ONLY:false}"
# Mail settings parameters # Mail settings parameters
# Configures mail service OAuth2 token refresh and per-tenant sending rate limits.
mail: mail:
oauth2: oauth2:
# Interval for checking refresh token expiration in seconds(by default, 1 day). # Interval for checking refresh token expiration in seconds(by default, 1 day).
@ -192,6 +250,7 @@ mail:
per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}" per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}"
# Usage statistics parameters # Usage statistics parameters
# Controls collection and reporting intervals for API usage stats at system, tenant, and customer levels.
usage: usage:
stats: stats:
report: report:
@ -215,6 +274,7 @@ usage:
report_interval: "${DEVICES_STATS_REPORT_INTERVAL:60}" report_interval: "${DEVICES_STATS_REPORT_INTERVAL:60}"
# UI settings parameters # UI settings parameters
# Configures dashboard data limits and base URL for UI help assets.
ui: ui:
# Dashboard parameters # Dashboard parameters
dashboard: dashboard:
@ -226,6 +286,7 @@ ui:
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.4}" base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.4}"
# Database telemetry parameters # Database telemetry parameters
# Selects the storage backend (SQL, Cassandra, or TimescaleDB) for time-series and latest telemetry data.
database: database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by a single API call to fetch telemetry records ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by a single API call to fetch telemetry records
ts: ts:
@ -234,6 +295,7 @@ database:
type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale) type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale)
# Cassandra driver configuration parameters # Cassandra driver configuration parameters
# Configures cluster name, keyspace, SSL, credentials, socket, and query settings for the Cassandra driver.
cassandra: cassandra:
# Thingsboard cluster name # Thingsboard cluster name
cluster_name: "${CASSANDRA_CLUSTER_NAME:Thingsboard Cluster}" cluster_name: "${CASSANDRA_CLUSTER_NAME:Thingsboard Cluster}"
@ -352,6 +414,7 @@ cassandra:
print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}" print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}"
# SQL configuration parameters # SQL configuration parameters
# Tunes batch sizes, delays, thread counts, TTL, and partitioning for SQL-backed persistence of telemetry, events, and audit logs.
sql: sql:
# Specify batch size for persisting attribute updates # Specify batch size for persisting attribute updates
attributes: attributes:
@ -455,6 +518,7 @@ sql:
query_timeout: "${SQL_RELATIONS_QUERY_TIMEOUT_SEC:20}" # This value has to be reasonably small to prevent the relation query from blocking all other DB calls query_timeout: "${SQL_RELATIONS_QUERY_TIMEOUT_SEC:20}" # This value has to be reasonably small to prevent the relation query from blocking all other DB calls
# Actor system parameters # Actor system parameters
# Configures thread pools, timeouts, and behavior for the internal actor system processing devices, rules, and RPCs.
actors: actors:
system: system:
throughput: "${ACTORS_SYSTEM_THROUGHPUT:5}" # Number of messages the actor system will process per actor before switching to processing of messages for the next actor throughput: "${ACTORS_SYSTEM_THROUGHPUT:5}" # Number of messages the actor system will process per actor before switching to processing of messages for the next actor
@ -523,6 +587,10 @@ actors:
# Comma-separated list of additional blocked destinations (IPs, CIDR subnets, or hostnames). # Comma-separated list of additional blocked destinations (IPs, CIDR subnets, or hostnames).
# Example: "198.51.100.0/24,metadata.tencentyun.com,rancher-metadata" # Example: "198.51.100.0/24,metadata.tencentyun.com,rancher-metadata"
ssrf_additional_blocked_hosts: "${SSRF_ADDITIONAL_BLOCKED_HOSTS:}" ssrf_additional_blocked_hosts: "${SSRF_ADDITIONAL_BLOCKED_HOSTS:}"
# Comma-separated list of allowed destinations that bypass SSRF blocking (IPs, CIDR subnets, or hostnames).
# Use this when your rule chains need to reach devices on private networks (e.g., 192.168.1.0/24).
# Example: "192.168.1.0/24,10.0.0.0/8,my-internal-service.corp"
ssrf_allowed_hosts: "${SSRF_ALLOWED_HOSTS:}"
rpc: rpc:
# Maximum number of persistent RPC call retries in case of failed request delivery. # Maximum number of persistent RPC call retries in case of failed request delivery.
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}" max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
@ -555,6 +623,8 @@ actors:
# Time in seconds to receive calculation result. # Time in seconds to receive calculation result.
calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}" calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}"
# Debug settings parameters
# Configures the global default duration for debug mode used by rule chains and calculated fields.
debug: debug:
settings: settings:
# Default duration (in minutes) for debug mode. Min value is 1 minute. Tenant profile settings override this one. # Default duration (in minutes) for debug mode. Min value is 1 minute. Tenant profile settings override this one.
@ -562,6 +632,7 @@ debug:
default_duration: "${DEBUG_SETTINGS_DEFAULT_DURATION_MINUTES:15}" default_duration: "${DEBUG_SETTINGS_DEFAULT_DURATION_MINUTES:15}"
# Cache settings parameters # Cache settings parameters
# Configures cache type (Caffeine or Redis), pool size, and per-entity TTL and max-size limits for all caches.
cache: cache:
# caffeine or redis(7.2 - latest compatible version) # caffeine or redis(7.2 - latest compatible version)
type: "${CACHE_TYPE:caffeine}" type: "${CACHE_TYPE:caffeine}"
@ -721,6 +792,7 @@ cache:
spring.data.redis.repositories.enabled: false # Disable this because it is not required. spring.data.redis.repositories.enabled: false # Disable this because it is not required.
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Configures standalone, cluster, or sentinel Redis connection, SSL, and connection pool settings used for caching.
redis: redis:
# standalone or cluster or sentinel # standalone or cluster or sentinel
connection: connection:
@ -757,9 +829,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# If set false will be used pool config build from values of the pool config section # If set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -804,27 +876,36 @@ redis:
# Update version parameters # Update version parameters
# Controls whether the platform periodically checks for new ThingsBoard releases.
updates: updates:
# Enable/disable checks for the new version # Enable/disable checks for the new version
enabled: "${UPDATES_ENABLED:true}" enabled: "${UPDATES_ENABLED:true}"
# Spring CORS configuration parameters # Spring CORS configuration parameters.
# Controls the Access-Control-Allow-Origin and Access-Control-Allow-Credentials response headers.
# WARNING: The default configuration allows cross-origin requests from ANY domain with credentials.
# This means any website can make API requests on behalf of an authenticated user if the token
# is accessible (e.g., via XSS). For production deployments, restrict to your domain(s):
# TB_CORS_ALLOWED_ORIGIN_PATTERNS=https://your-domain.com
# For multi-domain deployments, list all allowed domains comma-separated:
# TB_CORS_ALLOWED_ORIGIN_PATTERNS=https://domain1.com,https://domain2.com
spring.mvc.cors: spring.mvc.cors:
mappings: mappings:
# Intercept path # Intercept path
"[/api/**]": "[/api/**]":
#Comma-separated list of origins to allow. '*' allows all origins. When not set, CORS support is disabled. #Comma-separated list of origins to allow. '*' allows all origins. When not set, CORS support is disabled.
allowed-origin-patterns: "*" allowed-origin-patterns: "${TB_CORS_ALLOWED_ORIGIN_PATTERNS:*}"
#Comma-separated list of methods to allow. '*' allows all methods. #Comma-separated list of methods to allow. '*' allows all methods.
allowed-methods: "*" allowed-methods: "${TB_CORS_ALLOWED_METHODS:*}"
#Comma-separated list of headers to allow in a request. '*' allows all headers. #Comma-separated list of headers to allow in a request. '*' allows all headers.
allowed-headers: "*" allowed-headers: "${TB_CORS_ALLOWED_HEADERS:*}"
#How long, in seconds, the response from a pre-flight request can be cached by clients. #How long, in seconds, the response from a pre-flight request can be cached by clients.
max-age: "1800" max-age: "${TB_CORS_MAX_AGE:1800}"
#Set whether credentials are supported. When not set, credentials are not supported. #Set whether credentials are supported. When not set, credentials are not supported.
allow-credentials: "true" allow-credentials: "${TB_CORS_ALLOW_CREDENTIALS:true}"
# General spring parameters # General spring parameters
# Miscellaneous Spring Boot settings for circular references, Freemarker, MVC, multipart, and JPA dialect.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
spring.freemarker.checkTemplateLocation: "false" # spring freemarker configuration spring.freemarker.checkTemplateLocation: "false" # spring freemarker configuration
spring.mvc.async.request-timeout: "${SPRING_MVC_ASYNC_REQUEST_TIMEOUT:30000}" # The default timeout for asynchronous requests in milliseconds spring.mvc.async.request-timeout: "${SPRING_MVC_ASYNC_REQUEST_TIMEOUT:30000}" # The default timeout for asynchronous requests in milliseconds
@ -843,6 +924,7 @@ spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PR
spring.jpa.properties.hibernate.dialect: "${SPRING_JPA_DIALECT:org.thingsboard.server.dao.ThingsboardPostgreSQLDialect}" # we use custom dialect that contains ilike(arg1, arg2) function (is interpreted to postgres ILIKE operator) spring.jpa.properties.hibernate.dialect: "${SPRING_JPA_DIALECT:org.thingsboard.server.dao.ThingsboardPostgreSQLDialect}" # we use custom dialect that contains ilike(arg1, arg2) function (is interpreted to postgres ILIKE operator)
# SQL DAO Configuration parameters # SQL DAO Configuration parameters
# Configures Spring JPA, Hibernate DDL, and HikariCP datasource connections for the primary and events databases.
spring: spring:
data: data:
jpa: jpa:
@ -893,6 +975,7 @@ spring:
registerMbeans: "${SPRING_EVENTS_DATASOURCE_HIKARI_REGISTER_MBEANS:false}" registerMbeans: "${SPRING_EVENTS_DATASOURCE_HIKARI_REGISTER_MBEANS:false}"
# Audit log parameters # Audit log parameters
# Configures audit logging levels per entity type and optional forwarding to an external sink such as Elasticsearch.
audit-log: audit-log:
# Enable/disable audit log functionality. # Enable/disable audit log functionality.
enabled: "${AUDIT_LOG_ENABLED:true}" enabled: "${AUDIT_LOG_ENABLED:true}"
@ -935,6 +1018,7 @@ audit-log:
password: "${AUDIT_LOG_SINK_PASSWORD:}" # Password used to access external sink system password: "${AUDIT_LOG_SINK_PASSWORD:}" # Password used to access external sink system
# Device state parameters # Device state parameters
# Configures device inactivity detection, state persistence strategy, and rate limits for state rule nodes.
state: state:
# Device inactivity timeout is a global configuration parameter that defines when the device will be marked as "inactive" by the server. # Device inactivity timeout is a global configuration parameter that defines when the device will be marked as "inactive" by the server.
# The parameter value is in seconds. A user can overwrite this parameter for an individual device by setting the “inactivityTimeout” server-side attribute (NOTE: expects value in milliseconds). # The parameter value is in seconds. A user can overwrite this parameter for an individual device by setting the “inactivityTimeout” server-side attribute (NOTE: expects value in milliseconds).
@ -965,7 +1049,8 @@ state:
# Refill is set to be greedy. Please refer to Bucket4j library documentation for more details. # Refill is set to be greedy. Please refer to Bucket4j library documentation for more details.
rateLimit: "${DEVICE_STATE_NODE_RATE_LIMIT_CONFIGURATION:1:1,30:60,60:3600}" rateLimit: "${DEVICE_STATE_NODE_RATE_LIMIT_CONFIGURATION:1:1,30:60,60:3600}"
# Tbel parameters # TBEL parameters
# Configures the ThingsBoard Expression Language (TBEL) engine: limits, timeouts, thread pool, and blacklisting.
tbel: tbel:
# Enable/Disable TBEL feature. # Enable/Disable TBEL feature.
enabled: "${TBEL_ENABLED:true}" enabled: "${TBEL_ENABLED:true}"
@ -994,6 +1079,7 @@ tbel:
print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}"
# JS parameters # JS parameters
# Configures the JavaScript execution engine (local Nashorn or remote Node.js): limits, sandboxing, and thread pools.
js: js:
# local (Nashorn Engine, deprecated) OR remote JS-Executors (NodeJS) # local (Nashorn Engine, deprecated) OR remote JS-Executors (NodeJS)
evaluator: "${JS_EVALUATOR:local}" evaluator: "${JS_EVALUATOR:local}"
@ -1041,6 +1127,7 @@ js:
print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}"
# Transport configuration parameters # Transport configuration parameters
# Configures session management, rate limits, and protocol-specific settings for HTTP, MQTT, CoAP, LwM2M, and SNMP transports.
transport: transport:
sessions: sessions:
# Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device.
@ -1351,6 +1438,7 @@ transport:
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}" fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
# CoAP server parameters # CoAP server parameters
# Configures the standalone CoAP server bind address, port, DTLS encryption, and credential settings.
coap: coap:
server: server:
# Enable/disable coap server. # Enable/disable coap server.
@ -1435,6 +1523,7 @@ coap:
dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}"
# Device connectivity parameters # Device connectivity parameters
# Specifies the hosts, ports, and credentials exposed to the UI for generating device connection check commands.
device: device:
connectivity: connectivity:
http: http:
@ -1488,6 +1577,7 @@ device:
image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.8-stable}" image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.8-stable}"
# Edges parameters # Edges parameters
# Controls Edge instance gRPC communication, event storage, state persistence, and statistics reporting.
edges: edges:
# Enable/disable Edge instance # Enable/disable Edge instance
enabled: "${EDGES_ENABLED:true}" enabled: "${EDGES_ENABLED:true}"
@ -1556,6 +1646,7 @@ edges:
report-interval-millis: "${EDGES_STATS_REPORT_INTERVAL_MS:600000}" report-interval-millis: "${EDGES_STATS_REPORT_INTERVAL_MS:600000}"
# Spring doc common parameters # Spring doc common parameters
# Enables or disables the OpenAPI/Swagger documentation endpoint and sets the default media type.
springdoc: springdoc:
# If false swagger API docs will be unavailable # If false swagger API docs will be unavailable
api-docs.enabled: "${SWAGGER_ENABLED:true}" api-docs.enabled: "${SWAGGER_ENABLED:true}"
@ -1563,6 +1654,7 @@ springdoc:
default-produces-media-type: "${SWAGGER_DEFAULT_PRODUCES_MEDIA_TYPE:application/json}" default-produces-media-type: "${SWAGGER_DEFAULT_PRODUCES_MEDIA_TYPE:application/json}"
# Swagger common parameters # Swagger common parameters
# Configures Swagger UI metadata: title, description, contact, license, version, and API path patterns.
swagger: swagger:
# General swagger match pattern of swagger UI links # General swagger match pattern of swagger UI links
api_path: "${SWAGGER_API_PATH:/api/**}" api_path: "${SWAGGER_API_PATH:/api/**}"
@ -1594,6 +1686,7 @@ swagger:
doc_expansion: "${SWAGGER_DOC_EXPANSION:list}" doc_expansion: "${SWAGGER_DOC_EXPANSION:list}"
# Queue configuration parameters # Queue configuration parameters
# Configures the message queue backend (in-memory or Kafka) and all topic/partition/consumer settings for each service.
queue: queue:
type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -2007,12 +2100,14 @@ queue:
processing_interval: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}" processing_interval: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}"
# Event configuration parameters # Event configuration parameters
# Controls limits on debug event content size stored for rule chain and rule node execution.
event: event:
debug: debug:
# Maximum number of symbols per debug event. The event content will be truncated if needed # Maximum number of symbols per debug event. The event content will be truncated if needed
max-symbols: "${TB_MAX_DEBUG_EVENT_SYMBOLS:4096}" max-symbols: "${TB_MAX_DEBUG_EVENT_SYMBOLS:4096}"
# General service parameters # General service parameters
# Sets the deployment type (monolith or microservice) and assigns tenant profiles to specific Rule Engine instances.
service: service:
type: "${TB_SERVICE_TYPE:monolith}" # monolith or tb-core or tb-rule-engine type: "${TB_SERVICE_TYPE:monolith}" # monolith or tb-core or tb-rule-engine
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
@ -2026,6 +2121,7 @@ service:
executor_thread_pool_size: "${TB_RULE_ENGINE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" executor_thread_pool_size: "${TB_RULE_ENGINE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}"
# Metrics parameters # Metrics parameters
# Enables actuator metrics endpoint and configures system info (CPU, memory) persistence frequency and TTL.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
@ -2039,6 +2135,7 @@ metrics:
ttl: "${METRICS_SYSTEM_INFO_TTL_DAYS:7}" ttl: "${METRICS_SYSTEM_INFO_TTL_DAYS:7}"
# Version control parameters # Version control parameters
# Configures thread pools and Git repository folder for entity version control (export/import) operations.
vc: vc:
# Pool size for handling export tasks # Pool size for handling export tasks
thread_pool_size: "${TB_VC_POOL_SIZE:6}" thread_pool_size: "${TB_VC_POOL_SIZE:6}"
@ -2049,6 +2146,7 @@ vc:
repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}" repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}"
# Notification system parameters # Notification system parameters
# Configures thread pool size and deduplication intervals for the notification rule processing engine.
notification_system: notification_system:
# Specify thread pool size for Notification System processing notification rules and notification sending. Recommend value <= 10 # Specify thread pool size for Notification System processing notification rules and notification sending. Recommend value <= 10
thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}" thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}"
@ -2057,6 +2155,7 @@ notification_system:
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}" deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure and health indicator settings.
management: management:
endpoints: endpoints:
web: web:
@ -2069,6 +2168,7 @@ management:
enabled: "false" enabled: "false"
# Mobile application settings for Thingsboard mobile application # Mobile application settings for Thingsboard mobile application
# Specifies domain name and store links used for the ThingsBoard Live mobile application QR code and deep links.
mobileApp: mobileApp:
# Server domain name for Thingsboard Live mobile application # Server domain name for Thingsboard Live mobile application
domain: "${TB_MOBILE_APP_DOMAIN:demo.thingsboard.io}" domain: "${TB_MOBILE_APP_DOMAIN:demo.thingsboard.io}"
@ -2077,6 +2177,8 @@ mobileApp:
# Link to App Store for Thingsboard Live mobile application # Link to App Store for Thingsboard Live mobile application
appStoreLink: "${TB_MOBILE_APP_APP_STORE_LINK:https://apps.apple.com/us/app/thingsboard-live/id1594355695}" appStoreLink: "${TB_MOBILE_APP_APP_STORE_LINK:https://apps.apple.com/us/app/thingsboard-live/id1594355695}"
# MQTT client parameters
# Configures MQTT client retransmission behavior including max attempts, initial delay, and jitter factor.
mqtt: mqtt:
# MQTT client configuration parameters # MQTT client configuration parameters
client: client:

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

@ -188,8 +188,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
rule.setTriggerType(triggerConfig.getTriggerType()); rule.setTriggerType(triggerConfig.getTriggerType());
rule.setTriggerConfig(triggerConfig); rule.setTriggerConfig(triggerConfig);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTargets(DaoUtil.toUUIDs(targets)); recipientsConfig.setTargets(DaoUtil.toUUIDs(targets));
rule.setRecipientsConfig(recipientsConfig); rule.setRecipientsConfig(recipientsConfig);

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

@ -211,7 +211,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRule.setTriggerConfig(triggerConfig); notificationRule.setTriggerConfig(triggerConfig);
EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM);
Map<Integer, List<UUID>> escalationTable = new HashMap<>(); Map<Integer, List<UUID>> escalationTable = new HashMap<>();
recipientsConfig.setEscalationTable(escalationTable); recipientsConfig.setEscalationTable(escalationTable);
Map<Integer, NotificationApiWsClient> clients = new HashMap<>(); Map<Integer, NotificationApiWsClient> clients = new HashMap<>();
@ -329,7 +328,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRule.setTriggerConfig(triggerConfig); notificationRule.setTriggerConfig(triggerConfig);
EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM);
Map<Integer, List<UUID>> escalationTable = new HashMap<>(); Map<Integer, List<UUID>> escalationTable = new HashMap<>();
recipientsConfig.setEscalationTable(escalationTable); recipientsConfig.setEscalationTable(escalationTable);
@ -640,8 +638,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
rule.setTriggerConfig(triggerConfig); rule.setTriggerConfig(triggerConfig);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId()));
rule.setRecipientsConfig(recipientsConfig); rule.setRecipientsConfig(recipientsConfig);
rule = saveNotificationRule(rule); rule = saveNotificationRule(rule);
@ -671,8 +668,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
triggerConfig.setCreated(true); triggerConfig.setCreated(true);
rule.setTriggerConfig(triggerConfig); rule.setTriggerConfig(triggerConfig);
NotificationTarget target = createNotificationTarget(tenantAdminUserId); NotificationTarget target = createNotificationTarget(tenantAdminUserId);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
recipientsConfig.setTargets(List.of(target.getUuidId())); recipientsConfig.setTargets(List.of(target.getUuidId()));
rule.setRecipientsConfig(recipientsConfig); rule.setRecipientsConfig(recipientsConfig);
rule = saveNotificationRule(rule); rule = saveNotificationRule(rule);

2
common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java

@ -33,7 +33,7 @@ import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@JsonPropertyOrder({"title", "image", "mobileHide", "mobileOrder", "configuration", "name", "resources"}) @JsonPropertyOrder({"id", "createdTime", "tenantId", "title", "name", "image", "mobileHide", "mobileOrder", "assignedCustomers", "configuration", "resources", "version"})
public class Dashboard extends DashboardInfo implements ExportableEntity<DashboardId> { public class Dashboard extends DashboardInfo implements ExportableEntity<DashboardId> {
private static final long serialVersionUID = 872682138346187503L; private static final long serialVersionUID = 872682138346187503L;

16
common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java

@ -15,13 +15,29 @@
*/ */
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@JsonPropertyOrder({
"emailEnabled",
"smsEnabled",
"notificationEnabled",
"oauthEnabled",
"twoFaEnabled"
})
@Schema
@Data @Data
public class FeaturesInfo { public class FeaturesInfo {
@JsonProperty("emailEnabled")
boolean isEmailEnabled; boolean isEmailEnabled;
@JsonProperty("smsEnabled")
boolean isSmsEnabled; boolean isSmsEnabled;
@JsonProperty("notificationEnabled")
boolean isNotificationEnabled; boolean isNotificationEnabled;
@JsonProperty("oauthEnabled")
boolean isOauthEnabled; boolean isOauthEnabled;
@JsonProperty("twoFaEnabled")
boolean isTwoFaEnabled; boolean isTwoFaEnabled;
} }

11
common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@ -39,9 +40,15 @@ public class ShortCustomerInfo {
@NoXss @NoXss
private String title; private String title;
@JsonProperty("isPublic")
@Schema(description = "Indicates special 'Public' customer used to embed dashboards on public websites.") @Schema(description = "Indicates special 'Public' customer used to embed dashboards on public websites.")
@Getter @Setter private boolean publicCustomer;
private boolean isPublic;
@JsonProperty("isPublic")
public boolean isPublic() { return publicCustomer; }
@JsonProperty("isPublic")
public void setPublic(boolean publicCustomer) { this.publicCustomer = publicCustomer; }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {

2
common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@ -23,6 +24,7 @@ import java.util.List;
@Data @Data
public class SystemInfo { public class SystemInfo {
@Schema(description = "Is monolith.") @Schema(description = "Is monolith.")
@JsonProperty("monolith")
private boolean isMonolith; private boolean isMonolith;
@Schema(description = "System data.") @Schema(description = "System data.")
private List<SystemInfoData> systemData; private List<SystemInfoData> systemData;

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java

@ -84,6 +84,7 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
@Schema(description = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465") @Schema(description = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465")
private long assignTs; private long assignTs;
@Schema(description = "JSON object with alarm details") @Schema(description = "JSON object with alarm details")
@JsonProperty
private transient JsonNode details; private transient JsonNode details;
@Schema(description = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") @Schema(description = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true")
private boolean propagate; private boolean propagate;

8
common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.event; package org.thingsboard.server.common.data.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -26,10 +27,17 @@ public abstract class DebugEventFilter implements EventFilter {
@Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") @Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152")
protected String server; protected String server;
@Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"}) @Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"})
@JsonProperty("isError")
protected boolean isError; protected boolean isError;
@Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") @Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB")
protected String errorStr; protected String errorStr;
@JsonProperty("isError")
public boolean isError() {
return isError;
}
@JsonProperty("isError")
public void setIsError(boolean isError) { public void setIsError(boolean isError) {
this.isError = isError; this.isError = isError;
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class AdminSettingsId extends UUIDBased implements EntityId { public class AdminSettingsId extends UUIDBased implements EntityId {
@Serial @Serial
@ -33,7 +34,7 @@ public class AdminSettingsId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.ADMIN_SETTINGS; return EntityType.ADMIN_SETTINGS;

2
common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public final class AiModelId extends UUIDBased implements EntityId { public final class AiModelId extends UUIDBased implements EntityId {
@Serial @Serial
@ -36,6 +37,7 @@ public final class AiModelId extends UUIDBased implements EntityId {
@Override @Override
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY,
description = "Entity type of the AI model", description = "Entity type of the AI model",
example = "AI_MODEL", example = "AI_MODEL",
allowableValues = "AI_MODEL" allowableValues = "AI_MODEL"

4
common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public class AlarmId extends UUIDBased implements EntityId { public class AlarmId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -36,7 +36,7 @@ public class AlarmId extends UUIDBased implements EntityId {
return new AlarmId(UUID.fromString(alarmId)); return new AlarmId(UUID.fromString(alarmId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ALARM", allowableValues = "ALARM") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ALARM", allowableValues = "ALARM")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.ALARM; return EntityType.ALARM;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class ApiKeyId extends UUIDBased implements EntityId { public class ApiKeyId extends UUIDBased implements EntityId {
@Serial @Serial
@ -38,7 +39,7 @@ public class ApiKeyId extends UUIDBased implements EntityId {
} }
@Override @Override
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_KEY", allowableValues = "API_KEY") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "API_KEY", allowableValues = "API_KEY")
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.API_KEY; return EntityType.API_KEY;
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public class ApiUsageStateId extends UUIDBased implements EntityId { public class ApiUsageStateId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -34,7 +34,7 @@ public class ApiUsageStateId extends UUIDBased implements EntityId {
return new ApiUsageStateId(UUID.fromString(userId)); return new ApiUsageStateId(UUID.fromString(userId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.API_USAGE_STATE; return EntityType.API_USAGE_STATE;

4
common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public class AssetId extends UUIDBased implements EntityId { public class AssetId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -36,7 +36,7 @@ public class AssetId extends UUIDBased implements EntityId {
return new AssetId(UUID.fromString(assetId)); return new AssetId(UUID.fromString(assetId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET", allowableValues = "ASSET") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ASSET", allowableValues = "ASSET")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.ASSET; return EntityType.ASSET;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class AssetProfileId extends UUIDBased implements EntityId { public class AssetProfileId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -35,7 +36,7 @@ public class AssetProfileId extends UUIDBased implements EntityId {
return new AssetProfileId(UUID.fromString(assetProfileId)); return new AssetProfileId(UUID.fromString(assetProfileId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.ASSET_PROFILE; return EntityType.ASSET_PROFILE;

4
common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java

@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public class CalculatedFieldId extends UUIDBased implements EntityId { public class CalculatedFieldId extends UUIDBased implements EntityId {
@Serial @Serial
@ -38,7 +38,7 @@ public class CalculatedFieldId extends UUIDBased implements EntityId {
return new CalculatedFieldId(UUID.fromString(calculatedFieldId)); return new CalculatedFieldId(UUID.fromString(calculatedFieldId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD", allowableValues = "CALCULATED_FIELD") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "CALCULATED_FIELD", allowableValues = "CALCULATED_FIELD")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD; return EntityType.CALCULATED_FIELD;

4
common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public final class CustomerId extends UUIDBased implements EntityId { public final class CustomerId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -32,7 +32,7 @@ public final class CustomerId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.CUSTOMER; return EntityType.CUSTOMER;

4
common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public class DashboardId extends UUIDBased implements EntityId { public class DashboardId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -34,7 +34,7 @@ public class DashboardId extends UUIDBased implements EntityId {
return new DashboardId(UUID.fromString(dashboardId)); return new DashboardId(UUID.fromString(dashboardId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.DASHBOARD; return EntityType.DASHBOARD;

4
common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public class DeviceId extends UUIDBased implements EntityId { public class DeviceId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -37,7 +37,7 @@ public class DeviceId extends UUIDBased implements EntityId {
} }
@Override @Override
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE", allowableValues = "DEVICE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DEVICE", allowableValues = "DEVICE")
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.DEVICE; return EntityType.DEVICE;
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class DeviceProfileId extends UUIDBased implements EntityId { public class DeviceProfileId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -35,7 +36,7 @@ public class DeviceProfileId extends UUIDBased implements EntityId {
return new DeviceProfileId(UUID.fromString(deviceProfileId)); return new DeviceProfileId(UUID.fromString(deviceProfileId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.DEVICE_PROFILE; return EntityType.DEVICE_PROFILE;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java

@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class DomainId extends UUIDBased implements EntityId { public class DomainId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -32,6 +34,7 @@ public class DomainId extends UUIDBased implements EntityId {
return new DomainId(UUID.fromString(oauth2DomainId)); return new DomainId(UUID.fromString(oauth2DomainId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DOMAIN", allowableValues = "DOMAIN")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.DOMAIN; return EntityType.DOMAIN;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class EdgeId extends UUIDBased implements EntityId { public class EdgeId extends UUIDBased implements EntityId {
@Serial @Serial
@ -43,7 +44,7 @@ public class EdgeId extends UUIDBased implements EntityId {
return new EdgeId(UUID.fromString(edgeId)); return new EdgeId(UUID.fromString(edgeId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "EDGE", allowableValues = "EDGE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "EDGE", allowableValues = "EDGE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.EDGE; return EntityType.EDGE;

43
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -26,7 +27,47 @@ import java.util.UUID;
@JsonDeserialize(using = EntityIdDeserializer.class) @JsonDeserialize(using = EntityIdDeserializer.class)
@JsonSerialize(using = EntityIdSerializer.class) @JsonSerialize(using = EntityIdSerializer.class)
@Schema @Schema(
discriminatorProperty = "entityType",
discriminatorMapping = {
@DiscriminatorMapping(value = "ADMIN_SETTINGS", schema = AdminSettingsId.class),
@DiscriminatorMapping(value = "AI_MODEL", schema = AiModelId.class),
@DiscriminatorMapping(value = "ALARM", schema = AlarmId.class),
@DiscriminatorMapping(value = "API_KEY", schema = ApiKeyId.class),
@DiscriminatorMapping(value = "API_USAGE_STATE", schema = ApiUsageStateId.class),
@DiscriminatorMapping(value = "ASSET", schema = AssetId.class),
@DiscriminatorMapping(value = "ASSET_PROFILE", schema = AssetProfileId.class),
@DiscriminatorMapping(value = "CALCULATED_FIELD", schema = CalculatedFieldId.class),
@DiscriminatorMapping(value = "CUSTOMER", schema = CustomerId.class),
@DiscriminatorMapping(value = "DASHBOARD", schema = DashboardId.class),
@DiscriminatorMapping(value = "DEVICE", schema = DeviceId.class),
@DiscriminatorMapping(value = "DEVICE_PROFILE", schema = DeviceProfileId.class),
@DiscriminatorMapping(value = "DOMAIN", schema = DomainId.class),
@DiscriminatorMapping(value = "EDGE", schema = EdgeId.class),
@DiscriminatorMapping(value = "ENTITY_VIEW", schema = EntityViewId.class),
@DiscriminatorMapping(value = "JOB", schema = JobId.class),
@DiscriminatorMapping(value = "MOBILE_APP", schema = MobileAppId.class),
@DiscriminatorMapping(value = "MOBILE_APP_BUNDLE", schema = MobileAppBundleId.class),
@DiscriminatorMapping(value = "NOTIFICATION", schema = NotificationId.class),
@DiscriminatorMapping(value = "NOTIFICATION_REQUEST", schema = NotificationRequestId.class),
@DiscriminatorMapping(value = "NOTIFICATION_RULE", schema = NotificationRuleId.class),
@DiscriminatorMapping(value = "NOTIFICATION_TARGET", schema = NotificationTargetId.class),
@DiscriminatorMapping(value = "NOTIFICATION_TEMPLATE", schema = NotificationTemplateId.class),
@DiscriminatorMapping(value = "OAUTH2_CLIENT", schema = OAuth2ClientId.class),
@DiscriminatorMapping(value = "OTA_PACKAGE", schema = OtaPackageId.class),
@DiscriminatorMapping(value = "QUEUE", schema = QueueId.class),
@DiscriminatorMapping(value = "QUEUE_STATS", schema = QueueStatsId.class),
@DiscriminatorMapping(value = "RPC", schema = RpcId.class),
@DiscriminatorMapping(value = "RULE_CHAIN", schema = RuleChainId.class),
@DiscriminatorMapping(value = "RULE_NODE", schema = RuleNodeId.class),
@DiscriminatorMapping(value = "TB_RESOURCE", schema = TbResourceId.class),
@DiscriminatorMapping(value = "TENANT", schema = TenantId.class),
@DiscriminatorMapping(value = "TENANT_PROFILE", schema = TenantProfileId.class),
@DiscriminatorMapping(value = "USER", schema = UserId.class),
@DiscriminatorMapping(value = "WIDGETS_BUNDLE", schema = WidgetsBundleId.class),
@DiscriminatorMapping(value = "WIDGET_TYPE", schema = WidgetTypeId.class)
}
)
public interface EntityId extends HasUUID, Serializable { //NOSONAR, the constant is closely related to EntityId public interface EntityId extends HasUUID, Serializable { //NOSONAR, the constant is closely related to EntityId
UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080"); UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080");

3
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java

@ -25,6 +25,7 @@ import java.util.UUID;
/** /**
* Created by Victor Basanets on 8/27/2017. * Created by Victor Basanets on 8/27/2017.
*/ */
@Schema(allOf = EntityId.class)
public class EntityViewId extends UUIDBased implements EntityId { public class EntityViewId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -38,7 +39,7 @@ public class EntityViewId extends UUIDBased implements EntityId {
return new EntityViewId(UUID.fromString(entityViewID)); return new EntityViewId(UUID.fromString(entityViewID));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.ENTITY_VIEW; return EntityType.ENTITY_VIEW;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class JobId extends UUIDBased implements EntityId { public class JobId extends UUIDBased implements EntityId {
@Serial @Serial
@ -33,7 +34,7 @@ public class JobId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "JOB", allowableValues = "JOB") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "JOB", allowableValues = "JOB")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.JOB; return EntityType.JOB;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java

@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class MobileAppBundleId extends UUIDBased implements EntityId{ public class MobileAppBundleId extends UUIDBased implements EntityId{
@JsonCreator @JsonCreator
@ -32,6 +34,7 @@ public class MobileAppBundleId extends UUIDBased implements EntityId{
return new MobileAppBundleId(UUID.fromString(mobileAppId)); return new MobileAppBundleId(UUID.fromString(mobileAppId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "MOBILE_APP_BUNDLE", allowableValues = "MOBILE_APP_BUNDLE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.MOBILE_APP_BUNDLE; return EntityType.MOBILE_APP_BUNDLE;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java

@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class MobileAppId extends UUIDBased implements EntityId{ public class MobileAppId extends UUIDBased implements EntityId{
@JsonCreator @JsonCreator
@ -32,6 +34,7 @@ public class MobileAppId extends UUIDBased implements EntityId{
return new MobileAppId(UUID.fromString(mobileAppId)); return new MobileAppId(UUID.fromString(mobileAppId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "MOBILE_APP", allowableValues = "MOBILE_APP")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.MOBILE_APP; return EntityType.MOBILE_APP;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class NotificationId extends UUIDBased implements EntityId { public class NotificationId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -29,7 +30,7 @@ public class NotificationId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION; return EntityType.NOTIFICATION;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class NotificationRequestId extends UUIDBased implements EntityId { public class NotificationRequestId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -29,7 +30,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION_REQUEST; return EntityType.NOTIFICATION_REQUEST;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class NotificationRuleId extends UUIDBased implements EntityId { public class NotificationRuleId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -29,7 +30,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION_RULE; return EntityType.NOTIFICATION_RULE;

4
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public class NotificationTargetId extends UUIDBased implements EntityId { public class NotificationTargetId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -30,7 +30,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION_TARGET; return EntityType.NOTIFICATION_TARGET;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class NotificationTemplateId extends UUIDBased implements EntityId { public class NotificationTemplateId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -29,7 +30,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION_TEMPLATE; return EntityType.NOTIFICATION_TEMPLATE;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java

@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class OAuth2ClientId extends UUIDBased implements EntityId { public class OAuth2ClientId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -32,6 +34,7 @@ public class OAuth2ClientId extends UUIDBased implements EntityId {
return new OAuth2ClientId(UUID.fromString(oauth2ClientId)); return new OAuth2ClientId(UUID.fromString(oauth2ClientId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "OAUTH2_CLIENT", allowableValues = "OAUTH2_CLIENT")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.OAUTH2_CLIENT; return EntityType.OAUTH2_CLIENT;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class OtaPackageId extends UUIDBased implements EntityId { public class OtaPackageId extends UUIDBased implements EntityId {
@Serial @Serial
@ -37,7 +38,7 @@ public class OtaPackageId extends UUIDBased implements EntityId {
return new OtaPackageId(UUID.fromString(firmwareId)); return new OtaPackageId(UUID.fromString(firmwareId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.OTA_PACKAGE; return EntityType.OTA_PACKAGE;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class QueueId extends UUIDBased implements EntityId { public class QueueId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -35,7 +36,7 @@ public class QueueId extends UUIDBased implements EntityId {
return new QueueId(UUID.fromString(queueId)); return new QueueId(UUID.fromString(queueId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE", allowableValues = "QUEUE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "QUEUE", allowableValues = "QUEUE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.QUEUE; return EntityType.QUEUE;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class QueueStatsId extends UUIDBased implements EntityId { public class QueueStatsId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -35,7 +36,7 @@ public class QueueStatsId extends UUIDBased implements EntityId {
return new QueueStatsId(UUID.fromString(queueId)); return new QueueStatsId(UUID.fromString(queueId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.QUEUE_STATS; return EntityType.QUEUE_STATS;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public final class RpcId extends UUIDBased implements EntityId { public final class RpcId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -31,7 +32,7 @@ public final class RpcId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RPC", allowableValues = "RPC") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RPC", allowableValues = "RPC")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.RPC; return EntityType.RPC;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class RuleChainId extends UUIDBased implements EntityId { public class RuleChainId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -29,7 +30,7 @@ public class RuleChainId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.RULE_CHAIN; return EntityType.RULE_CHAIN;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class RuleNodeId extends UUIDBased implements EntityId { public class RuleNodeId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -29,7 +30,7 @@ public class RuleNodeId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.RULE_NODE; return EntityType.RULE_NODE;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class TbResourceId extends UUIDBased implements EntityId { public class TbResourceId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -31,7 +32,7 @@ public class TbResourceId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.TB_RESOURCE; return EntityType.TB_RESOURCE;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public final class TenantId extends UUIDBased implements EntityId { public final class TenantId extends UUIDBased implements EntityId {
@JsonIgnore @JsonIgnore
@ -54,7 +55,7 @@ public final class TenantId extends UUIDBased implements EntityId {
return this.equals(SYS_TENANT_ID); return this.equals(SYS_TENANT_ID);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT", allowableValues = "TENANT") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TENANT", allowableValues = "TENANT")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.TENANT; return EntityType.TENANT;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public class TenantProfileId extends UUIDBased implements EntityId { public class TenantProfileId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -35,7 +36,7 @@ public class TenantProfileId extends UUIDBased implements EntityId {
return new TenantProfileId(UUID.fromString(tenantProfileId)); return new TenantProfileId(UUID.fromString(tenantProfileId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.TENANT_PROFILE; return EntityType.TENANT_PROFILE;

4
common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema @Schema(allOf = EntityId.class)
public class UserId extends UUIDBased implements EntityId { public class UserId extends UUIDBased implements EntityId {
@JsonCreator @JsonCreator
@ -34,7 +34,7 @@ public class UserId extends UUIDBased implements EntityId {
return new UserId(UUID.fromString(userId)); return new UserId(UUID.fromString(userId));
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "USER", allowableValues = "USER") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "USER", allowableValues = "USER")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.USER; return EntityType.USER;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public final class WidgetTypeId extends UUIDBased implements EntityId { public final class WidgetTypeId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -31,7 +32,7 @@ public final class WidgetTypeId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.WIDGET_TYPE; return EntityType.WIDGET_TYPE;

3
common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@Schema(allOf = EntityId.class)
public final class WidgetsBundleId extends UUIDBased implements EntityId { public final class WidgetsBundleId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -31,7 +32,7 @@ public final class WidgetsBundleId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.WIDGETS_BUNDLE; return EntityType.WIDGETS_BUNDLE;

83
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java

@ -25,62 +25,127 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Schema(description = "Default notification rule recipients configuration", allOf = NotificationRuleRecipientsConfig.class) @Schema(description = "Default notification rule recipients configuration")
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode
public class DefaultNotificationRuleRecipientsConfig extends NotificationRuleRecipientsConfig { public abstract class DefaultNotificationRuleRecipientsConfig implements NotificationRuleRecipientsConfig {
@NotEmpty @NotEmpty
private List<UUID> targets; private List<UUID> targets;
@Override
@Schema(hidden = true)
public NotificationRuleTriggerType getTriggerType() {
return super.getTriggerType();
}
@Override @Override
public Map<Integer, List<UUID>> getTargetsTable() { public Map<Integer, List<UUID>> getTargetsTable() {
return Map.of(0, targets); return Map.of(0, targets);
} }
public static DefaultNotificationRuleRecipientsConfig forTriggerType(NotificationRuleTriggerType triggerType) {
return switch (triggerType) {
case ENTITY_ACTION -> new EntityActionRecipientsConfig();
case ALARM_COMMENT -> new AlarmCommentRecipientsConfig();
case ALARM_ASSIGNMENT -> new AlarmAssignmentRecipientsConfig();
case DEVICE_ACTIVITY -> new DeviceActivityRecipientsConfig();
case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT -> new RuleEngineComponentLifecycleEventRecipientsConfig();
case EDGE_CONNECTION -> new EdgeConnectionRecipientsConfig();
case EDGE_COMMUNICATION_FAILURE -> new EdgeCommunicationFailureRecipientsConfig();
case NEW_PLATFORM_VERSION -> new NewPlatformVersionRecipientsConfig();
case ENTITIES_LIMIT -> new EntitiesLimitRecipientsConfig();
case API_USAGE_LIMIT -> new ApiUsageLimitRecipientsConfig();
case RATE_LIMITS -> new RateLimitsRecipientsConfig();
case TASK_PROCESSING_FAILURE -> new TaskProcessingFailureRecipientsConfig();
case RESOURCES_SHORTAGE -> new ResourceShortageRecipientsConfig();
default -> throw new IllegalArgumentException("Unsupported trigger type for default recipients config: " + triggerType);
};
}
public static class EntityActionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class EntityActionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ENTITY_ACTION;
}
} }
public static class AlarmCommentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class AlarmCommentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM_COMMENT;
}
} }
public static class AlarmAssignmentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class AlarmAssignmentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM_ASSIGNMENT;
}
} }
public static class DeviceActivityRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class DeviceActivityRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.DEVICE_ACTIVITY;
}
} }
public static class RuleEngineComponentLifecycleEventRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class RuleEngineComponentLifecycleEventRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT;
}
} }
public static class EdgeConnectionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class EdgeConnectionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_CONNECTION;
}
} }
public static class EdgeCommunicationFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class EdgeCommunicationFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE;
}
} }
public static class NewPlatformVersionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class NewPlatformVersionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.NEW_PLATFORM_VERSION;
}
} }
public static class EntitiesLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class EntitiesLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ENTITIES_LIMIT;
}
} }
public static class ApiUsageLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class ApiUsageLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.API_USAGE_LIMIT;
}
} }
public static class RateLimitsRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class RateLimitsRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RATE_LIMITS;
}
} }
public static class TaskProcessingFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class TaskProcessingFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.TASK_PROCESSING_FAILURE;
}
} }
public static class ResourceShortageRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { public static class ResourceShortageRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RESOURCES_SHORTAGE;
}
} }
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java

@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -26,12 +27,17 @@ import java.util.UUID;
@Schema(description = "Escalated notification rule recipients configuration") @Schema(description = "Escalated notification rule recipients configuration")
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode
public class EscalatedNotificationRuleRecipientsConfig extends NotificationRuleRecipientsConfig { public class EscalatedNotificationRuleRecipientsConfig implements NotificationRuleRecipientsConfig {
@NotEmpty @NotEmpty
private Map<Integer, List<UUID>> escalationTable; private Map<Integer, List<UUID>> escalationTable;
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM;
}
@Override @Override
public Map<Integer, List<UUID>> getTargetsTable() { public Map<Integer, List<UUID>> getTargetsTable() {
return escalationTable; return escalationTable;

14
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java

@ -17,14 +17,11 @@ package org.thingsboard.server.common.data.notification.rule;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serializable; import java.io.Serializable;
@ -51,7 +48,7 @@ import java.util.UUID;
@DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class) @DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class)
}) })
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", include = JsonTypeInfo.As.EXISTING_PROPERTY)
@JsonSubTypes({ @JsonSubTypes({
@Type(name = "ALARM", value = EscalatedNotificationRuleRecipientsConfig.class), @Type(name = "ALARM", value = EscalatedNotificationRuleRecipientsConfig.class),
@Type(name = "ENTITY_ACTION", value = DefaultNotificationRuleRecipientsConfig.EntityActionRecipientsConfig.class), @Type(name = "ENTITY_ACTION", value = DefaultNotificationRuleRecipientsConfig.EntityActionRecipientsConfig.class),
@ -68,14 +65,11 @@ import java.util.UUID;
@Type(name = "TASK_PROCESSING_FAILURE", value = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class), @Type(name = "TASK_PROCESSING_FAILURE", value = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class),
@Type(name = "RESOURCES_SHORTAGE", value = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class) @Type(name = "RESOURCES_SHORTAGE", value = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class)
}) })
@Data public interface NotificationRuleRecipientsConfig extends Serializable {
public abstract class NotificationRuleRecipientsConfig implements Serializable {
@NotNull NotificationRuleTriggerType getTriggerType();
@JsonProperty("triggerType")
private NotificationRuleTriggerType triggerType;
@JsonIgnore @JsonIgnore
public abstract Map<Integer, List<UUID>> getTargetsTable(); Map<Integer, List<UUID>> getTargetsTable();
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java

@ -15,16 +15,17 @@
*/ */
package org.thingsboard.server.common.data.query; package org.thingsboard.server.common.data.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@Schema
@Data @Data
public class RelationsQueryFilter implements EntityFilter { public class RelationsQueryFilter implements EntityFilter {
@ -34,6 +35,7 @@ public class RelationsQueryFilter implements EntityFilter {
} }
private AliasEntityId rootEntity; private AliasEntityId rootEntity;
@JsonProperty("multiRoot")
private boolean isMultiRoot; private boolean isMultiRoot;
private EntityType multiRootEntitiesType; private EntityType multiRootEntitiesType;
private Set<String> multiRootEntityIds; private Set<String> multiRootEntityIds;

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java

@ -23,6 +23,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
@Schema @Schema
@ -31,6 +32,9 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
@Data @Data
public class DeviceExportData extends EntityExportData<Device> { public class DeviceExportData extends EntityExportData<Device> {
@Override
public EntityType getEntityType() { return EntityType.DEVICE; }
@JsonProperty(index = 3) @JsonProperty(index = 3)
@JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"}) @JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"})
private DeviceCredentials credentials; private DeviceCredentials credentials;

114
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java

@ -65,6 +65,7 @@ import java.util.Map;
@Type(name = "DEVICE_PROFILE", value = EntityExportData.DeviceProfileExportData.class), @Type(name = "DEVICE_PROFILE", value = EntityExportData.DeviceProfileExportData.class),
@Type(name = "ENTITY_VIEW", value = EntityExportData.EntityViewExportData.class), @Type(name = "ENTITY_VIEW", value = EntityExportData.EntityViewExportData.class),
@Type(name = "NOTIFICATION_TEMPLATE", value = EntityExportData.NotificationTemplateExportData.class), @Type(name = "NOTIFICATION_TEMPLATE", value = EntityExportData.NotificationTemplateExportData.class),
@Type(name = "NOTIFICATION_TARGET", value = EntityExportData.NotificationTargetExportData.class),
@Type(name = "NOTIFICATION_RULE", value = EntityExportData.NotificationRuleExportData.class), @Type(name = "NOTIFICATION_RULE", value = EntityExportData.NotificationRuleExportData.class),
@Type(name = "AI_MODEL", value = EntityExportData.AiModelExportData.class) @Type(name = "AI_MODEL", value = EntityExportData.AiModelExportData.class)
}) })
@ -92,7 +93,7 @@ import java.util.Map;
} }
) )
@Data @Data
public class EntityExportData<E extends ExportableEntity<? extends EntityId>> { public abstract class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
public static final Comparator<EntityRelation> relationsComparator = Comparator public static final Comparator<EntityRelation> relationsComparator = Comparator
.comparing(EntityRelation::getFrom, Comparator.comparing(EntityId::getId)) .comparing(EntityRelation::getFrom, Comparator.comparing(EntityId::getId))
@ -111,7 +112,7 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
private E entity; private E entity;
@JsonProperty(index = 1) @JsonProperty(index = 1)
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private EntityType entityType; public abstract EntityType getEntityType();
@JsonProperty(index = 100) @JsonProperty(index = 100)
@ArraySchema(schema = @Schema(implementation = EntityRelation.class)) @ArraySchema(schema = @Schema(implementation = EntityRelation.class))
@ -124,6 +125,28 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
@ArraySchema(schema = @Schema(implementation = CalculatedField.class)) @ArraySchema(schema = @Schema(implementation = CalculatedField.class))
private List<CalculatedField> calculatedFields; private List<CalculatedField> calculatedFields;
public static EntityExportData<?> newInstance(EntityType entityType) {
return switch (entityType) {
case DEVICE -> new DeviceExportData();
case RULE_CHAIN -> new RuleChainExportData();
case WIDGET_TYPE -> new WidgetTypeExportData();
case WIDGETS_BUNDLE -> new WidgetsBundleExportData();
case OTA_PACKAGE -> new OtaPackageExportData();
case CUSTOMER -> new CustomerExportData();
case TB_RESOURCE -> new TbResourceExportData();
case DASHBOARD -> new DashboardExportData();
case ASSET_PROFILE -> new AssetProfileExportData();
case ASSET -> new AssetExportData();
case DEVICE_PROFILE -> new DeviceProfileExportData();
case ENTITY_VIEW -> new EntityViewExportData();
case NOTIFICATION_TEMPLATE -> new NotificationTemplateExportData();
case NOTIFICATION_TARGET -> new NotificationTargetExportData();
case NOTIFICATION_RULE -> new NotificationRuleExportData();
case AI_MODEL -> new AiModelExportData();
default -> throw new IllegalArgumentException("Unsupported entity type: " + entityType);
};
}
public EntityExportData<E> sort() { public EntityExportData<E> sort() {
if (relations != null && !relations.isEmpty()) { if (relations != null && !relations.isEmpty()) {
relations.sort(relationsComparator); relations.sort(relationsComparator);
@ -163,26 +186,91 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
} }
@Schema @Schema
public static class CustomerExportData extends EntityExportData<Customer> {} public static class CustomerExportData extends EntityExportData<Customer> {
@Override
public EntityType getEntityType() {
return EntityType.CUSTOMER;
}
}
@Schema @Schema
public static class TbResourceExportData extends EntityExportData<TbResource> {} public static class TbResourceExportData extends EntityExportData<TbResource> {
@Override
public EntityType getEntityType() {
return EntityType.TB_RESOURCE;
}
}
@Schema @Schema
public static class DashboardExportData extends EntityExportData<Dashboard> {} public static class DashboardExportData extends EntityExportData<Dashboard> {
@Override
public EntityType getEntityType() {
return EntityType.DASHBOARD;
}
}
@Schema @Schema
public static class AssetProfileExportData extends EntityExportData<AssetProfile> {} public static class AssetProfileExportData extends EntityExportData<AssetProfile> {
@Override
public EntityType getEntityType() {
return EntityType.ASSET_PROFILE;
}
}
@Schema @Schema
public static class AssetExportData extends EntityExportData<Asset> {} public static class AssetExportData extends EntityExportData<Asset> {
@Override
public EntityType getEntityType() {
return EntityType.ASSET;
}
}
@Schema @Schema
public static class DeviceProfileExportData extends EntityExportData<DeviceProfile> {} public static class DeviceProfileExportData extends EntityExportData<DeviceProfile> {
@Override
public EntityType getEntityType() {
return EntityType.DEVICE_PROFILE;
}
}
@Schema @Schema
public static class EntityViewExportData extends EntityExportData<EntityView> {} public static class EntityViewExportData extends EntityExportData<EntityView> {
@Override
public EntityType getEntityType() {
return EntityType.ENTITY_VIEW;
}
}
@Schema @Schema
public static class NotificationTemplateExportData extends EntityExportData<NotificationTemplate> {} public static class NotificationTemplateExportData extends EntityExportData<NotificationTemplate> {
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TEMPLATE;
}
}
@Schema @Schema
public static class NotificationTargetExportData extends EntityExportData<NotificationTarget> {} public static class NotificationTargetExportData extends EntityExportData<NotificationTarget> {
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TARGET;
}
}
@Schema @Schema
public static class NotificationRuleExportData extends EntityExportData<NotificationRule> {} public static class NotificationRuleExportData extends EntityExportData<NotificationRule> {
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_RULE;
}
}
@Schema @Schema
public static class AiModelExportData extends EntityExportData<AiModel> {} public static class AiModelExportData extends EntityExportData<AiModel> {
@Override
public EntityType getEntityType() {
return EntityType.AI_MODEL;
}
}
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java

@ -18,12 +18,16 @@ package org.thingsboard.server.common.data.sync.ie;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
@Schema @Schema
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class OtaPackageExportData extends EntityExportData<OtaPackage> { public class OtaPackageExportData extends EntityExportData<OtaPackage> {
@Override
public EntityType getEntityType() { return EntityType.OTA_PACKAGE; }
/* /*
* OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking) * OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking)
* We override both methods to ensure 'version' is not ignored during (de)serialization. * We override both methods to ensure 'version' is not ignored during (de)serialization.

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java

@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -30,6 +31,9 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@Data @Data
public class RuleChainExportData extends EntityExportData<RuleChain> { public class RuleChainExportData extends EntityExportData<RuleChain> {
@Override
public EntityType getEntityType() { return EntityType.RULE_CHAIN; }
@JsonProperty(index = 3) @JsonProperty(index = 3)
@JsonIgnoreProperties({"ruleChainId", "version"}) @JsonIgnoreProperties({"ruleChainId", "version"})
private RuleChainMetaData metaData; private RuleChainMetaData metaData;

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.sync.ie;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
@Schema @Schema
@ -25,4 +26,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class WidgetTypeExportData extends EntityExportData<WidgetTypeDetails> { public class WidgetTypeExportData extends EntityExportData<WidgetTypeDetails> {
@Override
public EntityType getEntityType() { return EntityType.WIDGET_TYPE; }
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java

@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import java.util.ArrayList; import java.util.ArrayList;
@ -32,6 +33,9 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class WidgetsBundleExportData extends EntityExportData<WidgetsBundle> { public class WidgetsBundleExportData extends EntityExportData<WidgetsBundle> {
@Override
public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; }
@ArraySchema(arraySchema = @Schema(description = "List of widgets in the bundle"), schema = @Schema(implementation = JsonNode.class)) @ArraySchema(arraySchema = @Schema(description = "List of widgets in the bundle"), schema = @Schema(implementation = JsonNode.class))
@JsonProperty(index = 3) @JsonProperty(index = 3)
private List<ObjectNode> widgets; private List<ObjectNode> widgets;

2
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java

@ -33,7 +33,7 @@ import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId", "resources"}) @JsonPropertyOrder({"id", "createdTime", "tenantId", "fqn", "name", "deprecated", "scada", "version", "descriptor", "image", "description", "tags", "externalId", "resources"})
public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity<WidgetTypeId> { public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity<WidgetTypeId> {
@Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") @Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.")

50
common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java

@ -15,8 +15,18 @@
*/ */
package org.thingsboard.server.common.data.id; package org.thingsboard.server.common.data.id;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.EntityType;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
public class EntityIdTest { public class EntityIdTest {
@ -25,4 +35,44 @@ public class EntityIdTest {
Assertions.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString()); Assertions.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString());
} }
@Test
public void allEntityIdImplementors_shouldBeInDiscriminatorMapping() {
Schema schemaAnnotation = EntityId.class.getAnnotation(Schema.class);
assertThat(schemaAnnotation).as("EntityId must have @Schema annotation").isNotNull();
DiscriminatorMapping[] mappings = schemaAnnotation.discriminatorMapping();
Map<String, Class<?>> discriminatorMap = Arrays.stream(mappings)
.collect(Collectors.toMap(DiscriminatorMapping::value, DiscriminatorMapping::schema));
UUID testUuid = UUID.randomUUID();
for (EntityType entityType : EntityType.values()) {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid);
String typeName = entityType.name();
assertThat(discriminatorMap)
.as("EntityId @Schema discriminatorMapping is missing entry for EntityType." + typeName)
.containsKey(typeName);
assertThat(discriminatorMap.get(typeName))
.as("Discriminator mapping for " + typeName + " should point to " + entityId.getClass().getSimpleName())
.isEqualTo(entityId.getClass());
}
}
@Test
public void allEntityIdImplementors_shouldHaveAllOfEntityId() {
UUID testUuid = UUID.randomUUID();
for (EntityType entityType : EntityType.values()) {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid);
Class<?> idClass = entityId.getClass();
Schema schemaAnnotation = idClass.getAnnotation(Schema.class);
assertThat(schemaAnnotation)
.as(idClass.getSimpleName() + " must have @Schema annotation")
.isNotNull();
assertThat(schemaAnnotation.allOf())
.as(idClass.getSimpleName() + " @Schema must include allOf = EntityId.class")
.contains(EntityId.class);
}
}
} }

52
common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java

@ -0,0 +1,52 @@
/**
* Copyright © 2016-2026 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.sync.ie;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.EntityType;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
public class EntityExportDataTest {
@Test
public void newInstance_shouldSupportAllJsonSubTypes() {
JsonSubTypes subTypes = EntityExportData.class.getAnnotation(JsonSubTypes.class);
assertThat(subTypes).as("EntityExportData must have @JsonSubTypes annotation").isNotNull();
Set<String> jsonSubTypeNames = Arrays.stream(subTypes.value())
.map(JsonSubTypes.Type::name)
.collect(Collectors.toSet());
for (String typeName : jsonSubTypeNames) {
EntityType entityType = EntityType.valueOf(typeName);
EntityExportData<?> instance = EntityExportData.newInstance(entityType);
assertThat(instance)
.as("newInstance(%s) should not return null", typeName)
.isNotNull();
assertThat(instance.getEntityType())
.as("newInstance(%s).getEntityType() should return %s", typeName, entityType)
.isEqualTo(entityType);
}
}
}

56
common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java

@ -38,6 +38,7 @@ public class SsrfProtectionValidator {
private static final Set<String> BLOCKED_HOSTNAME_SUFFIXES = Set.of(".internal", ".local"); private static final Set<String> BLOCKED_HOSTNAME_SUFFIXES = Set.of(".internal", ".local");
private static volatile AdditionalBlockedHosts additionalBlocked = AdditionalBlockedHosts.EMPTY; private static volatile AdditionalBlockedHosts additionalBlocked = AdditionalBlockedHosts.EMPTY;
private static volatile AllowedHosts allowedHosts = AllowedHosts.EMPTY;
// Well-known cloud metadata endpoints not covered by the JDK checks (isLoopback, isSiteLocal, isLinkLocal) // Well-known cloud metadata endpoints not covered by the JDK checks (isLoopback, isSiteLocal, isLinkLocal)
private static final List<CidrRange> CLOUD_METADATA_RANGES = List.of( private static final List<CidrRange> CLOUD_METADATA_RANGES = List.of(
@ -66,6 +67,13 @@ public class SsrfProtectionValidator {
} }
String hostLower = host.toLowerCase(); String hostLower = host.toLowerCase();
// Allow-listed hostnames bypass all hostname and IP checks
AllowedHosts currentAllowed = allowedHosts;
if (currentAllowed.hostnames.contains(hostLower)) {
return;
}
if (BLOCKED_HOSTNAMES.contains(hostLower) || additionalBlocked.hostnames.contains(hostLower)) { if (BLOCKED_HOSTNAMES.contains(hostLower) || additionalBlocked.hostnames.contains(hostLower)) {
throwBlockedHost(host); throwBlockedHost(host);
} }
@ -98,7 +106,15 @@ public class SsrfProtectionValidator {
} }
} }
private static boolean isBlockedAddress(InetAddress address) { public static boolean isBlockedAddress(InetAddress address) {
// Check allow-list first: allowed addresses bypass all block checks
AllowedHosts currentAllowed = allowedHosts;
for (CidrRange cidr : currentAllowed.cidrRanges) {
if (cidr.contains(address)) {
return false;
}
}
// Covers 127.0.0.0/8 and ::1 // Covers 127.0.0.0/8 and ::1
if (address.isLoopbackAddress()) { if (address.isLoopbackAddress()) {
return true; return true;
@ -142,14 +158,37 @@ public class SsrfProtectionValidator {
throw new RuntimeException("URI is invalid: host '" + host + "' is not allowed"); throw new RuntimeException("URI is invalid: host '" + host + "' is not allowed");
} }
public static boolean isEnabled() {
return enabled;
}
public static void setEnabled(boolean enabled) { public static void setEnabled(boolean enabled) {
SsrfProtectionValidator.enabled = enabled; SsrfProtectionValidator.enabled = enabled;
} }
public static void setAdditionalBlockedHosts(List<String> entries) { public static void setAdditionalBlockedHosts(List<String> entries) {
ParsedHostEntries parsed = parseHostEntries(entries);
additionalBlocked = new AdditionalBlockedHosts(parsed.cidrRanges, parsed.hostnames);
if (!parsed.cidrRanges.isEmpty() || !parsed.hostnames.isEmpty()) {
log.info("SSRF additional blocked hosts configured: {} CIDR range(s), {} hostname(s)", parsed.cidrRanges.size(), parsed.hostnames.size());
}
}
public static void setAllowedHosts(List<String> entries) {
ParsedHostEntries parsed = parseHostEntries(entries);
allowedHosts = new AllowedHosts(parsed.cidrRanges, parsed.hostnames);
if (!parsed.cidrRanges.isEmpty() || !parsed.hostnames.isEmpty()) {
log.info("SSRF allowed hosts configured: {} CIDR range(s), {} hostname(s)", parsed.cidrRanges.size(), parsed.hostnames.size());
}
}
public static boolean isHostnameAllowed(String hostname) {
return allowedHosts.hostnames.contains(hostname.toLowerCase());
}
private static ParsedHostEntries parseHostEntries(List<String> entries) {
if (entries == null || entries.isEmpty()) { if (entries == null || entries.isEmpty()) {
additionalBlocked = AdditionalBlockedHosts.EMPTY; return ParsedHostEntries.EMPTY;
return;
} }
List<CidrRange> cidrRanges = new ArrayList<>(); List<CidrRange> cidrRanges = new ArrayList<>();
Set<String> hostnames = new HashSet<>(); Set<String> hostnames = new HashSet<>();
@ -168,10 +207,9 @@ public class SsrfProtectionValidator {
hostnames.add(trimmed.toLowerCase()); hostnames.add(trimmed.toLowerCase());
} }
} }
additionalBlocked = new AdditionalBlockedHosts( return new ParsedHostEntries(
Collections.unmodifiableList(cidrRanges), Collections.unmodifiableList(cidrRanges),
Collections.unmodifiableSet(hostnames)); Collections.unmodifiableSet(hostnames));
log.info("SSRF additional blocked hosts configured: {} CIDR range(s), {} hostname(s)", cidrRanges.size(), hostnames.size());
} }
private static boolean isIpLiteral(String entry) { private static boolean isIpLiteral(String entry) {
@ -179,10 +217,18 @@ public class SsrfProtectionValidator {
return !entry.isEmpty() && (Character.isDigit(entry.charAt(0)) || entry.contains(":")); return !entry.isEmpty() && (Character.isDigit(entry.charAt(0)) || entry.contains(":"));
} }
private record ParsedHostEntries(List<CidrRange> cidrRanges, Set<String> hostnames) {
static final ParsedHostEntries EMPTY = new ParsedHostEntries(Collections.emptyList(), Collections.emptySet());
}
record AdditionalBlockedHosts(List<CidrRange> cidrRanges, Set<String> hostnames) { record AdditionalBlockedHosts(List<CidrRange> cidrRanges, Set<String> hostnames) {
static final AdditionalBlockedHosts EMPTY = new AdditionalBlockedHosts(Collections.emptyList(), Collections.emptySet()); static final AdditionalBlockedHosts EMPTY = new AdditionalBlockedHosts(Collections.emptyList(), Collections.emptySet());
} }
record AllowedHosts(List<CidrRange> cidrRanges, Set<String> hostnames) {
static final AllowedHosts EMPTY = new AllowedHosts(Collections.emptyList(), Collections.emptySet());
}
record CidrRange(byte[] network, int prefixLength) { record CidrRange(byte[] network, int prefixLength) {
static CidrRange of(String ip, int prefixLength) { static CidrRange of(String ip, int prefixLength) {

242
common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java

@ -20,10 +20,12 @@ import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -335,4 +337,244 @@ public class SsrfProtectionValidatorTest {
} }
} }
// --- Allow-list tests ---
@Test
void testAllowListCidrAllowsPrivateAddress() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
// 192.168.1.1 is normally blocked (site-local), but allow-listed
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
// Other private ranges remain blocked
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://10.0.0.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListHostnameBypassesSuffixCheck() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("my-device.local"));
// .local suffix is normally blocked, but allow-listed hostname passes
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://my-device.local/api"), true));
// Other .local hostnames remain blocked
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://other-device.local/api"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListPrecedenceOverBlockList() {
try {
// Block 8.8.8.0/24 via additional-blocked, but allow 8.8.8.8 via allow-list
SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("8.8.8.0/24"));
SsrfProtectionValidator.setAllowedHosts(List.of("8.8.8.8"));
// Allow-list should win
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.8"), true));
// Adjacent IP still blocked
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.9"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testIsBlockedAddressPublicApi() throws Exception {
InetAddress loopback = InetAddress.getByName("127.0.0.1");
assertThat(SsrfProtectionValidator.isBlockedAddress(loopback)).isTrue();
InetAddress publicIp = InetAddress.getByName("8.8.8.8");
assertThat(SsrfProtectionValidator.isBlockedAddress(publicIp)).isFalse();
// Allow-listed private address
try {
SsrfProtectionValidator.setAllowedHosts(List.of("10.0.0.0/8"));
InetAddress privateIp = InetAddress.getByName("10.1.2.3");
assertThat(SsrfProtectionValidator.isBlockedAddress(privateIp)).isFalse();
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testIsEnabledAccessor() {
boolean original = SsrfProtectionValidator.isEnabled();
try {
SsrfProtectionValidator.setEnabled(true);
assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
SsrfProtectionValidator.setEnabled(false);
assertThat(SsrfProtectionValidator.isEnabled()).isFalse();
} finally {
SsrfProtectionValidator.setEnabled(original);
}
}
@Test
void testSetAllowedHostsEmptyAndNull() {
// Should not throw
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
SsrfProtectionValidator.setAllowedHosts(null);
}
@Test
void testIsHostnameAllowed() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("my-device.local", "Internal-Server.Corp"));
assertThat(SsrfProtectionValidator.isHostnameAllowed("my-device.local")).isTrue();
assertThat(SsrfProtectionValidator.isHostnameAllowed("MY-DEVICE.LOCAL")).isTrue(); // case-insensitive
assertThat(SsrfProtectionValidator.isHostnameAllowed("internal-server.corp")).isTrue();
assertThat(SsrfProtectionValidator.isHostnameAllowed("other-device.local")).isFalse();
assertThat(SsrfProtectionValidator.isHostnameAllowed("example.com")).isFalse();
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testIsHostnameAllowedEmptyList() {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
assertThat(SsrfProtectionValidator.isHostnameAllowed("anything")).isFalse();
}
@Test
void testValidateUriUsesStaticEnabledFlag() {
boolean original = SsrfProtectionValidator.isEnabled();
try {
// When enabled, loopback is blocked via the public one-arg overload
SsrfProtectionValidator.setEnabled(true);
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1")))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// When disabled, loopback passes
SsrfProtectionValidator.setEnabled(false);
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1")));
} finally {
SsrfProtectionValidator.setEnabled(original);
}
}
@Test
void testAllowListHostnameCaseInsensitive() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("My-Device.LOCAL"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://my-device.local/api"), true));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://MY-DEVICE.LOCAL/api"), true));
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListOverridesCloudMetadataRange() {
try {
// 169.254.169.254 is link-local (blocked by default), allow-list should override
SsrfProtectionValidator.setAllowedHosts(List.of("169.254.169.254"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://169.254.169.254/latest/meta-data/"), true));
// Other link-local still blocked
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://169.254.1.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListOverridesLoopback() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("127.0.0.0/8"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1"), true));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.1.2.3"), true));
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListCidrBoundary() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
// Last address in range
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.255"), true));
// First address outside range
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.2.0"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// Different subnet entirely
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.0.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testBlockedIpv6UniqueLocal() throws Exception {
// fc00::/7 covers fc00:: through fdff::
InetAddress fc00 = InetAddress.getByName("fc00::1");
assertThat(SsrfProtectionValidator.isBlockedAddress(fc00)).isTrue();
InetAddress fdAddr = InetAddress.getByName("fd12:3456:789a::1");
assertThat(SsrfProtectionValidator.isBlockedAddress(fdAddr)).isTrue();
// fe00:: is NOT in fc00::/7 (it's in fe80::/10 link-local, but fe00:: without the 80 bits is different)
// 2001:db8:: is a public documentation prefix, not blocked
InetAddress publicV6 = InetAddress.getByName("2001:db8::1");
assertThat(SsrfProtectionValidator.isBlockedAddress(publicV6)).isFalse();
}
@Test
void testParseHostEntriesWithWhitespaceAndBlanks() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of(" 192.168.1.0/24 ", "", " ", "my-host.corp"));
// Trimmed CIDR works
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
// Trimmed hostname works
assertThat(SsrfProtectionValidator.isHostnameAllowed("my-host.corp")).isTrue();
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testSetAllowedHostsReplacePrevious() {
try {
SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
// Replace with different range
SsrfProtectionValidator.setAllowedHosts(List.of("10.0.0.0/8"));
// Old range no longer allowed
assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
// New range allowed
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://10.1.2.3"), true));
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
@Test
void testAllowListHostnameBypassesBlockedHostname() {
try {
// "localhost" is in BLOCKED_HOSTNAMES; allow-listing it should let it through
SsrfProtectionValidator.setAllowedHosts(List.of("localhost"));
assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://localhost/path"), true));
} finally {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
}
} }

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

@ -511,12 +511,10 @@ public class DefaultNotifications {
rule.setTriggerConfig(defaultRule.getTriggerConfig()); rule.setTriggerConfig(defaultRule.getTriggerConfig());
if (rule.getTriggerType() == NotificationRuleTriggerType.ALARM) { if (rule.getTriggerType() == NotificationRuleTriggerType.ALARM) {
EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(rule.getTriggerType());
recipientsConfig.setEscalationTable(Map.of(0, toUUIDs(List.of(targets)))); recipientsConfig.setEscalationTable(Map.of(0, toUUIDs(List.of(targets))));
rule.setRecipientsConfig(recipientsConfig); rule.setRecipientsConfig(recipientsConfig);
} else { } else {
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(rule.getTriggerType());
recipientsConfig.setTriggerType(rule.getTriggerType());
recipientsConfig.setTargets(toUUIDs(List.of(targets))); recipientsConfig.setTargets(toUUIDs(List.of(targets)));
rule.setRecipientsConfig(recipientsConfig); rule.setRecipientsConfig(recipientsConfig);
} }

8
dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service.validator;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.MapperType;
@ -28,6 +29,8 @@ import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType;
import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import java.net.URI;
@Component @Component
@AllArgsConstructor @AllArgsConstructor
public class Oauth2ClientDataValidator extends DataValidator<OAuth2Client> { public class Oauth2ClientDataValidator extends DataValidator<OAuth2Client> {
@ -64,6 +67,11 @@ public class Oauth2ClientDataValidator extends DataValidator<OAuth2Client> {
if (StringUtils.isEmpty(customConfig.getUrl())) { if (StringUtils.isEmpty(customConfig.getUrl())) {
throw new DataValidationException("Custom mapper URL should be specified!"); throw new DataValidationException("Custom mapper URL should be specified!");
} }
try {
SsrfProtectionValidator.validateUri(new URI(customConfig.getUrl()));
} catch (Exception e) {
throw new DataValidationException("Custom mapper URL is not allowed: " + e.getMessage());
}
} }
} }
} }

14
edqs/src/main/resources/edqs.yml

@ -25,19 +25,19 @@ app:
# Application version # Application version
version: "@project.version@" version: "@project.version@"
# Zookeeper connection parameters # ZooKeeper connection parameters
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:true}" enabled: "${ZOOKEEPER_ENABLED:true}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.

64
msa/js-executor/config/default.yml

@ -14,46 +14,56 @@
# limitations under the License. # limitations under the License.
# #
queue_type: "kafka" # Queue configuration
request_topic: "js_eval.requests" # Defines the messaging queue backend used to dispatch JS evaluation requests to the executor and receive results.
queue_prefix: "" queue_type: "kafka" # Queue type used for JS evaluation requests: kafka or in-memory
http_port: "8888" # /livenessProbe request_topic: "js_eval.requests" # Kafka topic name for incoming JS evaluation requests
queue_prefix: "" # Optional prefix applied to all queue/topic names; useful for environment isolation
http_port: "8888" # HTTP port exposing the /livenessProbe health check endpoint
# JavaScript execution configuration
# Controls response polling behavior and output size limits for individual JS script executions.
js: js:
response_poll_interval: "25" response_poll_interval: "25" # Interval in milliseconds to poll for JS execution responses
max_result_size: "300000" max_result_size: "300000" # Maximum allowed size in bytes of a JS execution result
# Kafka configuration
# Connection, producer, and topic settings for the Kafka message broker used as the JS evaluation queue.
kafka: kafka:
bootstrap: bootstrap:
# Kafka Bootstrap Servers # Kafka Bootstrap Servers
servers: "localhost:9092" servers: "localhost:9092"
replication_factor: "1" replication_factor: "1" # Number of replicas for each Kafka topic partition
acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge
batch_size: "128" # for producer batch_size: "128" # Maximum number of messages to accumulate before triggering a batch send
linger_ms: "5" # for producer linger_ms: "5" # Time in milliseconds the producer waits before sending a batch, allowing more messages to accumulate
partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency
requestTimeout: "30000" # The default value in kafkajs is: 30000 requestTimeout: "30000" # Time in milliseconds to wait for a response to a Kafka request before timing out
connectionTimeout: "1000" # The default value in kafkajs is: 1000 connectionTimeout: "1000" # Time in milliseconds to wait when establishing a connection to a Kafka broker
compression: "none" # gzip or uncompressed compression: "none" # Message compression codec for the producer: none, gzip
topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1" topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1" # Semicolon-separated Kafka topic configuration properties applied on topic creation
use_confluent_cloud: false use_confluent_cloud: false # Set to true to enable Confluent Cloud-specific configuration (SSL + SASL)
client_id: "kafkajs" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh client_id: "kafkajs" # Kafka client identifier; inject the pod name to easily identify the client via kafka-consumer-groups.sh
ssl: ssl:
enabled: false enabled: false # Enable SSL/TLS for Kafka broker connections
confluent: confluent:
sasl: sasl:
mechanism: "PLAIN" mechanism: "PLAIN" # SASL mechanism for Confluent Cloud authentication: PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512
# Logging configuration
# Controls log verbosity, output directory, and log file naming pattern for the JS executor service.
logger: logger:
level: "info" level: "info" # Log level: trace, debug, info, warn, or error
path: "logs" path: "logs" # Directory path where log files are written
filename: "tb-js-executor-%DATE%.log" filename: "tb-js-executor-%DATE%.log" # Log file name pattern; %DATE% is replaced with the current date
# JavaScript execution and monitoring configuration
# Sandboxing, script caching, and observability settings for the JS script runtime.
script: script:
use_sandbox: "true" use_sandbox: "true" # Run scripts inside an isolated sandbox to prevent access to Node.js internals
memory_usage_trace_frequency: "1000" memory_usage_trace_frequency: "1000" # Log memory usage every N script executions
script_body_trace_frequency: "10000" script_body_trace_frequency: "10000" # Log the script body every N script executions
stat_print_frequency: "10000" stat_print_frequency: "10000" # Print execution statistics every N script executions
max_active_scripts: "1000" max_active_scripts: "1000" # Maximum number of compiled scripts kept in the cache
slow_query_log_ms: "5.000000" #millis slow_query_log_ms: "5.000000" # Execution time threshold in milliseconds above which a script is considered slow and logged
slow_query_log_body: "false" slow_query_log_body: "false" # Log the script body when execution time exceeds slow_query_log_ms threshold

12
msa/js-executor/yarn.lock

@ -1036,9 +1036,9 @@ mimic-response@^3.1.0:
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.1.2: minimatch@^3.1.2:
version "3.1.3" version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA== integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies: dependencies:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
@ -1549,9 +1549,9 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1" readable-stream "^3.1.1"
tar@>=7.5.8, tar@^7.4.3: tar@>=7.5.8, tar@^7.4.3:
version "7.5.9" version "7.5.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8" resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.11.tgz#1250fae45d98806b36d703b30973fa8e0a6d8868"
integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg== integrity sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==
dependencies: dependencies:
"@isaacs/fs-minipass" "^4.0.0" "@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0" chownr "^3.0.0"

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

@ -15,30 +15,33 @@
# #
# Spring common parameters # Spring common parameters
# Controls Spring Boot startup mode, web environment type, and bean wiring behaviour.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters # Server common parameters
# Configures the embedded HTTP server bind address and port used for actuator and metrics endpoints.
server: server:
# Server bind address (has no effect if web-environment is disabled). # Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled). # Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8086}" port: "${HTTP_BIND_PORT:8086}"
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:true}" enabled: "${ZOOKEEPER_ENABLED:true}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Queue configuration parameters # Queue configuration parameters
# Defines the message queue type (Kafka or in-memory) and all related producer, consumer, and topic settings.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -196,6 +200,7 @@ queue:
msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}" msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}"
# Version control parameters # Version control parameters
# Configures the Git-based version control executor, including thread pools and local repository storage.
vc: vc:
# Pool size for handling export tasks # Pool size for handling export tasks
thread_pool_size: "${TB_VC_POOL_SIZE:6}" thread_pool_size: "${TB_VC_POOL_SIZE:6}"
@ -206,6 +211,7 @@ vc:
repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}" repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}"
# Usage statistics parameters # Usage statistics parameters
# Controls collection and reporting of API usage statistics at system, tenant, and customer levels.
usage: usage:
stats: stats:
report: report:
@ -221,6 +227,7 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables actuator-based metrics collection and configures histogram/timer percentile exports.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
@ -229,6 +236,7 @@ metrics:
percentiles: "${METRICS_TIMER_PERCENTILES:0.5}" percentiles: "${METRICS_TIMER_PERCENTILES:0.5}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure, including the metrics and info endpoints.
management: management:
endpoints: endpoints:
web: web:
@ -237,12 +245,14 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Service common properties # Service common properties
# Identifies this microservice instance by type and optional unique ID for cluster coordination.
service: service:
type: "${TB_SERVICE_TYPE:tb-vc-executor}" # service type type: "${TB_SERVICE_TYPE:tb-vc-executor}" # service type
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Notification system parameters # Notification system parameters
# Defines deduplication rules for notification triggers to prevent repeated alerts within a time window.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

14
msa/web-ui/config/custom-environment-variables.yml

@ -25,6 +25,20 @@ thingsboard:
host: "TB_HOST" host: "TB_HOST"
# ThingsBoard node port # ThingsBoard node port
port: "TB_PORT" port: "TB_PORT"
security:
headers:
x-content-type-options:
enabled: "SECURITY_HEADERS_X_CONTENT_TYPE_OPTIONS_ENABLED"
referrer-policy:
enabled: "SECURITY_HEADERS_REFERRER_POLICY_ENABLED"
value: "SECURITY_HEADERS_REFERRER_POLICY_VALUE"
x-frame-options:
enabled: "SECURITY_HEADERS_X_FRAME_OPTIONS_ENABLED"
value: "SECURITY_HEADERS_X_FRAME_OPTIONS_VALUE"
content-security-policy:
enabled: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_ENABLED"
value: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_VALUE"
report-only: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_REPORT_ONLY"
logger: logger:
level: "LOGGER_LEVEL" level: "LOGGER_LEVEL"
path: "LOG_FOLDER" path: "LOG_FOLDER"

14
msa/web-ui/config/default.yml

@ -25,6 +25,20 @@ thingsboard:
host: "localhost" host: "localhost"
# ThingsBoard node port # ThingsBoard node port
port: "8080" port: "8080"
security:
headers:
x-content-type-options:
enabled: true
referrer-policy:
enabled: true
value: "strict-origin-when-cross-origin"
x-frame-options:
enabled: false
value: "SAMEORIGIN"
content-security-policy:
enabled: false
value: ""
report-only: false
logger: logger:
level: "info" level: "info"
path: "logs" path: "logs"

30
msa/web-ui/server.ts

@ -60,6 +60,36 @@ let connections: Socket[] = [];
const app = express(); const app = express();
server = http.createServer(app); server = http.createServer(app);
// Build security headers map once at startup.
// node-config passes env var overrides as strings, so enabled can be boolean or string.
const isEnabled = (val: any) => val === true || val === 'true';
const securityHeaders: Record<string, string> = {};
const hc: any = config.get('security.headers');
if (isEnabled(hc['x-content-type-options']?.enabled)) {
securityHeaders['X-Content-Type-Options'] = 'nosniff';
}
if (isEnabled(hc['referrer-policy']?.enabled)) {
securityHeaders['Referrer-Policy'] = hc['referrer-policy']?.value || 'strict-origin-when-cross-origin';
}
if (isEnabled(hc['x-frame-options']?.enabled)) {
securityHeaders['X-Frame-Options'] = hc['x-frame-options']?.value || 'SAMEORIGIN';
}
if (isEnabled(hc['content-security-policy']?.enabled) && hc['content-security-policy']?.value) {
const csp = hc['content-security-policy'];
const name = isEnabled(csp['report-only'])
? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
securityHeaders[name] = csp.value;
}
logger.info('Security headers: %s', JSON.stringify(securityHeaders));
// Apply security headers to all responses
app.use((_req, res, next) => {
for (const [name, value] of Object.entries(securityHeaders)) {
res.setHeader(name, value);
}
next();
});
let apiProxy: httpProxy; let apiProxy: httpProxy;
if (useApiProxy) { if (useApiProxy) {
apiProxy = httpProxy.createProxyServer({ apiProxy = httpProxy.createProxyServer({

12
msa/web-ui/yarn.lock

@ -1098,9 +1098,9 @@ mimic-response@^3.1.0:
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.1.2: minimatch@^3.1.2:
version "3.1.3" version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA== integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies: dependencies:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
@ -1631,9 +1631,9 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1" readable-stream "^3.1.1"
tar@>=7.5.8, tar@^7.4.3: tar@>=7.5.8, tar@^7.4.3:
version "7.5.9" version "7.5.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8" resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.11.tgz#1250fae45d98806b36d703b30973fa8e0a6d8868"
integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg== integrity sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==
dependencies: dependencies:
"@isaacs/fs-minipass" "^4.0.0" "@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0" chownr "^3.0.0"

29
pom.xml

@ -63,7 +63,7 @@
<leshan.version>2.0.0-M15</leshan.version> <leshan.version>2.0.0-M15</leshan.version>
<mail.version>2.0.1</mail.version> <mail.version>2.0.1</mail.version>
<curator.version>5.6.0</curator.version> <curator.version>5.6.0</curator.version>
<zookeeper.version>3.9.3</zookeeper.version> <zookeeper.version>3.9.5</zookeeper.version> <!-- to fix CVE-2026-24308 and CVE-2026-24281. TODO: remove override when fixed in curator-client -->
<protobuf.version>3.25.5</protobuf.version> <!-- A Major v4 does not support by the pubsub yet--> <protobuf.version>3.25.5</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<grpc.version>1.76.0</grpc.version> <grpc.version>1.76.0</grpc.version>
<tbel.version>1.2.9</tbel.version> <tbel.version>1.2.9</tbel.version>
@ -122,6 +122,7 @@
<perfmark-api.version>0.27.0</perfmark-api.version> <perfmark-api.version>0.27.0</perfmark-api.version>
<threetenbp.version>1.7.0</threetenbp.version> <threetenbp.version>1.7.0</threetenbp.version>
<!-- TEST SCOPE --> <!-- TEST SCOPE -->
<assertj.version>3.27.7</assertj.version> <!-- to fix CVE-2026-24400 (XXE). TODO: remove when fixed in spring-boot-dependencies -->
<dbunit.version>2.7.3</dbunit.version> <dbunit.version>2.7.3</dbunit.version>
<java-websocket.version>1.5.6</java-websocket.version> <java-websocket.version>1.5.6</java-websocket.version>
<mock-server.version>5.15.0</mock-server.version> <mock-server.version>5.15.0</mock-server.version>
@ -533,6 +534,8 @@
<arg>-PpkgInstallFolder=${pkg.installFolder}</arg> <arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
<arg>-PpkgCopyInstallScripts=${pkg.copyInstallScripts}</arg> <arg>-PpkgCopyInstallScripts=${pkg.copyInstallScripts}</arg>
<arg>-PpkgLogFolder=${pkg.unixLogFolder}</arg> <arg>-PpkgLogFolder=${pkg.unixLogFolder}</arg>
<arg>--project-cache-dir</arg>
<arg>${project.build.directory}/.gradle</arg>
<arg>--warning-mode</arg> <arg>--warning-mode</arg>
<arg>all</arg> <arg>all</arg>
</args> </args>
@ -890,6 +893,21 @@
<groupId>com.mycila</groupId> <groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId> <artifactId>license-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<inherited>false</inherited>
<configuration>
<filesets>
<fileset>
<directory>${main.dir}/packaging/java/.gradle</directory>
</fileset>
<fileset>
<directory>${main.dir}/packaging/js/.gradle</directory>
</fileset>
</filesets>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
@ -931,6 +949,15 @@
</dependency> </dependency>
<!-- End of Jackson version override --> <!-- End of Jackson version override -->
<!-- Temporary assertj-core version override -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<!-- End of assertj-core version override -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId> <artifactId>spring-boot-dependencies</artifactId>

174
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java

@ -0,0 +1,174 @@
/**
* Copyright © 2016-2026 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.rest;
import io.netty.resolver.AddressResolver;
import io.netty.resolver.AddressResolverGroup;
import io.netty.resolver.DefaultAddressResolverGroup;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import org.thingsboard.common.util.SsrfProtectionValidator;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Custom Netty {@link AddressResolverGroup} that validates every resolved IP address
* against the SSRF block-list at connection time. This eliminates the DNS rebinding
* TOCTOU gap where a hostname resolves to a safe IP during validation but to a
* private/metadata IP when the actual connection is made.
* <p>
* Only wired into {@link TbHttpClient} when SSRF protection is enabled.
*/
public final class SsrfSafeAddressResolverGroup extends AddressResolverGroup<InetSocketAddress> {
public static final SsrfSafeAddressResolverGroup INSTANCE = new SsrfSafeAddressResolverGroup();
private SsrfSafeAddressResolverGroup() {
}
@Override
protected AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) throws Exception {
AddressResolver<InetSocketAddress> delegate = DefaultAddressResolverGroup.INSTANCE.getResolver(executor);
return new SsrfValidatingResolver(executor, delegate);
}
private static final class SsrfValidatingResolver implements AddressResolver<InetSocketAddress> {
private final EventExecutor executor;
private final AddressResolver<InetSocketAddress> delegate;
SsrfValidatingResolver(EventExecutor executor, AddressResolver<InetSocketAddress> delegate) {
this.executor = executor;
this.delegate = delegate;
}
@Override
public boolean isSupported(SocketAddress address) {
return delegate.isSupported(address);
}
@Override
public boolean isResolved(SocketAddress address) {
return delegate.isResolved(address);
}
@Override
public Future<InetSocketAddress> resolve(SocketAddress address) {
return resolve(address, executor.newPromise());
}
@Override
public Future<InetSocketAddress> resolve(SocketAddress address, Promise<InetSocketAddress> promise) {
delegate.resolve(address).addListener((Future<InetSocketAddress> future) -> {
try {
if (!future.isSuccess()) {
promise.tryFailure(future.cause());
return;
}
InetSocketAddress resolved = future.getNow();
if (isOriginalHostAllowed(address)) {
promise.trySuccess(resolved);
} else if (isBlocked(resolved)) {
promise.tryFailure(new RuntimeException(
"URI is invalid: host '" + getHostString(address) + "' is not allowed"));
} else {
promise.trySuccess(resolved);
}
} catch (Exception e) {
promise.tryFailure(e);
}
});
return promise;
}
@Override
public Future<List<InetSocketAddress>> resolveAll(SocketAddress address) {
return resolveAll(address, executor.newPromise());
}
@Override
public Future<List<InetSocketAddress>> resolveAll(SocketAddress address, Promise<List<InetSocketAddress>> promise) {
delegate.resolveAll(address).addListener((Future<List<InetSocketAddress>> future) -> {
try {
if (!future.isSuccess()) {
promise.tryFailure(future.cause());
return;
}
List<InetSocketAddress> resolved = future.getNow();
if (isOriginalHostAllowed(address)) {
promise.trySuccess(resolved);
return;
}
Set<InetSocketAddress> blocked = null;
for (InetSocketAddress addr : resolved) {
if (isBlocked(addr)) {
if (blocked == null) {
blocked = new HashSet<>(2);
}
blocked.add(addr);
}
}
if (blocked == null) {
promise.trySuccess(resolved);
} else if (blocked.size() == resolved.size()) {
promise.tryFailure(new RuntimeException(
"URI is invalid: host '" + getHostString(address) + "' is not allowed"));
} else {
List<InetSocketAddress> safe = new ArrayList<>(resolved.size() - blocked.size());
for (InetSocketAddress addr : resolved) {
if (!blocked.contains(addr)) {
safe.add(addr);
}
}
promise.trySuccess(safe);
}
} catch (Exception e) {
promise.tryFailure(e);
}
});
return promise;
}
@Override
public void close() {
delegate.close();
}
private static boolean isBlocked(InetSocketAddress socketAddress) {
InetAddress addr = socketAddress.getAddress();
return addr != null && SsrfProtectionValidator.isBlockedAddress(addr);
}
private static boolean isOriginalHostAllowed(SocketAddress address) {
if (address instanceof InetSocketAddress isa) {
String host = isa.getHostString();
return host != null && SsrfProtectionValidator.isHostnameAllowed(host);
}
return false;
}
private static String getHostString(SocketAddress address) {
return address instanceof InetSocketAddress isa ? isa.getHostString() : address.toString();
}
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -103,6 +103,7 @@ public class TbHttpClient {
.build(); .build();
HttpClient httpClient = HttpClient.create(connectionProvider) HttpClient httpClient = HttpClient.create(connectionProvider)
.followRedirect(false)
.runOn(getSharedOrCreateEventLoopGroup(eventLoopGroupShared)) .runOn(getSharedOrCreateEventLoopGroup(eventLoopGroupShared))
.doOnConnected(c -> .doOnConnected(c ->
c.addHandlerLast(new ReadTimeoutHandler(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS))); c.addHandlerLast(new ReadTimeoutHandler(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)));
@ -138,6 +139,10 @@ public class TbHttpClient {
httpClient = httpClient.secure(t -> t.sslContext(sslContext)); httpClient = httpClient.secure(t -> t.sslContext(sslContext));
} }
if (SsrfProtectionValidator.isEnabled()) {
httpClient = httpClient.resolver(SsrfSafeAddressResolverGroup.INSTANCE);
}
validateMaxInMemoryBufferSize(config); validateMaxInMemoryBufferSize(config);
this.webClient = WebClient.builder() this.webClient = WebClient.builder()

161
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java

@ -0,0 +1,161 @@
/**
* Copyright © 2016-2026 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.rest;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.resolver.AddressResolver;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Promise;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.thingsboard.common.util.SsrfProtectionValidator;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ResourceLock("SsrfSafeAddressResolverGroupTest")
class SsrfSafeAddressResolverGroupTest {
private static NioEventLoopGroup eventLoopGroup;
@BeforeAll
static void setUp() {
eventLoopGroup = new NioEventLoopGroup(1);
}
@AfterAll
static void tearDown() {
eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
SsrfProtectionValidator.setEnabled(false);
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
@BeforeEach
void enableSsrf() {
SsrfProtectionValidator.setEnabled(true);
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
}
@AfterEach
void resetState() {
SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
SsrfProtectionValidator.setEnabled(false);
}
@Test
void isBlockedAddressWorksForLoopback() throws Exception {
assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("127.0.0.1"))).isTrue();
assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("192.168.1.1"))).isTrue();
assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("8.8.8.8"))).isFalse();
}
@Test
void resolvePublicIpSucceeds() throws Exception {
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<InetSocketAddress> promise = executor.newPromise();
executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("8.8.8.8", 80), promise));
InetSocketAddress result = promise.get(10, TimeUnit.SECONDS);
assertThat(result.getAddress()).isNotNull();
assertThat(result.getAddress().getHostAddress()).isEqualTo("8.8.8.8");
}
@Test
void resolveLoopbackFailsWhenSsrfEnabled() throws Exception {
assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<InetSocketAddress> promise = executor.newPromise();
executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("127.0.0.1", 80), promise));
assertThatThrownBy(() -> promise.get(10, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.hasRootCauseInstanceOf(RuntimeException.class)
.rootCause().hasMessageContaining("is not allowed");
}
@Test
void resolvePrivateIpFailsWhenSsrfEnabled() throws Exception {
assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<InetSocketAddress> promise = executor.newPromise();
executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("192.168.1.1", 80), promise));
assertThatThrownBy(() -> promise.get(10, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.hasRootCauseInstanceOf(RuntimeException.class)
.rootCause().hasMessageContaining("is not allowed");
}
@Test
void resolveAllowedPrivateIpSucceeds() throws Exception {
SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<InetSocketAddress> promise = executor.newPromise();
executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("192.168.1.1", 80), promise));
InetSocketAddress result = promise.get(10, TimeUnit.SECONDS);
assertThat(result.getAddress().getHostAddress()).isEqualTo("192.168.1.1");
}
@Test
void resolveAllPublicIpSucceeds() throws Exception {
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<List<InetSocketAddress>> promise = executor.newPromise();
executor.submit(() -> resolver.resolveAll(InetSocketAddress.createUnresolved("8.8.8.8", 80), promise));
List<InetSocketAddress> results = promise.get(10, TimeUnit.SECONDS);
assertThat(results).isNotEmpty();
assertThat(results.get(0).getAddress().getHostAddress()).isEqualTo("8.8.8.8");
}
@Test
void resolveAllPrivateIpFailsWhenSsrfEnabled() {
assertThatThrownBy(() -> {
EventExecutor executor = eventLoopGroup.next();
AddressResolver<InetSocketAddress> resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
Promise<List<InetSocketAddress>> promise = executor.newPromise();
executor.submit(() -> resolver.resolveAll(InetSocketAddress.createUnresolved("127.0.0.1", 80), promise));
promise.get(10, TimeUnit.SECONDS);
}).isInstanceOf(ExecutionException.class)
.hasRootCauseInstanceOf(RuntimeException.class)
.rootCause().hasMessageContaining("is not allowed");
}
}

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

@ -15,30 +15,33 @@
# #
# Spring common parameters # Spring common parameters
# Controls core Spring Boot application settings such as web environment and application type.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters # Server common parameters
# Configures the HTTP server bind address and port for the actuator/metrics web endpoint.
server: server:
# Server bind address (has no effect if web-environment is disabled). # Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled). # Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8083}" port: "${HTTP_BIND_PORT:8083}"
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters. Used for service discovery.
# Defines how this service registers itself and discovers other nodes via Apache ZooKeeper.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cache parameters # Cache parameters
# Configures the caching backend and per-entity cache settings used by the transport service.
cache: cache:
# caffeine or redis # caffeine or redis
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"
@ -55,6 +59,7 @@ cache:
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Defines connection mode, authentication, SSL, and connection pool settings for Redis or Valkey.
redis: redis:
connection: connection:
# standalone or cluster or sentinel # standalone or cluster or sentinel
@ -90,9 +95,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -172,6 +177,7 @@ transport:
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
# CoAP server parameters # CoAP server parameters
# Configures the CoAP and CoAP/DTLS server endpoints, including bind addresses, timeouts, and TLS credentials.
coap: coap:
# CoAP bind-address # CoAP bind-address
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
@ -253,6 +259,7 @@ coap:
dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}"
# Queue configuration parameters # Queue configuration parameters
# Defines the message queue backend (Kafka) and all topic, partition, and consumer settings for inter-service communication.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -406,12 +413,14 @@ queue:
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
# Service common properties # Service common properties
# Identifies this service instance within the ThingsBoard cluster.
service: service:
type: "${TB_SERVICE_TYPE:tb-transport}" # service type type: "${TB_SERVICE_TYPE:tb-transport}" # service type
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Usage statistics parameters # Usage statistics parameters
# Controls reporting of API usage metrics at system, tenant, and customer levels.
usage: usage:
stats: stats:
report: report:
@ -427,11 +436,13 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables Micrometer/Actuator metrics collection and configures the exposed management endpoints.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure for health checks and metrics scraping.
management: management:
endpoints: endpoints:
web: web:
@ -440,6 +451,7 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Notification system parameters # Notification system parameters
# Configures deduplication rules for platform notifications to prevent repeated alerts within a time window.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

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

@ -15,6 +15,7 @@
# #
# Server common properties # Server common properties
# Configures the HTTP server bind address, port, SSL/TLS settings, and HTTP/2 support.
server: server:
# Server bind address # Server bind address
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
@ -54,23 +55,25 @@ server:
enabled: "${HTTP2_ENABLED:true}" enabled: "${HTTP2_ENABLED:true}"
# Spring common parameters # Spring common parameters
# Controls Spring Boot framework-level settings such as circular references and multipart upload limits.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
spring.servlet.multipart.max-file-size: "${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}" # Total file size cannot exceed 50MB when configuring file uploads spring.servlet.multipart.max-file-size: "${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}" # Total file size cannot exceed 50MB when configuring file uploads
spring.servlet.multipart.max-request-size: "${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}" # Total request size for a multipart/form-data cannot exceed 50MB spring.servlet.multipart.max-request-size: "${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}" # Total request size for a multipart/form-data cannot exceed 50MB
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -78,6 +81,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cache parameters # Cache parameters
# Defines the cache backend type and per-entity cache configuration such as TTL and maximum size.
cache: cache:
# caffeine or redis # caffeine or redis
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"
@ -87,6 +91,7 @@ cache:
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Configures connection mode (standalone, cluster, sentinel), credentials, SSL, and connection pool settings.
redis: redis:
# standalone or cluster or sentinel # standalone or cluster or sentinel
connection: connection:
@ -123,9 +128,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -166,6 +171,7 @@ redis:
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
# HTTP server parameters # HTTP server parameters
# Configures HTTP transport request timeouts, payload limits, session management, JSON processing, logging, and statistics.
transport: transport:
http: http:
# HTTP request processing timeout in milliseconds # HTTP request processing timeout in milliseconds
@ -203,6 +209,7 @@ transport:
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
# Queue configuration parameters # Queue configuration parameters
# Configures the messaging queue backend (Kafka), topic definitions, partitioning, and per-service consumer settings.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) . prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) .
@ -355,12 +362,14 @@ queue:
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
# General service parameters # General service parameters
# Defines the service type and unique identifier used for service discovery and cluster coordination.
service: service:
type: "${TB_SERVICE_TYPE:tb-transport}" # type of service type: "${TB_SERVICE_TYPE:tb-transport}" # type of service
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Usage statistics parameters # Usage statistics parameters
# Controls reporting of API usage statistics at the system, tenant, and optional customer level.
usage: usage:
stats: stats:
report: report:
@ -376,11 +385,13 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables or disables actuator metrics collection and exposure for monitoring integrations such as Prometheus.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator management endpoints, including which endpoints are exposed over HTTP.
management: management:
endpoints: endpoints:
web: web:
@ -389,6 +400,7 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Notification system parameters # Notification system parameters
# Configures notification rules, including deduplication windows to prevent repeated alerts for the same trigger.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

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

@ -15,30 +15,33 @@
# #
# Spring common parameters # Spring common parameters
# Controls Spring Boot web environment and application type settings used for optional metrics exposure.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters # Server common parameters
# HTTP server bind address and port, effective only when the web environment is enabled.
server: server:
# Server bind address (has no effect if web-environment is disabled). # Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled). # Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8083}" port: "${HTTP_BIND_PORT:8083}"
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cache parameters # Cache parameters
# Configures the cache backend type and per-entity cache specifications such as TTL and maximum size.
cache: cache:
# caffeine or redis # caffeine or redis
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"
@ -55,6 +59,7 @@ cache:
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Connection, authentication, SSL, and connection pool settings for the Redis or Valkey cache backend.
redis: redis:
connection: connection:
# standalone or cluster or sentinel # standalone or cluster or sentinel
@ -90,9 +95,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -133,6 +138,7 @@ redis:
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
# LWM2M server parameters # LWM2M server parameters
# Session, JSON processing, RPC, logging, and LwM2M-specific transport settings including DTLS, bootstrap, and OTA.
transport: transport:
sessions: sessions:
# Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device.
@ -303,6 +309,7 @@ transport:
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
# Queue configuration properties # Queue configuration properties
# Defines the message queue backend (Kafka) and per-topic settings for transport, core, rule-engine, and JS evaluation.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -456,12 +463,14 @@ queue:
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
# Service common parameters # Service common parameters
# Identifies the service type and its unique instance ID within a ThingsBoard cluster.
service: service:
type: "${TB_SERVICE_TYPE:tb-transport}" # service type type: "${TB_SERVICE_TYPE:tb-transport}" # service type
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Usage statistics parameters # Usage statistics parameters
# Controls reporting of API usage statistics at system, tenant, and optionally customer level.
usage: usage:
stats: stats:
report: report:
@ -477,11 +486,13 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables or disables Actuator-based metrics collection (e.g., Prometheus endpoint).
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure, e.g. for Prometheus metrics scraping.
management: management:
endpoints: endpoints:
web: web:
@ -490,6 +501,7 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Notification system parameters # Notification system parameters
# Configures deduplication rules for notification triggers to prevent repeated alerts within a time window.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

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

@ -15,30 +15,33 @@
# #
# Spring common parameters # Spring common parameters
# Controls Spring Boot application type, web environment, and bean dependency settings.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters # Server common parameters
# Configures the embedded HTTP server bind address and port for actuator/metrics endpoints.
server: server:
# Server bind address (has no effect if web-environment is disabled). # Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled). # Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8083}" port: "${HTTP_BIND_PORT:8083}"
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters. Used for service discovery.
# Defines connection, session, retry settings and the node directory path for ZooKeeper-based cluster coordination.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cache parameters # Cache parameters
# Configures the cache backend type (Caffeine or Redis) and per-cache TTL and size limits.
cache: cache:
# caffeine or redis # caffeine or redis
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"
@ -55,6 +59,7 @@ cache:
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Covers connection mode (standalone, cluster, sentinel), SSL, credentials, and connection pool tuning.
redis: redis:
# standalone or cluster or sentinel # standalone or cluster or sentinel
connection: connection:
@ -91,9 +96,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -134,6 +139,7 @@ redis:
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
# MQTT server parameters # MQTT server parameters
# Configures the MQTT transport layer including bind address/port, SSL, Netty tuning, session management, rate limits, and message processing options.
transport: transport:
mqtt: mqtt:
# MQTT bind-address # MQTT bind-address
@ -236,6 +242,7 @@ transport:
ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}" ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}"
# Queue configuration parameters # Queue configuration parameters
# Defines the messaging queue backend (Kafka), topic names, partitioning, and poll/processing settings for all platform microservices.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -389,12 +396,14 @@ queue:
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
# Service common properties # Service common properties
# Identifies this service instance within the cluster by type and optional unique ID.
service: service:
type: "${TB_SERVICE_TYPE:tb-transport}" # service type type: "${TB_SERVICE_TYPE:tb-transport}" # service type
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Usage statistics parameters # Usage statistics parameters
# Controls reporting of API usage statistics at system, tenant, and customer levels with configurable intervals.
usage: usage:
stats: stats:
report: report:
@ -410,11 +419,13 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables or disables Micrometer actuator metrics collection for monitoring this service.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure, including which endpoints are accessible over HTTP.
management: management:
endpoints: endpoints:
web: web:
@ -423,6 +434,7 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Notification system parameters # Notification system parameters
# Configures deduplication rules for notification triggers to prevent repeated alerts within a defined time window.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

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

Loading…
Cancel
Save